diff options
63 files changed, 1808 insertions, 453 deletions
diff --git a/src/assets/gifs/dotted-arrow-white.gif b/src/assets/gifs/dotted-arrow-white.gif Binary files differnew file mode 100644 index 00000000..a3f0a153 --- /dev/null +++ b/src/assets/gifs/dotted-arrow-white.gif diff --git a/src/assets/icons/celebration-logo.svg b/src/assets/icons/celebration-logo.svg new file mode 100644 index 00000000..5e4e89a0 --- /dev/null +++ b/src/assets/icons/celebration-logo.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 792 792"><defs><style>.cls-1,.cls-2{fill:none;stroke:#fff;stroke-miterlimit:10;stroke-width:16px;}.cls-1{stroke-linecap:round;}</style></defs><path class="cls-1" d="M204.9,167.55,28.36,646.4S1.75,738.3,98.49,728.63l474-241.84s12.09-65.3-166.87-208c0,0-128.76-110.7-167.1-104.59s-.72,56.67-.72,56.67"/><path class="cls-1" d="M229.34,306.39S331,523.27,447.9,550.36"/><path class="cls-1" d="M253.14,60.8S384.68,150,315.57,321.65"/><line class="cls-1" x1="360.41" y1="346.18" x2="427.04" y2="228.01"/><line class="cls-1" x1="427.04" y1="339.49" x2="624.4" y2="216.5"/><path class="cls-1" d="M460.48,377.39s165-42.71,251.94,19.89"/><path class="cls-1" d="M509.83,448.45s181.48,37.33,143.76,85.09-68.67-73.61-23.17-88S809.22,484.4,768,513.07s-61.64-91.86-25.16-106.5"/><path class="cls-1" d="M102.2,292.31s61.68-84.89,0-93.63S2.53,214.54,42.88,161.89s57-50.28,11.87-55-9.49-64.09-9.49-64.09"/><rect class="cls-2" x="332.02" y="88.01" width="41.09" height="41.09" rx="7.51" transform="translate(204.55 -229.35) rotate(49.23)"/><rect class="cls-2" x="540.13" y="88.01" width="41.09" height="41.09" rx="7.51" transform="translate(276.77 -386.97) rotate(49.23)"/><rect class="cls-2" x="439.94" y="157.42" width="41.09" height="41.09" rx="7.51" transform="translate(518.03 -312.35) rotate(75.56)"/><rect class="cls-2" x="170.83" y="88.01" width="41.09" height="41.09" rx="7.51" transform="translate(148.62 -107.27) rotate(49.23)"/><rect class="cls-2" x="594.88" y="556.9" width="41.09" height="41.09" rx="7.51" transform="translate(250.85 -183.66) rotate(21.23)"/><rect class="cls-2" x="464.79" y="561.95" width="41.09" height="41.09" rx="7.51" transform="translate(686.08 -148.64) rotate(55.15)"/><rect class="cls-2" x="57.36" y="386.02" width="41.09" height="41.09" rx="7.51" transform="translate(275.76 42.94) rotate(39.4)"/><path class="cls-1" d="M128.59,421.33s31.27,145.46,170.25,203"/><path class="cls-1" d="M77.91,544.6s9.62,149.81,65.73,159.48"/></svg>
\ No newline at end of file diff --git a/src/assets/icons/down_icon.svg b/src/assets/icons/down_icon.svg new file mode 100644 index 00000000..1c3ed71a --- /dev/null +++ b/src/assets/icons/down_icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 792 792"><defs><style>.cls-1{fill:#6c8dc8;}.cls-2{fill:none;stroke:#6c8dc8;stroke-miterlimit:10;stroke-width:47.9px;}</style></defs><path class="cls-1" d="M522.4,416.32l-18.18-16.86a13.07,13.07,0,0,0-18.47.68l-62.59,67.45V248.64a22,22,0,0,0-22-22h-8.21a22,22,0,0,0-22,22V470.19l-65-70.05a13.11,13.11,0,0,0-18.5-.68l-18.18,16.86a13.1,13.1,0,0,0-.68,18.51l96.51,104,20.39,22,1.52,1.4a14,14,0,0,0,2.52,1.84,14.37,14.37,0,0,0,2.85,1.17,2.18,2.18,0,0,0,.88.2,6.27,6.27,0,0,0,1.2.2,11.3,11.3,0,0,0,1.16.08H396a11.3,11.3,0,0,0,1.16-.08,6.68,6.68,0,0,0,1.29-.2,4.48,4.48,0,0,0,.92-.24c.48-.12.92-.32,1.4-.48l1-.49a13.55,13.55,0,0,0,2.84-2l1.53-1.4,20.42-22,96.48-104A13.11,13.11,0,0,0,522.4,416.32Z"/><circle class="cls-2" cx="395.82" cy="396.18" r="369.73"/></svg>
\ No newline at end of file diff --git a/src/assets/icons/plus_icon-01.svg b/src/assets/icons/plus_icon-01.svg index 32632897..7a3b21d2 100644 --- a/src/assets/icons/plus_icon-01.svg +++ b/src/assets/icons/plus_icon-01.svg @@ -1 +1 @@ -<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 216 216"><defs><style>.cls-1{fill:none;stroke:#718dc3;stroke-miterlimit:10;stroke-width:11px;}.cls-2{fill:#718dc3;}</style></defs><circle class="cls-1" cx="108" cy="108" r="84.9"/><rect class="cls-2" x="101.05" y="59.11" width="13.91" height="97.78" rx="6.34"/><rect class="cls-2" x="101.05" y="59.11" width="13.91" height="97.78" rx="6.34" transform="translate(0 216) rotate(-90)"/></svg>
\ No newline at end of file +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 216 216"><defs><style>.cls-1{fill:none;stroke-miterlimit:10;stroke-width:11px;}</style></defs><circle stroke="currentColor" class="cls-1" cx="108" cy="108" r="84.9" /><rect class="cls-2" x="101.05" y="59.11" width="13.91" height="97.78" rx="6.34" fill="currentColor"/><rect class="cls-2" x="101.05" y="59.11" width="13.91" height="97.78" rx="6.34" transform="translate(0 216) rotate(-90)" fill="currentColor" /></svg>
\ No newline at end of file diff --git a/src/assets/icons/up_icon.svg b/src/assets/icons/up_icon.svg new file mode 100644 index 00000000..20773a8a --- /dev/null +++ b/src/assets/icons/up_icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 792 792"><defs><style>.cls-1{fill:#6c8dc8;}.cls-2{fill:none;stroke:#718cc2;stroke-miterlimit:10;stroke-width:47.9px;}</style></defs><path class="cls-1" d="M522.58,375.86l-18.19,16.86a13.06,13.06,0,0,1-18.46-.68l-62.6-67.45v219a22,22,0,0,1-22,22H393.1a22,22,0,0,1-22-22V322l-65,70.05a13.11,13.11,0,0,1-18.5.68l-18.19-16.86a13.12,13.12,0,0,1-.68-18.51l96.52-104,20.39-22,1.52-1.4a13.6,13.6,0,0,1,2.52-1.85,14.44,14.44,0,0,1,2.85-1.16,2.18,2.18,0,0,1,.88-.2,6.27,6.27,0,0,1,1.2-.2,11.3,11.3,0,0,1,1.16-.08h.44a11.3,11.3,0,0,1,1.16.08,6.58,6.58,0,0,1,1.28.2,4.48,4.48,0,0,1,.92.24c.48.12.93.32,1.41.48l1,.48a13.24,13.24,0,0,1,2.84,2l1.52,1.4,20.43,22,96.48,104A13.12,13.12,0,0,1,522.58,375.86Z"/><circle class="cls-2" cx="396" cy="396" r="369.73"/></svg>
\ No newline at end of file diff --git a/src/assets/moment-categories/custom-icon.png b/src/assets/moment-categories/custom-icon.png Binary files differnew file mode 100644 index 00000000..f81546d6 --- /dev/null +++ b/src/assets/moment-categories/custom-icon.png diff --git a/src/components/comments/CommentsCount.tsx b/src/components/comments/CommentsCount.tsx index 325e2788..f4f8197d 100644 --- a/src/components/comments/CommentsCount.tsx +++ b/src/components/comments/CommentsCount.tsx @@ -30,7 +30,7 @@ const CommentsCount: React.FC<CommentsCountProps> = ({ }; return ( <> - <TouchableOpacity onPress={() => navigateToCommentsScreen()}> + <TouchableOpacity onPress={navigateToCommentsScreen}> <CommentIcon style={styles.image} /> <Text style={styles.count}> {commentsCount !== '0' ? commentsCount : ''} diff --git a/src/components/common/TaggPopup.tsx b/src/components/common/TaggPopup.tsx index db24adb8..86a472b1 100644 --- a/src/components/common/TaggPopup.tsx +++ b/src/components/common/TaggPopup.tsx @@ -31,7 +31,11 @@ const TaggPopup: React.FC<TaggPopupProps> = ({route, navigation}) => { const {messageHeader, messageBody, next} = route.params.popupProps; return ( - <View style={styles.container}> + <TouchableOpacity + style={styles.container} + onPressOut={() => { + navigation.goBack(); + }}> <View style={styles.popup}> <Image style={styles.icon} @@ -61,7 +65,7 @@ const TaggPopup: React.FC<TaggPopupProps> = ({route, navigation}) => { /> </View> )} - </View> + </TouchableOpacity> ); }; @@ -92,23 +96,23 @@ const styles = StyleSheet.create({ }, header: { color: '#fff', - fontSize: 16, + fontSize: SCREEN_WIDTH / 25, fontWeight: '600', textAlign: 'justify', marginBottom: '2%', - marginHorizontal: '2%', + marginLeft: '4%', }, subtext: { color: '#fff', - fontSize: 12, + fontSize: SCREEN_WIDTH / 30, fontWeight: '600', textAlign: 'justify', marginBottom: '15%', - marginHorizontal: '2%', + marginLeft: '3%', }, popup: { width: SCREEN_WIDTH * 0.8, - height: SCREEN_WIDTH * 0.2, + height: SCREEN_WIDTH * 0.24, backgroundColor: 'black', borderRadius: 8, flexDirection: 'row', @@ -116,6 +120,7 @@ const styles = StyleSheet.create({ flexWrap: 'wrap', position: 'absolute', bottom: SCREEN_HEIGHT * 0.7, + padding: SCREEN_WIDTH / 40, }, footer: { marginLeft: '50%', diff --git a/src/components/common/TaggPrompt.tsx b/src/components/common/TaggPrompt.tsx new file mode 100644 index 00000000..5cd3ac3f --- /dev/null +++ b/src/components/common/TaggPrompt.tsx @@ -0,0 +1,79 @@ +import * as React from 'react'; +import {Platform, Text, StyleSheet, TouchableOpacity} from 'react-native'; +import {Image, View} from 'react-native-animatable'; +import {SCREEN_HEIGHT} from '../../utils'; +import CloseIcon from '../../assets/ionicons/close-outline.svg'; + +type TaggPromptProps = { + messageHeader: string; + messageBody: string; + logoType: string; + onClose: () => void; +}; + +const TaggPrompt: React.FC<TaggPromptProps> = ({ + messageHeader, + messageBody, + logoType, + onClose, +}) => { + /** + * Generic prompt for Tagg + */ + + return ( + <View style={styles.container}> + <Image + style={styles.icon} + source={require('../../assets/icons/plus-logo.png')} + /> + <Text style={styles.header}>{messageHeader}</Text> + <Text style={styles.subtext}>{messageBody}</Text> + <TouchableOpacity + style={styles.closeButton} + onPress={() => { + onClose(); + }}> + <CloseIcon height={'50%'} width={'50%'} color="gray" /> + </TouchableOpacity> + </View> + ); +}; + +const styles = StyleSheet.create({ + container: { + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'white', + height: SCREEN_HEIGHT / 4.5, + paddingTop: SCREEN_HEIGHT / 10, + paddingBottom: SCREEN_HEIGHT / 50, + }, + closeButton: { + position: 'relative', + height: '40%', + bottom: SCREEN_HEIGHT / 6, + aspectRatio: 1, + alignSelf: 'flex-end', + }, + icon: { + width: 40, + height: 40, + }, + header: { + color: 'black', + fontSize: 16, + fontWeight: '600', + textAlign: 'center', + marginTop: '2%', + }, + subtext: { + color: 'gray', + fontSize: 12, + fontWeight: '500', + textAlign: 'center', + marginTop: '2%', + }, +}); +export default TaggPrompt; diff --git a/src/components/common/index.ts b/src/components/common/index.ts index d5d36297..9162ec70 100644 --- a/src/components/common/index.ts +++ b/src/components/common/index.ts @@ -18,3 +18,4 @@ export {default as BottomDrawer} from './BottomDrawer'; export {default as TaggLoadingTndicator} from './TaggLoadingIndicator'; export {default as GenericMoreInfoDrawer} from './GenericMoreInfoDrawer'; export {default as TaggPopUp} from './TaggPopup'; +export {default as TaggPrompt} from './TaggPrompt'; diff --git a/src/components/moments/Moment.tsx b/src/components/moments/Moment.tsx index 0d2c2b62..6dbcd170 100644 --- a/src/components/moments/Moment.tsx +++ b/src/components/moments/Moment.tsx @@ -1,25 +1,39 @@ import {useNavigation} from '@react-navigation/native'; -import React from 'react'; -import {Alert, StyleSheet, View} from 'react-native'; +import React, {Fragment} from 'react'; +import { + Alert, + StyleProp, + StyleSheet, + View, + ViewProps, + ViewStyle, +} from 'react-native'; import {Text} from 'react-native-animatable'; import {ScrollView, TouchableOpacity} from 'react-native-gesture-handler'; import LinearGradient from 'react-native-linear-gradient'; import PlusIcon from '../../assets/icons/plus_icon-01.svg'; +import UpIcon from '../../assets/icons/up_icon.svg'; +import DownIcon from '../../assets/icons/down_icon.svg'; import DeleteIcon from '../../assets/icons/delete-logo.svg'; import BigPlusIcon from '../../assets/icons/plus_icon-02.svg'; import {TAGG_TEXT_LIGHT_BLUE} from '../../constants'; import {SCREEN_WIDTH} from '../../utils'; import ImagePicker from 'react-native-image-crop-picker'; import MomentTile from './MomentTile'; -import {MomentCategoryType, MomentType, ScreenType} from 'src/types'; +import {MomentType, ScreenType} from 'src/types'; +import {useDispatch} from 'react-redux'; interface MomentProps { - title: MomentCategoryType; + title: string; images: MomentType[] | undefined; userXId: string | undefined; screenType: ScreenType; - handleMomentCategoryDelete: (_: MomentCategoryType) => void; + handleMomentCategoryDelete: (_: string) => void; shouldAllowDeletion: boolean; + showUpButton: boolean; + showDownButton: boolean; + move?: (direction: 'up' | 'down', title: string) => void; + externalStyles?: Record<string, StyleProp<ViewStyle>>; } const Moment: React.FC<MomentProps> = ({ @@ -29,8 +43,13 @@ const Moment: React.FC<MomentProps> = ({ screenType, handleMomentCategoryDelete, shouldAllowDeletion, + showUpButton, + showDownButton, + move, + externalStyles, }) => { const navigation = useNavigation(); + const dispatch = useDispatch(); const navigateToImagePicker = () => { ImagePicker.openPicker({ @@ -57,19 +76,50 @@ const Moment: React.FC<MomentProps> = ({ } }) .catch((err) => { - Alert.alert('Unable to upload moment!'); + if (err.code && err.code !== 'E_PICKER_CANCELLED') { + Alert.alert('Unable to upload moment!'); + } }); }; + return ( - <View style={styles.container}> - <View style={styles.header}> - <Text style={styles.titleText}>{title}</Text> + <View style={[styles.container, externalStyles?.container]}> + <View style={[styles.header, externalStyles?.header]}> + <Text style={[styles.titleText, externalStyles?.titleText]}> + {title} + </Text> + {!userXId ? ( + <> + {showUpButton && move && ( + <UpIcon + width={19} + height={19} + onPress={() => move('up', title)} + color={TAGG_TEXT_LIGHT_BLUE} + style={{marginLeft: 5}} + /> + )} + {showDownButton && move && ( + <DownIcon + width={19} + height={19} + onPress={() => move('down', title)} + color={TAGG_TEXT_LIGHT_BLUE} + style={{marginLeft: 5}} + /> + )} + </> + ) : ( + <Fragment /> + )} + <View style={styles.flexer} /> {!userXId ? ( <> <PlusIcon width={21} height={21} onPress={() => navigateToImagePicker()} + color={TAGG_TEXT_LIGHT_BLUE} style={{marginRight: 10}} /> {shouldAllowDeletion && ( @@ -87,7 +137,7 @@ const Moment: React.FC<MomentProps> = ({ <ScrollView horizontal showsHorizontalScrollIndicator={false} - style={styles.scrollContainer}> + style={[styles.scrollContainer, externalStyles?.scrollContainer]}> {images && images.map((imageObj: MomentType) => ( <MomentTile @@ -104,7 +154,7 @@ const Moment: React.FC<MomentProps> = ({ <View style={styles.defaultImage}> <BigPlusIcon width={24} height={24} /> <Text style={styles.defaultImageText}> - Add a moment of your {title.toLowerCase()}! + Add a moment of your {title?.toLowerCase()}! </Text> </View> </LinearGradient> @@ -123,21 +173,23 @@ const styles = StyleSheet.create({ }, header: { flex: 1, - paddingLeft: '3%', - padding: 5, - paddingTop: 20, + padding: '3%', backgroundColor: 'white', flexDirection: 'row', - justifyContent: 'space-between', alignItems: 'center', }, titleText: { fontSize: 16, fontWeight: 'bold', color: TAGG_TEXT_LIGHT_BLUE, + }, + // titleContainer: { + // flex: 1, + // flexDirection: 'row', + // justifyContent: 'flex-end', + // }, + flexer: { flex: 1, - flexDirection: 'row', - justifyContent: 'flex-end', }, scrollContainer: { height: SCREEN_WIDTH / 3.25, diff --git a/src/components/notifications/Notification.tsx b/src/components/notifications/Notification.tsx index f6a04526..184e3f27 100644 --- a/src/components/notifications/Notification.tsx +++ b/src/components/notifications/Notification.tsx @@ -1,9 +1,22 @@ import {useNavigation} from '@react-navigation/native'; import React, {useEffect, useState} from 'react'; -import {Image, StyleSheet, Text, View} from 'react-native'; +import { + ActivityIndicatorBase, + Alert, + Image, + StyleSheet, + Text, + View, +} from 'react-native'; import {TouchableWithoutFeedback} from 'react-native-gesture-handler'; -import {useDispatch, useStore} from 'react-redux'; +import {useDispatch, useSelector, useStore} from 'react-redux'; +import {MomentCommentsScreen} from '../../screens'; import {loadAvatar} from '../../services'; +import { + EMPTY_MOMENTS_LIST, + EMPTY_MOMENT_CATEGORIES, +} from '../../store/initialStates'; +import {userSocialsReducer} from '../../store/reducers'; import {RootState} from '../../store/rootReducer'; import {NotificationType, ScreenType} from '../../types'; import { @@ -15,6 +28,7 @@ import { interface NotificationProps { item: NotificationType; + userXId: string | undefined; screenType: ScreenType; } @@ -27,11 +41,16 @@ const Notification: React.FC<NotificationProps> = (props) => { notification_object, unread, }, + userXId, screenType, } = props; const navigation = useNavigation(); const state: RootState = useStore().getState(); const dispatch = useDispatch(); + const {moments: loggedInUserMoments} = + notification_type === 'CMT' + ? useSelector((state: RootState) => state.moments) + : {moments: undefined}; const [avatarURI, setAvatarURI] = useState<string | undefined>(undefined); const [momentURI, setMomentURI] = useState<string | undefined>(undefined); @@ -81,6 +100,25 @@ const Notification: React.FC<NotificationProps> = (props) => { screenType, }); break; + case 'CMT': + // find the moment we need to display + const moment = loggedInUserMoments?.find( + (m) => m.moment_id === notification_object?.moment_id, + ); + if (moment) { + navigation.push('IndividualMoment', { + moment, + userXId, + screenType, + }); + setTimeout(() => { + navigation.push('MomentCommentsScreen', { + moment_id: moment.moment_id, + screenType, + }); + }, 500); + } + break; default: break; } diff --git a/src/components/onboarding/MomentCategory.tsx b/src/components/onboarding/MomentCategory.tsx index 827ab207..97099b9e 100644 --- a/src/components/onboarding/MomentCategory.tsx +++ b/src/components/onboarding/MomentCategory.tsx @@ -1,19 +1,17 @@ -import * as React from 'react'; +import React from 'react'; import {StyleSheet} from 'react-native'; import {Image, Text} from 'react-native-animatable'; import {TouchableOpacity} from 'react-native-gesture-handler'; import LinearGradient from 'react-native-linear-gradient'; -import {BACKGROUND_GRADIENT_MAP} from '../../constants'; -import {MomentCategoryType} from '../../types'; +import { + BACKGROUND_GRADIENT_MAP, + MOMENT_CATEGORY_BG_COLORS, +} from '../../constants'; import {SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils'; type MomentCategoryProps = { - categoryType: MomentCategoryType; - onSelect: ( - category: MomentCategoryType, - isSelected: boolean, - isAdded: boolean, - ) => void; + categoryType: string; + onSelect: (category: string, isSelected: boolean, isAdded: boolean) => void; isSelected: boolean; isAdded: boolean; }; @@ -32,63 +30,75 @@ const MomentCategory: React.FC<MomentCategoryProps> = ({ switch (categoryType) { case 'Friends': icon = require('../../assets/moment-categories/friends-icon.png'); - bgColor = '#5E4AE4'; + bgColor = MOMENT_CATEGORY_BG_COLORS[0]; break; case 'Adventure': icon = require('../../assets/moment-categories/adventure-icon.png'); - bgColor = '#5044A6'; + bgColor = MOMENT_CATEGORY_BG_COLORS[1]; break; case 'Photo Dump': icon = require('../../assets/moment-categories/photo-dump-icon.png'); - bgColor = '#4755A1'; + bgColor = MOMENT_CATEGORY_BG_COLORS[2]; break; case 'Food': icon = require('../../assets/moment-categories/food-icon.png'); - bgColor = '#444BA8'; + bgColor = MOMENT_CATEGORY_BG_COLORS[3]; break; case 'Music': icon = require('../../assets/moment-categories/music-icon.png'); - bgColor = '#374898'; + bgColor = MOMENT_CATEGORY_BG_COLORS[4]; break; case 'Art': icon = require('../../assets/moment-categories/art-icon.png'); - bgColor = '#3F5C97'; + bgColor = MOMENT_CATEGORY_BG_COLORS[5]; break; case 'Sports': icon = require('../../assets/moment-categories/sports-icon.png'); - bgColor = '#3A649F'; + bgColor = MOMENT_CATEGORY_BG_COLORS[6]; break; case 'Fashion': icon = require('../../assets/moment-categories/fashion-icon.png'); - bgColor = '#386A95'; + bgColor = MOMENT_CATEGORY_BG_COLORS[7]; break; case 'Travel': icon = require('../../assets/moment-categories/travel-icon.png'); - bgColor = '#366D84'; + bgColor = MOMENT_CATEGORY_BG_COLORS[8]; break; case 'Pets': icon = require('../../assets/moment-categories/pets-icon.png'); - bgColor = '#335E76'; + bgColor = MOMENT_CATEGORY_BG_COLORS[9]; break; case 'Fitness': icon = require('../../assets/moment-categories/fitness-icon.png'); - bgColor = '#2E5471'; + bgColor = MOMENT_CATEGORY_BG_COLORS[10]; break; case 'DIY': icon = require('../../assets/moment-categories/diy-icon.png'); - bgColor = '#274765'; + bgColor = MOMENT_CATEGORY_BG_COLORS[11]; break; case 'Nature': icon = require('../../assets/moment-categories/nature-icon.png'); - bgColor = '#225363'; + bgColor = MOMENT_CATEGORY_BG_COLORS[12]; break; case 'Early Life': icon = require('../../assets/moment-categories/early-life-icon.png'); - bgColor = '#365F6A'; + bgColor = MOMENT_CATEGORY_BG_COLORS[13]; break; case 'Beauty': icon = require('../../assets/moment-categories/beauty-icon.png'); - bgColor = '#4E7175'; + bgColor = MOMENT_CATEGORY_BG_COLORS[14]; + break; + default: + // All custom categories + icon = require('../../assets/moment-categories/custom-icon.png'); + // A quick deterministic "random" color picker by summing up ascii char codees + const charCodeSum = categoryType + .split('') + .reduce((acc: number, x: string) => acc + x.charCodeAt(0), 0); + bgColor = + MOMENT_CATEGORY_BG_COLORS[ + charCodeSum % MOMENT_CATEGORY_BG_COLORS.length + ]; break; } diff --git a/src/components/onboarding/TaggBigInput.tsx b/src/components/onboarding/TaggBigInput.tsx index ba965465..4e8e1ef7 100644 --- a/src/components/onboarding/TaggBigInput.tsx +++ b/src/components/onboarding/TaggBigInput.tsx @@ -1,6 +1,7 @@ import React from 'react'; import {View, TextInput, StyleSheet, TextInputProps} from 'react-native'; import * as Animatable from 'react-native-animatable'; +import {TAGG_LIGHT_PURPLE} from '../../constants'; interface TaggBigInputProps extends TextInputProps { valid?: boolean; @@ -55,7 +56,7 @@ const styles = StyleSheet.create({ warning: { fontSize: 14, marginTop: 5, - color: '#f4ddff', + color: TAGG_LIGHT_PURPLE, maxWidth: 350, textAlign: 'center', }, diff --git a/src/components/onboarding/TaggInput.tsx b/src/components/onboarding/TaggInput.tsx index 12d99325..405564ab 100644 --- a/src/components/onboarding/TaggInput.tsx +++ b/src/components/onboarding/TaggInput.tsx @@ -1,6 +1,7 @@ import React from 'react'; import {View, TextInput, StyleSheet, TextInputProps} from 'react-native'; import * as Animatable from 'react-native-animatable'; +import {TAGG_LIGHT_PURPLE} from '../../constants'; interface TaggInputProps extends TextInputProps { valid?: boolean; @@ -52,7 +53,7 @@ const styles = StyleSheet.create({ warning: { fontSize: 14, marginTop: 5, - color: '#f4ddff', + color: TAGG_LIGHT_PURPLE, maxWidth: 350, textAlign: 'center', }, diff --git a/src/components/onboarding/TermsAndConditionsText.tsx b/src/components/onboarding/TermsAndConditionsText.tsx index 39450667..2102d613 100644 --- a/src/components/onboarding/TermsAndConditionsText.tsx +++ b/src/components/onboarding/TermsAndConditionsText.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; -import {StyleSheet, Text} from 'react-native'; +import {Linking, StyleSheet, Text} from 'react-native'; +import {TAGG_WEBSITE} from '../../constants'; const TermsAndConditionsText: React.FC = () => { const textWithBulletPoint = (data: string, style: object) => { @@ -550,8 +551,14 @@ const TermsAndConditionsText: React.FC = () => { By email: <Text style={styles.link}>support@tagg.id</Text> </Text> <Text style={styles.paraLeftAlign}> - By visiting this page on our website:{' '} - <Text style={styles.link}>https://www.tagg.id/</Text> + By visiting this page on our{' '} + <Text + style={styles.link} + onPress={() => { + Linking.openURL(TAGG_WEBSITE + 'terms-and-conditions/'); + }}> + website + </Text> </Text> </React.Fragment> ); diff --git a/src/components/profile/Content.tsx b/src/components/profile/Content.tsx index 3a304938..1d639a41 100644 --- a/src/components/profile/Content.tsx +++ b/src/components/profile/Content.tsx @@ -12,19 +12,14 @@ import { import Animated from 'react-native-reanimated'; import { CategorySelectionScreenType, - MomentCategoryType, MomentType, ProfilePreviewType, ProfileType, ScreenType, UserType, } from '../../types'; -import { - COVER_HEIGHT, - MOMENT_CATEGORIES, - TAGG_TEXT_LIGHT_BLUE, -} from '../../constants'; -import {fetchUserX, SCREEN_HEIGHT, userLogin} from '../../utils'; +import {COVER_HEIGHT, TAGG_TEXT_LIGHT_BLUE} from '../../constants'; +import {fetchUserX, moveCategory, SCREEN_HEIGHT, userLogin} from '../../utils'; import TaggsBar from '../taggs/TaggsBar'; import {Moment} from '../moments'; import ProfileBody from './ProfileBody'; @@ -45,12 +40,12 @@ import { NO_PROFILE, EMPTY_PROFILE_PREVIEW_LIST, EMPTY_MOMENTS_LIST, - MOMENT_CATEGORIES_MAP, } from '../../store/initialStates'; import {Cover} from '.'; import {TouchableOpacity} from 'react-native-gesture-handler'; -import {useNavigation} from '@react-navigation/native'; +import {useFocusEffect, useNavigation} from '@react-navigation/native'; import GreyPlusLogo from '../../assets/icons/grey-plus-logo.svg'; +import {TaggPrompt} from '../common'; interface ContentProps { y: Animated.Value<number>; @@ -77,7 +72,7 @@ const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => { ? useSelector((state: RootState) => state.userX[screenType][userXId]) : useSelector((state: RootState) => state.moments); - const {momentCategories = MOMENT_CATEGORIES_MAP} = userXId + const {momentCategories = []} = userXId ? useSelector((state: RootState) => state.userX[screenType][userXId]) : useSelector((state: RootState) => state.momentCategories); @@ -103,12 +98,15 @@ const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => { const [shouldBounce, setShouldBounce] = useState<boolean>(true); const [refreshing, setRefreshing] = useState<boolean>(false); - /** - * Filter list of categories already selected by user - */ - const userMomentCategories = MOMENT_CATEGORIES.filter( - (category) => momentCategories[category] === true, + const [isStageTwoPromptClosed, setIsStageTwoPromptClosed] = useState<boolean>( + false, + ); + const [isStageOnePromptClosed, setIsStageOnePromptClosed] = useState<boolean>( + false, ); + const [isStageThreePromptClosed, setIsStageThreePromptClosed] = useState< + boolean + >(false); const onRefresh = useCallback(() => { const refrestState = async () => { @@ -146,6 +144,61 @@ const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => { createImagesMap(); }, [createImagesMap]); + const move = (direction: 'up' | 'down', title: string) => { + let categories = [...momentCategories]; + categories = moveCategory(categories, title, direction === 'up'); + dispatch(updateMomentCategories(categories, false)); + }; + + /** + * Prompt user to perform an activity based on their profile completion stage + * To fire 2 seconds after the screen comes in focus + * 1 means STAGE_1: + * The user must upload a moment, so take them to a screen guiding them to post a moment + * 2 means STAGE_2: + * The user must create another category so show a prompt on top of the screen + * 3 means STAGE_3: + * The user must upload a moment to the second category, so show a prompt on top of the screen + * Else, profile is complete and no prompt needs to be shown + */ + useFocusEffect( + useCallback(() => { + const navigateToMomentUploadPrompt = () => { + switch (profile.profile_completion_stage) { + case 1: + if ( + momentCategories && + momentCategories[0] && + !isStageOnePromptClosed + ) { + navigation.navigate('MomentUploadPrompt', { + screenType, + momentCategory: momentCategories[0], + }); + setIsStageOnePromptClosed(true); + } + break; + case 2: + setIsStageTwoPromptClosed(false); + break; + case 3: + setIsStageThreePromptClosed(false); + break; + default: + break; + } + }; + if (!userXId) { + setTimeout(navigateToMomentUploadPrompt, 2000); + } + }, [ + profile.profile_completion_stage, + momentCategories, + userXId, + isStageOnePromptClosed, + ]), + ); + /** * This hook is called on load of profile and when you update the friends list. */ @@ -226,7 +279,7 @@ const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => { * Confirm with user before deleting the category * @param category category to be deleted */ - const handleCategoryDeletion = (category: MomentCategoryType) => { + const handleCategoryDeletion = (category: string) => { Alert.alert( 'Category Deletion', `Are you sure that you want to delete the category ${category} ?`, @@ -239,7 +292,10 @@ const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => { text: 'Yes', onPress: () => { dispatch( - updateMomentCategories([category], false, loggedInUser.userId), + updateMomentCategories( + momentCategories.filter((mc) => mc !== category), + false, + ), ); dispatch(deleteUserMomentsForCategory(category)); }, @@ -304,7 +360,35 @@ const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => { } has not posted any moments yet`}</Text> </View> )} - {userMomentCategories.map( + {!userXId && + profile.profile_completion_stage === 2 && + !isStageTwoPromptClosed && ( + <TaggPrompt + messageHeader="Create a new category" + messageBody={ + 'Post your first moment to continue building your digital identity!' + } + logoType="" + onClose={() => { + setIsStageTwoPromptClosed(true); + }} + /> + )} + {!userXId && + profile.profile_completion_stage === 3 && + !isStageThreePromptClosed && ( + <TaggPrompt + messageHeader="Continue to build your profile" + messageBody={ + 'Continue to personalize your own digital space in\nthis community by filling your profile with\ncategories and moments!' + } + logoType="" + onClose={() => { + setIsStageThreePromptClosed(true); + }} + /> + )} + {momentCategories.map( (title, index) => (!userXId || imagesMap.get(title)) && ( <Moment @@ -314,15 +398,17 @@ const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => { userXId={userXId} screenType={screenType} handleMomentCategoryDelete={handleCategoryDeletion} - shouldAllowDeletion={userMomentCategories.length > 2} + shouldAllowDeletion={momentCategories.length > 1} + showUpButton={index !== 0} + showDownButton={index !== momentCategories.length - 1} + move={move} /> ), )} - {!userXId && userMomentCategories.length < 6 && ( + {!userXId && ( <TouchableOpacity onPress={() => navigation.push('CategorySelection', { - categories: momentCategories, screenType: CategorySelectionScreenType.Profile, user: loggedInUser, }) diff --git a/src/components/profile/ProfilePreview.tsx b/src/components/profile/ProfilePreview.tsx index bd015811..134e94cd 100644 --- a/src/components/profile/ProfilePreview.tsx +++ b/src/components/profile/ProfilePreview.tsx @@ -134,20 +134,23 @@ const ProfilePreview: React.FC<ProfilePreviewProps> = ({ } } + const userXId = + loggedInUser.username === user.username ? undefined : user.id; + /** - * Dispatch an event to Fetch the user details - * If the user is already present in store, do not fetch again - * Finally, Navigate to profile of the user selected + * Dispatch an event to Fetch the user details only if we're navigating to + * a userX's profile. + * If the user is already present in store, do not fetch again. + * Finally, Navigate to profile of the user selected. */ - - if (!userXInStore(state, screenType, user.id)) { + if (userXId && !userXInStore(state, screenType, user.id)) { await fetchUserX( dispatch, {userId: user.id, username: user.username}, screenType, ); } - const userXId = loggedInUser.username === user.username ? undefined : user.id; + navigation.push('Profile', { userXId, screenType, @@ -205,7 +208,6 @@ const ProfilePreview: React.FC<ProfilePreviewProps> = ({ usernameStyle = styles.searchResultUsername; nameStyle = styles.searchResultName; } - return ( <TouchableOpacity onPress={addToRecentlyStoredAndNavigateToProfile} @@ -257,9 +259,9 @@ const styles = StyleSheet.create({ discoverUsersContainer: { alignItems: 'center', textAlign: 'center', - margin: '0.5%', width: '32%', marginVertical: 10, + borderWidth: 1, }, searchResultAvatar: { height: 60, @@ -290,6 +292,7 @@ const styles = StyleSheet.create({ discoverUsersNameContainer: { justifyContent: 'space-evenly', alignSelf: 'stretch', + marginTop: 5, }, searchResultUsername: { fontSize: 18, diff --git a/src/components/search/Explore.tsx b/src/components/search/Explore.tsx index a02205a4..c07c66b8 100644 --- a/src/components/search/Explore.tsx +++ b/src/components/search/Explore.tsx @@ -1,27 +1,18 @@ import React from 'react'; -import {View, StyleSheet} from 'react-native'; +import {StyleSheet, Text, View} from 'react-native'; +import {useSelector} from 'react-redux'; +import {EXPLORE_SECTION_TITLES} from '../../constants'; +import {RootState} from '../../store/rootReducer'; +import {ExploreSectionType} from '../../types'; import ExploreSection from './ExploreSection'; const Explore: React.FC = () => { - const sections: Array<string> = [ - 'People you follow', - 'People you may know', - 'Trending in sports', - 'Trending on Tagg', - 'Trending in music', - ]; - const users: Array<string> = [ - 'Sam Davis', - 'Becca Smith', - 'Ann Taylor', - 'Clara Johnson', - 'Sarah Jung', - 'Lila Hernandez', - ]; + const {explores} = useSelector((state: RootState) => state.taggUsers); return ( <View style={styles.container}> - {sections.map((title) => ( - <ExploreSection key={title} title={title} users={users} /> + <Text style={styles.header}>Search Profiles</Text> + {EXPLORE_SECTION_TITLES.map((title: ExploreSectionType) => ( + <ExploreSection key={title} title={title} users={explores[title]} /> ))} </View> ); @@ -30,6 +21,14 @@ const Explore: React.FC = () => { const styles = StyleSheet.create({ container: { zIndex: 0, + // margin: '5%', + }, + header: { + fontWeight: '700', + fontSize: 22, + color: '#fff', + marginBottom: '2%', + margin: '5%', }, }); export default Explore; diff --git a/src/components/search/ExploreSection.tsx b/src/components/search/ExploreSection.tsx index 8e826bd9..8e8b4988 100644 --- a/src/components/search/ExploreSection.tsx +++ b/src/components/search/ExploreSection.tsx @@ -1,5 +1,6 @@ -import React from 'react'; -import {View, Text, ScrollView, StyleSheet} from 'react-native'; +import React, {Fragment} from 'react'; +import {ScrollView, StyleSheet, Text, View} from 'react-native'; +import {ProfilePreviewType} from '../../types'; import ExploreSectionUser from './ExploreSectionUser'; /** @@ -9,33 +10,40 @@ import ExploreSectionUser from './ExploreSectionUser'; interface ExploreSectionProps { title: string; - users: Array<string>; + users: ProfilePreviewType[]; } const ExploreSection: React.FC<ExploreSectionProps> = ({title, users}) => { - return ( + return users.length !== 0 ? ( <View style={styles.container}> <Text style={styles.header}>{title}</Text> <ScrollView horizontal showsHorizontalScrollIndicator={false}> - {users.map((name, key) => ( - <ExploreSectionUser {...{name, key}} style={styles.user} /> + <View style={styles.padding} /> + {users.map((user) => ( + <ExploreSectionUser key={user.id} user={user} style={styles.user} /> ))} </ScrollView> </View> + ) : ( + <Fragment /> ); }; const styles = StyleSheet.create({ container: { - marginBottom: 30, + marginVertical: '5%', }, header: { fontWeight: '600', fontSize: 20, color: '#fff', - marginBottom: 20, + marginLeft: '5%', + marginBottom: '5%', }, user: { - marginHorizontal: 15, + marginHorizontal: 5, + }, + padding: { + width: 10, }, }); diff --git a/src/components/search/ExploreSectionUser.tsx b/src/components/search/ExploreSectionUser.tsx index a9fce063..0bf68a20 100644 --- a/src/components/search/ExploreSectionUser.tsx +++ b/src/components/search/ExploreSectionUser.tsx @@ -1,12 +1,18 @@ -import React from 'react'; +import {useNavigation} from '@react-navigation/native'; +import React, {useEffect, useState} from 'react'; import { + Image, StyleSheet, Text, - ViewProps, - Image, TouchableOpacity, + ViewProps, } from 'react-native'; import LinearGradient from 'react-native-linear-gradient'; +import {useDispatch, useSelector, useStore} from 'react-redux'; +import {loadAvatar} from '../../services'; +import {RootState} from '../../store/rootReducer'; +import {ProfilePreviewType, ScreenType} from '../../types'; +import {fetchUserX, userXInStore} from '../../utils'; /** * Search Screen for user recommendations and a search @@ -14,14 +20,52 @@ import LinearGradient from 'react-native-linear-gradient'; */ interface ExploreSectionUserProps extends ViewProps { - name: string; + user: ProfilePreviewType; } const ExploreSectionUser: React.FC<ExploreSectionUserProps> = ({ - name, + user, style, }) => { + const {id, username, first_name, last_name} = user; + const [avatar, setAvatar] = useState<string | null>(null); + const navigation = useNavigation(); + const {user: loggedInUser} = useSelector((state: RootState) => state.user); + const state: RootState = useStore().getState(); + const screenType = ScreenType.Search; + + const dispatch = useDispatch(); + + useEffect(() => { + let mounted = true; + const loadAvatarImage = async () => { + const response = await loadAvatar(id, true); + if (mounted) { + setAvatar(response); + } + }; + loadAvatarImage(); + return () => { + mounted = false; + }; + }, [user]); + + const handlePress = async () => { + if (!userXInStore(state, screenType, user.id)) { + await fetchUserX( + dispatch, + {userId: user.id, username: user.username}, + screenType, + ); + } + const userXId = loggedInUser.username === user.username ? undefined : id; + navigation.push('Profile', { + userXId, + screenType, + }); + }; + return ( - <TouchableOpacity style={[styles.container, style]}> + <TouchableOpacity style={[styles.container, style]} onPress={handlePress}> <LinearGradient colors={['#9F00FF', '#27EAE9']} useAngle @@ -29,12 +73,18 @@ const ExploreSectionUser: React.FC<ExploreSectionUserProps> = ({ angleCenter={{x: 0.5, y: 0.5}} style={styles.gradient}> <Image - source={require('../../assets/images/avatar-placeholder.png')} + source={ + avatar + ? {uri: avatar} + : require('../../assets/images/avatar-placeholder.png') + } style={styles.profile} /> </LinearGradient> - <Text style={styles.name}>{name}</Text> - <Text style={styles.username}>{`@${name.split(' ').join('')}`}</Text> + <Text style={styles.name} numberOfLines={2}> + {first_name} {last_name} + </Text> + <Text style={styles.username} numberOfLines={1}>{`@${username}`}</Text> </TouchableOpacity> ); }; @@ -42,27 +92,30 @@ const ExploreSectionUser: React.FC<ExploreSectionUserProps> = ({ const styles = StyleSheet.create({ container: { alignItems: 'center', + width: 100, }, gradient: { - height: 80, - width: 80, + height: 60, + aspectRatio: 1, borderRadius: 40, justifyContent: 'center', alignItems: 'center', marginBottom: 10, }, profile: { - height: 76, - width: 76, + height: 55, + aspectRatio: 1, borderRadius: 38, }, name: { fontWeight: '600', + flexWrap: 'wrap', fontSize: 16, color: '#fff', + textAlign: 'center', }, username: { - fontWeight: '600', + fontWeight: '400', fontSize: 14, color: '#fff', }, diff --git a/src/constants/api.ts b/src/constants/api.ts index 3b2289fd..de43b94d 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -27,6 +27,8 @@ export const BLOCK_USER_ENDPOINT: string = API_URL + 'block/'; export const PASSWORD_RESET_ENDPOINT: string = API_URL + 'password-reset/'; export const MOMENT_CATEGORY_ENDPOINT: string = API_URL + 'moment-category/'; export const NOTIFICATIONS_ENDPOINT: string = API_URL + 'notifications/'; +export const DISCOVER_ENDPOINT: string = API_URL + 'discover/'; +export const WAITLIST_USER_ENDPOINT: string = API_URL + 'waitlist-user/'; // Register as FCM device export const FCM_ENDPOINT: string = API_URL + 'fcm/'; diff --git a/src/constants/constants.ts b/src/constants/constants.ts index 531420e6..b96d9438 100644 --- a/src/constants/constants.ts +++ b/src/constants/constants.ts @@ -1,5 +1,5 @@ import {ReactText} from 'react'; -import {BackgroundGradientType, MomentCategoryType} from './../types/'; +import {BackgroundGradientType, ExploreSectionType} from './../types/'; import {SCREEN_WIDTH, SCREEN_HEIGHT} from '../utils'; export const CHIN_HEIGHT = 34; @@ -60,6 +60,7 @@ export const YOUTUBE_FONT_COLOR: string = '#FCA4A4'; export const TAGG_DARK_BLUE = '#4E699C'; export const TAGG_TEXT_LIGHT_BLUE: string = '#698DD3'; +export const TAGG_LIGHT_PURPLE = '#F4DDFF'; export const TAGGS_GRADIENT = { start: '#9F00FF', @@ -103,7 +104,7 @@ export const BROWSABLE_SOCIAL_URLS: Record<string, string> = { Twitter: 'https://twitter.com/', }; -export const MOMENT_CATEGORIES: Array<MomentCategoryType> = [ +export const MOMENT_CATEGORIES: string[] = [ 'Friends', 'Adventure', 'Photo Dump', @@ -140,3 +141,31 @@ export const CLASS_YEAR_LIST: Array<string> = [ '2025', '2026', ]; + +export const TAGG_WEBSITE = 'https://www.tagg.id/'; +export const MOMENT_CATEGORY_BG_COLORS: string[] = [ + '#5E4AE4', + '#5044A6', + '#4755A1', + '#444BA8', + '#374898', + '#3F5C97', + '#3A649F', + '#386A95', + '#366D84', + '#335E76', + '#2E5471', + '#274765', + '#225363', + '#365F6A', + '#4E7175', +]; + +export const EXPLORE_SECTION_TITLES: ExploreSectionType[] = [ + 'New to Tagg', + 'People You May Know', + 'Trending on Tagg', + "Brown '21", + "Brown '22", + "Brown '23", +]; diff --git a/src/routes/main/MainStackNavigator.tsx b/src/routes/main/MainStackNavigator.tsx index 4614168b..bd838ef2 100644 --- a/src/routes/main/MainStackNavigator.tsx +++ b/src/routes/main/MainStackNavigator.tsx @@ -2,7 +2,13 @@ * Note the name userXId here, it refers to the id of the user being visited */ import {createStackNavigator} from '@react-navigation/stack'; -import {CategorySelectionScreenType, MomentType, ScreenType} from '../../types'; +import {Image} from 'react-native-image-crop-picker'; +import { + CategorySelectionScreenType, + MomentType, + ScreenType, + UserType, +} from '../../types'; export type MainStackParams = { Search: { @@ -19,7 +25,7 @@ export type MainStackParams = { }; CaptionScreen: { title: string; - image: object; + image: Image; screenType: ScreenType; }; IndividualMoment: { @@ -41,12 +47,18 @@ export type MainStackParams = { username: string; }; CategorySelection: { - categories: Array<string>; screenType: CategorySelectionScreenType; + user: UserType; + newCustomCategory: string | undefined; }; + CreateCustomCategory: {}; Notifications: { screenType: ScreenType; }; + MomentUploadPrompt: { + screenType: ScreenType; + momentCategory: string; + }; }; export const MainStack = createStackNavigator<MainStackParams>(); diff --git a/src/routes/main/MainStackScreen.tsx b/src/routes/main/MainStackScreen.tsx index bf643fd8..b4eaa213 100644 --- a/src/routes/main/MainStackScreen.tsx +++ b/src/routes/main/MainStackScreen.tsx @@ -1,25 +1,25 @@ +import {RouteProp} from '@react-navigation/native'; +import {StackNavigationOptions} from '@react-navigation/stack'; import React from 'react'; import { - IndividualMoment, CaptionScreen, - SocialMediaTaggs, - SearchScreen, - ProfileScreen, - MomentCommentsScreen, - EditProfile, CategorySelection, + CreateCustomCategory, + EditProfile, FriendsListScreen, + IndividualMoment, + MomentCommentsScreen, + MomentUploadPromptScreen, NotificationsScreen, + ProfileScreen, + SearchScreen, + SocialMediaTaggs, } from '../../screens'; -import {MainStack, MainStackParams} from './MainStackNavigator'; -import {RouteProp} from '@react-navigation/native'; import {ScreenType} from '../../types'; -import {AvatarHeaderHeight} from '../../utils'; -import {StackNavigationOptions} from '@react-navigation/stack'; -import {Screen} from 'react-native-screens'; +import {AvatarHeaderHeight, SCREEN_WIDTH} from '../../utils'; +import {MainStack, MainStackParams} from './MainStackNavigator'; /** - * Trying to explain the purpose of each route on the stack (ACTUALLY A STACK) * Profile : To display the logged in user's profile when the userXId passed in to it is (undefined | null | empty string) else displays profile of the user being visited. * Search : To display the search screen. Search for a user on this screen, click on a result tile and navigate to the same. * When you click on the search icon after looking at a user's profile, the stack gets reset and you come back to the top of the stack (First screen : Search in this case) @@ -79,6 +79,7 @@ const MainStackScreen: React.FC<MainStackProps> = ({route}) => { <MainStack.Navigator screenOptions={{ headerShown: false, + gestureResponseDistance: {horizontal: SCREEN_WIDTH * 0.6}, }} mode="card" initialRouteName={initialRouteName}> @@ -141,6 +142,17 @@ const MainStackScreen: React.FC<MainStackProps> = ({route}) => { }} /> <MainStack.Screen + name="CreateCustomCategory" + component={CreateCustomCategory} + options={{ + headerShown: true, + headerTransparent: true, + headerBackTitleVisible: false, + headerTintColor: 'white', + headerTitle: '', + }} + /> + <MainStack.Screen name="IndividualMoment" component={IndividualMoment} options={{ @@ -157,6 +169,14 @@ const MainStackScreen: React.FC<MainStackProps> = ({route}) => { initialParams={{screenType}} /> <MainStack.Screen + name="MomentUploadPrompt" + component={MomentUploadPromptScreen} + options={{ + ...modalStyle, + }} + initialParams={{screenType}} + /> + <MainStack.Screen name="FriendsListScreen" component={FriendsListScreen} options={{ diff --git a/src/routes/onboarding/OnboardingStack.tsx b/src/routes/onboarding/OnboardingStackNavigator.tsx index 7ff00271..9f614f7c 100644 --- a/src/routes/onboarding/OnboardingStack.tsx +++ b/src/routes/onboarding/OnboardingStackNavigator.tsx @@ -1,7 +1,6 @@ import {createStackNavigator} from '@react-navigation/stack'; import { CategorySelectionScreenType, - MomentCategoryType, TaggPopupType, UserType, VerificationScreenType, @@ -28,13 +27,20 @@ export type OnboardingStackParams = { ProfileOnboarding: {username: string; userId: string}; SocialMedia: {username: string; userId: string}; CategorySelection: { - categories: Record<MomentCategoryType, boolean>; screenType: CategorySelectionScreenType; user: UserType; + newCustomCategory: string | undefined; + }; + CreateCustomCategory: { + screenType: CategorySelectionScreenType; + user: UserType; + existingCategories: string[]; }; TaggPopup: { popupProps: TaggPopupType; }; + AddWaitlistUser: undefined; + WaitlistSuccess: undefined; }; export const OnboardingStack = createStackNavigator<OnboardingStackParams>(); diff --git a/src/routes/onboarding/Onboarding.tsx b/src/routes/onboarding/OnboardingStackScreen.tsx index a3d281f5..afc5be99 100644 --- a/src/routes/onboarding/Onboarding.tsx +++ b/src/routes/onboarding/OnboardingStackScreen.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import {OnboardingStack} from './OnboardingStack'; +import {OnboardingStack} from './OnboardingStackNavigator'; import { Login, InvitationCodeVerification, @@ -14,6 +14,8 @@ import { PasswordReset, WelcomeScreen, CategorySelection, + AddWaitlistUserScreen, + WaitlistSuccessScreen, } from '../../screens'; import {StackCardInterpolationProps} from '@react-navigation/stack'; import TaggPopup from '../../components/common/TaggPopup'; @@ -97,6 +99,14 @@ const Onboarding: React.FC = () => { component={InvitationCodeVerification} /> <OnboardingStack.Screen + name="AddWaitlistUser" + component={AddWaitlistUserScreen} + /> + <OnboardingStack.Screen + name="WaitlistSuccess" + component={WaitlistSuccessScreen} + /> + <OnboardingStack.Screen name="RegistrationOne" component={RegistrationOne} /> diff --git a/src/routes/onboarding/index.ts b/src/routes/onboarding/index.ts index 66b0f3f4..ce9ac046 100644 --- a/src/routes/onboarding/index.ts +++ b/src/routes/onboarding/index.ts @@ -1,2 +1,2 @@ -export * from './OnboardingStack'; -export {default} from './Onboarding'; +export * from './OnboardingStackNavigator'; +export {default} from './OnboardingStackScreen'; diff --git a/src/routes/tabs/NavigationBar.tsx b/src/routes/tabs/NavigationBar.tsx index 9d7d4b12..3757c56b 100644 --- a/src/routes/tabs/NavigationBar.tsx +++ b/src/routes/tabs/NavigationBar.tsx @@ -1,12 +1,16 @@ import {createBottomTabNavigator} from '@react-navigation/bottom-tabs'; import React, {Fragment} from 'react'; +import {useSelector} from 'react-redux'; import {NavigationIcon} from '../../components'; +import {RootState} from '../../store/rootReducer'; import {ScreenType} from '../../types'; import MainStackScreen from '../main/MainStackScreen'; const Tabs = createBottomTabNavigator(); const NavigationBar: React.FC = () => { + const {isOnboardedUser} = useSelector((state: RootState) => state.user); + return ( <Tabs.Navigator screenOptions={({route}) => ({ @@ -27,7 +31,7 @@ const NavigationBar: React.FC = () => { } }, })} - initialRouteName="Search" + initialRouteName={isOnboardedUser ? 'Profile' : 'Search'} tabBarOptions={{ showLabel: false, style: { diff --git a/src/screens/main/NotificationsScreen.tsx b/src/screens/main/NotificationsScreen.tsx index d6d47b02..8aa47299 100644 --- a/src/screens/main/NotificationsScreen.tsx +++ b/src/screens/main/NotificationsScreen.tsx @@ -17,6 +17,7 @@ import {NotificationType, ScreenType} from '../../types'; import {getDateAge, SCREEN_HEIGHT} from '../../utils'; const NotificationsScreen: React.FC = () => { + const {user: loggedInUser} = useSelector((state: RootState) => state.user); const [refreshing, setRefreshing] = useState(false); // used for figuring out which ones are unread const [lastViewed, setLastViewed] = useState<moment.Moment | undefined>( @@ -95,7 +96,13 @@ const NotificationsScreen: React.FC = () => { }, [lastViewed, notifications]); const renderNotification = ({item}: {item: NotificationType}) => ( - <Notification item={item} screenType={ScreenType.Notifications} /> + <Notification + item={item} + userXId={ + item.actor.id === loggedInUser.userId ? undefined : item.actor.id + } + screenType={ScreenType.Notifications} + /> ); const renderSectionHeader = ({section: {title, data}}) => diff --git a/src/screens/onboarding/AddWaitlistUserScreen.tsx b/src/screens/onboarding/AddWaitlistUserScreen.tsx new file mode 100644 index 00000000..1c13ffb5 --- /dev/null +++ b/src/screens/onboarding/AddWaitlistUserScreen.tsx @@ -0,0 +1,238 @@ +import {StackNavigationProp} from '@react-navigation/stack'; +import * as React from 'react'; +import { + KeyboardAvoidingView, + Platform, + StatusBar, + StyleSheet, + Text, + TouchableOpacity, + View, +} from 'react-native'; +import { + ArrowButton, + Background, + LoadingIndicator, + SubmitButton, + TaggInput, +} from '../../components'; +import {nameRegex, phoneRegex} from '../../constants'; +import {OnboardingStackParams} from '../../routes'; +import {adduserToWaitlist} from '../../services'; +import {BackgroundGradientType} from '../../types'; +import {SCREEN_HEIGHT} from '../../utils'; + +type AddWaitlistUserScreenProp = StackNavigationProp< + OnboardingStackParams, + 'AddWaitlistUser' +>; + +interface AddWaitlistUserScreenProps { + navigation: AddWaitlistUserScreenProp; +} + +const AddWaitlistUserScreen: React.FC<AddWaitlistUserScreenProps> = ({ + navigation, +}) => { + const phoneRef = React.useRef(); + const lnameRef = React.useRef(); + + const [form, setForm] = React.useState({ + phone_number: {value: '', isValid: false}, + first_name: {value: '', isValid: false}, + last_name: {value: '', isValid: false}, + attemptedSubmit: false, + }); + + //Handlers + const handleFocusChange = (field: string): void => { + switch (field) { + case 'last_name': + const lnameField: any = lnameRef.current; + lnameField.focus(); + break; + case 'phone_number': + const phoneField: any = phoneRef.current; + phoneField.focus(); + break; + default: + return; + } + }; + + const validate = (value: string, type: string) => { + let isValid: boolean = false; + switch (type) { + case 'phone_number': + isValid = phoneRegex.test(value); + break; + default: + isValid = nameRegex.test(value); + break; + } + return isValid; + }; + + const handleUpdate = (value: string, type: string) => { + value = value.trim(); + const isValid = validate(value, type); + setForm({ + ...form, + [type]: {value, isValid}, + }); + }; + + const handleAddUser = async () => { + if (!form.attemptedSubmit) { + setForm({ + ...form, + attemptedSubmit: true, + }); + } + try { + const {phone_number, first_name, last_name} = form; + if (phone_number.isValid && first_name.isValid && last_name.isValid) { + const success = await adduserToWaitlist( + phone_number.value, + first_name.value, + last_name.value, + ); + if (success) { + navigation.navigate('WaitlistSuccess'); + } + } else { + setForm({...form, attemptedSubmit: false}); + setTimeout(() => setForm({...form, attemptedSubmit: true})); + } + } catch (err) { + console.log(err); + } + }; + + //Components + const Footer = () => ( + <View style={styles.footer}> + <ArrowButton + direction="backward" + onPress={() => navigation.navigate('InvitationCodeVerification')} + /> + </View> + ); + + const {phone_number, first_name, last_name} = form; + + return ( + <Background + style={styles.container} + gradientType={BackgroundGradientType.Light}> + <StatusBar barStyle="light-content" /> + <KeyboardAvoidingView + behavior={Platform.OS === 'ios' ? 'padding' : 'height'} + style={styles.container}> + <View> + <Text style={styles.formHeader}>JOIN WAITLIST</Text> + </View> + <TaggInput + accessibilityHint="Enter your first name." + accessibilityLabel="First name input field." + placeholder="First Name" + autoCompleteType="name" + textContentType="name" + returnKeyType="next" + onChangeText={(text) => handleUpdate(text, 'first_name')} + onSubmitEditing={() => handleFocusChange('first_name')} + blurOnSubmit={false} + valid={first_name.isValid} + invalidWarning="Please enter a valid first name." + attemptedSubmit={form.attemptedSubmit} + width={280} + /> + <TaggInput + accessibilityHint="Enter your last name." + accessibilityLabel="Last name input field." + placeholder="Last Name" + autoCompleteType="name" + textContentType="name" + returnKeyType="next" + onChangeText={(text) => handleUpdate(text, 'last_name')} + blurOnSubmit={false} + ref={lnameRef} + valid={last_name.isValid} + invalidWarning="Please enter a valid last name." + onSubmitEditing={() => handleFocusChange('phone_number')} + attemptedSubmit={form.attemptedSubmit} + width={280} + /> + <TaggInput + maxLength={12} + accessibilityHint="Enter your phone number." + accessibilityLabel="Phone number input field." + placeholder="Phone Number" + autoCompleteType="tel" + textContentType="telephoneNumber" + autoCapitalize="none" + returnKeyType="next" + keyboardType="phone-pad" + onChangeText={(text) => handleUpdate(text, 'phone_number')} + blurOnSubmit={false} + ref={phoneRef} + valid={phone_number.isValid} + invalidWarning="Please enter a valid 10 digit number." + onSubmitEditing={handleAddUser} + attemptedSubmit={form.attemptedSubmit} + width={280} + /> + <TouchableOpacity onPress={handleAddUser} style={styles.finalAction}> + <Text style={styles.finalActionLabel}>Submit</Text> + </TouchableOpacity> + <LoadingIndicator /> + </KeyboardAvoidingView> + <Footer /> + </Background> + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + }, + finalAction: { + backgroundColor: 'white', + justifyContent: 'center', + alignItems: 'center', + width: 150, + height: 40, + borderRadius: 5, + borderWidth: 1, + borderColor: '#fff', + marginVertical: SCREEN_HEIGHT / 20, + }, + finalActionLabel: { + fontSize: 16, + fontWeight: '500', + color: 'black', + }, + formHeader: { + color: '#fff', + fontSize: 30, + fontWeight: '600', + marginBottom: '16%', + }, + footer: { + width: '100%', + flexDirection: 'row', + justifyContent: 'space-around', + ...Platform.select({ + ios: { + bottom: '20%', + }, + android: { + bottom: '10%', + }, + }), + }, +}); + +export default AddWaitlistUserScreen; diff --git a/src/screens/onboarding/CategorySelection.tsx b/src/screens/onboarding/CategorySelection.tsx index b9677ed4..5589ea9e 100644 --- a/src/screens/onboarding/CategorySelection.tsx +++ b/src/screens/onboarding/CategorySelection.tsx @@ -1,8 +1,8 @@ import {RouteProp} from '@react-navigation/native'; -import React, {useCallback, useEffect, useState} from 'react'; +import {StackNavigationProp} from '@react-navigation/stack'; +import React, {useEffect, useState} from 'react'; import { Alert, - KeyboardAvoidingView, Platform, StatusBar, StyleSheet, @@ -10,20 +10,20 @@ import { TouchableOpacity, View, } from 'react-native'; -import {useDispatch} from 'react-redux'; -import { - BackgroundGradientType, - CategorySelectionScreenType, - MomentCategoryType, -} from '../../types'; +import {ScrollView} from 'react-native-gesture-handler'; +import {useDispatch, useSelector} from 'react-redux'; +import PlusIcon from '../../assets/icons/plus_icon-01.svg'; import {Background, MomentCategory} from '../../components'; import {MOMENT_CATEGORIES} from '../../constants'; import {OnboardingStackParams} from '../../routes'; -import {StackNavigationProp} from '@react-navigation/stack'; -import {getTokenOrLogout, userLogin} from '../../utils'; import {fcmService, postMomentCategories} from '../../services'; -import {updateMomentCategories} from '../../store/actions/momentCategories'; -import {ScrollView} from 'react-native-gesture-handler'; +import { + updateMomentCategories, + updateIsOnboardedUser, +} from '../../store/actions/'; +import {RootState} from '../../store/rootReducer'; +import {BackgroundGradientType, CategorySelectionScreenType} from '../../types'; +import {getTokenOrLogout, SCREEN_WIDTH, userLogin} from '../../utils'; type CategorySelectionRouteProps = RouteProp< OnboardingStackParams, @@ -47,17 +47,47 @@ const CategorySelection: React.FC<CategorySelectionProps> = ({ /** * Same component to be used for category selection while onboarding and while on profile */ - const {categories, screenType, user} = route.params; + const {screenType, user} = route.params; const isOnBoarding: boolean = screenType === CategorySelectionScreenType.Onboarding; const {userId, username} = user; - const [selectedCategories, setSelectedCategories] = useState< - Array<MomentCategoryType> + // During onboarding this will fail and default to [] + const {momentCategories = []} = useSelector( + (state: RootState) => state.momentCategories, + ); + + // Stores all the categories that will be saved to the store + const [selectedCategories, setSelectedCategories] = useState<string[]>([]); + + /** + * Stores all the custom categories for the UI, allow easier logic for + * unchecking a custom category. + * + * Each uncommited custom category should also have a copy in selectedCategories + * since that's the final value that will be stored in the store. + */ + const [uncommitedCustomCategories, setUncommitedCustomCategories] = useState< + string[] >([]); + const customCategories = momentCategories.filter( + (mc) => !MOMENT_CATEGORIES.includes(mc), + ); + const dispatch = useDispatch(); + useEffect(() => { + const newCustomCategory = route.params.newCustomCategory; + if (newCustomCategory) { + setUncommitedCustomCategories([ + ...uncommitedCustomCategories, + newCustomCategory, + ]); + selectedCategories.push(newCustomCategory); + } + }, [route.params?.newCustomCategory]); + /** * Show the tutorial if a new user is OnBoarding */ @@ -67,13 +97,8 @@ const CategorySelection: React.FC<CategorySelectionProps> = ({ popupProps: { messageHeader: 'Category And Moments', messageBody: - 'Use pictures and videos to share different aspects of you', - next: { - messageHeader: 'Select Categories', - messageBody: - 'Select between 2 - 6 categories to begin creating moments!', - next: undefined, - }, + 'Use pictures and videos to share \ndifferent aspects of you', + next: undefined, }, }); } @@ -89,11 +114,13 @@ const CategorySelection: React.FC<CategorySelectionProps> = ({ * Remove from the selected categories */ const onSelect = ( - category: MomentCategoryType, + category: string, isSelected: boolean, isAdded: boolean, ) => { - if (isAdded) return; + if (isAdded) { + return; + } if (isSelected) { setSelectedCategories((prev) => [...prev, category]); } else { @@ -104,40 +131,51 @@ const CategorySelection: React.FC<CategorySelectionProps> = ({ }; /** - * if onboarding - * Count of already added categories will always be 0 - * else - * Calculate number of selected categories by iterating through the user's pre-selected categories + * Handle deselection of custom category. + * + * Custom categories is "added" and "selected" by CreateCustomCategory screen. + * User can only "deselect" an uncommited custom category. + * + * case isAdded || isSelected: + * Return without doing anything + * default: + * Remove from selected categories AND uncommitedCustomCategories */ - const addedLength = !isOnBoarding - ? Object.keys(categories).filter((key) => { - return categories[key as MomentCategoryType] === true; - }).length - : 0; + const onDeselectCustomCategory = ( + category: string, + isSelected: boolean, + isAdded: boolean, + ) => { + if (isAdded || isSelected) { + return; + } + setSelectedCategories( + selectedCategories.filter((item) => item !== category), + ); + setUncommitedCustomCategories( + uncommitedCustomCategories.filter((item) => item !== category), + ); + }; const handleButtonPress = async () => { - /** - * Check for lower and upper bound before creating new categories - */ - const totalCategories = addedLength + selectedCategories.length; - if (totalCategories < 2) { - Alert.alert('Please select atleast 2 categories'); - return; - } else if (totalCategories > 6) { - Alert.alert('You may not add more than 6 categories'); - return; - } else if (selectedCategories.length === 0) { - Alert.alert('Please select some categories!'); + if (momentCategories.length + selectedCategories.length === 0) { + Alert.alert('Please select at least 1 category'); return; } try { if (isOnBoarding) { + dispatch(updateIsOnboardedUser(true)); const token = await getTokenOrLogout(dispatch); await postMomentCategories(selectedCategories, token); userLogin(dispatch, {userId: userId, username: username}); fcmService.sendFcmTokenToServer(); } else { - dispatch(updateMomentCategories(selectedCategories, true, userId)); + dispatch( + updateMomentCategories( + momentCategories.concat(selectedCategories), + true, + ), + ); navigation.goBack(); } } catch (error) { @@ -155,15 +193,55 @@ const CategorySelection: React.FC<CategorySelectionProps> = ({ style={styles.container} gradientType={BackgroundGradientType.Dark}> <StatusBar barStyle="light-content" /> - <Text style={styles.subtext}>Create new categories</Text> + <Text style={styles.subtext}>Create Categories</Text> <View style={styles.container}> + {!isOnBoarding && ( + <TouchableOpacity + style={styles.createCategory} + onPress={() => { + navigation.push('CreateCustomCategory', { + screenType, + user, + existingCategories: momentCategories.concat( + selectedCategories, + ), + }); + }}> + <PlusIcon width={30} height={30} color="white" /> + <Text style={styles.createCategoryLabel}> + Create your own category + </Text> + </TouchableOpacity> + )} <View style={styles.linkerContainer}> + {/* commited custom categories */} + {customCategories.map((category, index) => ( + <MomentCategory + key={index} + categoryType={category} + isSelected={false} + isAdded={true} + onSelect={onDeselectCustomCategory} + /> + ))} + {/* uncommited custom categroies */} + {uncommitedCustomCategories.map((category, index) => ( + <MomentCategory + key={index} + categoryType={category} + isSelected={selectedCategories.includes(category)} + isAdded={false} + onSelect={onDeselectCustomCategory} + /> + ))} + {customCategories.length + uncommitedCustomCategories.length !== + 0 && <View style={styles.divider} />} {MOMENT_CATEGORIES.map((category, index) => ( <MomentCategory key={index} categoryType={category} isSelected={selectedCategories.includes(category)} - isAdded={categories[category]} + isAdded={momentCategories.includes(category)} onSelect={onSelect} /> ))} @@ -215,11 +293,12 @@ const styles = StyleSheet.create({ }, subtext: { color: '#fff', - fontSize: 16, + fontSize: 20, fontWeight: '600', textAlign: 'center', marginVertical: '8%', marginHorizontal: '10%', + marginTop: '15%', }, finalAction: { backgroundColor: 'white', @@ -237,6 +316,31 @@ const styles = StyleSheet.create({ fontWeight: '500', color: 'black', }, + createCategory: { + backgroundColor: '#53329B', + width: SCREEN_WIDTH * 0.9, + height: 70, + alignItems: 'center', + justifyContent: 'center', + borderRadius: 10, + flexDirection: 'row', + marginBottom: '5%', + }, + createCategoryLabel: { + color: 'white', + marginLeft: '3%', + fontSize: 18, + fontWeight: '500', + }, + plusIcon: { + color: 'white', + }, + divider: { + borderColor: 'white', + borderBottomWidth: 1, + width: SCREEN_WIDTH * 0.9, + marginVertical: '2%', + }, }); export default CategorySelection; diff --git a/src/screens/onboarding/CreateCustomCategory.tsx b/src/screens/onboarding/CreateCustomCategory.tsx new file mode 100644 index 00000000..eab72c7d --- /dev/null +++ b/src/screens/onboarding/CreateCustomCategory.tsx @@ -0,0 +1,123 @@ +import {RouteProp} from '@react-navigation/native'; +import {StackNavigationProp} from '@react-navigation/stack'; +import React, {useState} from 'react'; +import { + Alert, + KeyboardAvoidingView, + StatusBar, + StyleSheet, + Text, + TextInput, + TouchableOpacity, +} from 'react-native'; +import {Background} from '../../components'; +import {OnboardingStackParams} from '../../routes'; +import {BackgroundGradientType} from '../../types'; +import {SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils'; + +type CreateCustomCategoryRouteProps = RouteProp< + OnboardingStackParams, + 'CreateCustomCategory' +>; + +type CreateCustomCategoryNavigationProps = StackNavigationProp< + OnboardingStackParams, + 'CreateCustomCategory' +>; + +interface CreateCustomCategoryProps { + route: CreateCustomCategoryRouteProps; + navigation: CreateCustomCategoryNavigationProps; +} + +const CreateCustomCategory: React.FC<CreateCustomCategoryProps> = ({ + route, + navigation, +}) => { + /** + * Same component to be used for category selection while onboarding and while on profile + */ + const {existingCategories} = route.params; + const [newCategory, setNewCategory] = useState(''); + + const handleButtonPress = () => { + if (existingCategories.includes(newCategory)) { + Alert.alert('Looks like you already have that one created!'); + } else { + navigation.navigate('CategorySelection', { + screenType: route.params.screenType, + user: route.params.user, + newCustomCategory: newCategory, + }); + } + }; + + return ( + <> + <StatusBar barStyle="light-content" /> + <Background + style={styles.container} + gradientType={BackgroundGradientType.Dark}> + <KeyboardAvoidingView + style={styles.innerContainer} + behavior={'padding'}> + <Text style={styles.title}>Give your category a name</Text> + <TextInput + style={styles.input} + selectionColor={'white'} + onChangeText={setNewCategory} + autoFocus={true} + /> + <TouchableOpacity + onPress={handleButtonPress} + style={styles.finalAction}> + <Text style={styles.finalActionLabel}>{'Create'}</Text> + </TouchableOpacity> + </KeyboardAvoidingView> + </Background> + </> + ); +}; + +const styles = StyleSheet.create({ + container: { + alignItems: 'center', + minHeight: SCREEN_HEIGHT, + }, + innerContainer: { + height: '40%', + top: '20%', + justifyContent: 'space-around', + alignItems: 'center', + }, + title: { + color: 'white', + fontSize: 20, + fontWeight: '600', + }, + input: { + width: SCREEN_WIDTH * 0.75, + fontSize: 30, + color: 'white', + textAlign: 'center', + borderBottomWidth: 1, + borderBottomColor: 'white', + }, + finalAction: { + backgroundColor: 'white', + justifyContent: 'center', + alignItems: 'center', + width: 150, + height: 40, + borderRadius: 5, + borderWidth: 1, + borderColor: '#8F01FF', + }, + finalActionLabel: { + fontSize: 16, + fontWeight: '500', + color: 'black', + }, +}); + +export default CreateCustomCategory; diff --git a/src/screens/onboarding/InvitationCodeVerification.tsx b/src/screens/onboarding/InvitationCodeVerification.tsx index a9d1c12e..cc7cd678 100644 --- a/src/screens/onboarding/InvitationCodeVerification.tsx +++ b/src/screens/onboarding/InvitationCodeVerification.tsx @@ -10,7 +10,10 @@ import { LoadingIndicator, } from '../../components'; -import {VERIFY_INVITATION_CODE_ENDPOUNT} from '../../constants'; +import { + TAGG_LIGHT_PURPLE, + VERIFY_INVITATION_CODE_ENDPOUNT, +} from '../../constants'; import {Text} from 'react-native-animatable'; import { @@ -83,6 +86,10 @@ const InvitationCodeVerification: React.FC<InvitationCodeVerificationProps> = ({ } }; + const navigateToAddWaitList = () => { + navigation.navigate('AddWaitlistUser'); + }; + const Footer = () => ( <View style={styles.footer}> <ArrowButton @@ -131,6 +138,13 @@ const InvitationCodeVerification: React.FC<InvitationCodeVerificationProps> = ({ accessibilityHint="Select this after entering your invitation code" onPress={handleInvitationCodeVerification} /> + <View style={styles.noInviteCode}> + <Text style={styles.inviteCodeText}>Don't have an invite? </Text> + <Text style={styles.inviteCodeLink} onPress={navigateToAddWaitList}> + {' '} + Join the Waitlist + </Text> + </View> <LoadingIndicator /> </KeyboardAvoidingView> <Footer /> @@ -206,6 +220,19 @@ const styles = StyleSheet.create({ }, }), }, + noInviteCode: { + flexDirection: 'row', + justifyContent: 'center', + }, + inviteCodeText: { + color: TAGG_LIGHT_PURPLE, + fontSize: 18, + }, + inviteCodeLink: { + color: 'white', + fontSize: 18, + textDecorationLine: 'underline', + }, }); export default InvitationCodeVerification; diff --git a/src/screens/onboarding/Login.tsx b/src/screens/onboarding/Login.tsx index 3e59b00e..d1717fc1 100644 --- a/src/screens/onboarding/Login.tsx +++ b/src/screens/onboarding/Login.tsx @@ -15,17 +15,16 @@ import { import {fcmService} from '../../services'; import {OnboardingStackParams} from '../../routes/onboarding'; import {Background, TaggInput, SubmitButton} from '../../components'; -import {usernameRegex, LOGIN_ENDPOINT} from '../../constants'; -import AsyncStorage from '@react-native-community/async-storage'; import { - BackgroundGradientType, - CategorySelectionScreenType, - UserType, -} from '../../types'; + usernameRegex, + LOGIN_ENDPOINT, + TAGG_LIGHT_PURPLE, +} from '../../constants'; +import AsyncStorage from '@react-native-community/async-storage'; +import {BackgroundGradientType, UserType} from '../../types'; import {useDispatch} from 'react-redux'; import {userLogin} from '../../utils'; import SplashScreen from 'react-native-splash-screen'; -import {MOMENT_CATEGORIES_MAP} from '../../store/initialStates'; type VerificationScreenRouteProp = RouteProp<OnboardingStackParams, 'Login'>; type VerificationScreenNavigationProp = StackNavigationProp< @@ -356,7 +355,7 @@ const styles = StyleSheet.create({ }, newUser: { fontSize: 14, - color: '#f4ddff', + color: TAGG_LIGHT_PURPLE, }, getStarted: { fontSize: 14, diff --git a/src/screens/onboarding/ProfileOnboarding.tsx b/src/screens/onboarding/ProfileOnboarding.tsx index 70550f36..1f8e58da 100644 --- a/src/screens/onboarding/ProfileOnboarding.tsx +++ b/src/screens/onboarding/ProfileOnboarding.tsx @@ -147,43 +147,51 @@ const ProfileOnboarding: React.FC<ProfileOnboardingProps> = ({ const goToGalleryLargePic = () => { ImagePicker.openPicker({ - smartAlbums: ['Favorites', 'RecentlyAdded', 'SelfPortraits', 'Screenshots', 'UserLibrary'], + smartAlbums: [ + 'Favorites', + 'RecentlyAdded', + 'SelfPortraits', + 'Screenshots', + 'UserLibrary', + ], width: 580, height: 580, cropping: true, cropperToolbarTitle: 'Select Header', mediaType: 'photo', - }) - .then((picture) => { - if ('path' in picture) { - setForm({ - ...form, - largePic: picture.path, - }); - } - }) - .catch(() => {}); + }).then((picture) => { + if ('path' in picture) { + setForm({ + ...form, + largePic: picture.path, + }); + } + }); }; const goToGallerySmallPic = () => { ImagePicker.openPicker({ - smartAlbums: ['Favorites', 'RecentlyAdded', 'SelfPortraits', 'Screenshots', 'UserLibrary'], + smartAlbums: [ + 'Favorites', + 'RecentlyAdded', + 'SelfPortraits', + 'Screenshots', + 'UserLibrary', + ], width: 580, height: 580, cropping: true, cropperToolbarTitle: 'Select Profile Picture', mediaType: 'photo', cropperCircleOverlay: true, - }) - .then((picture) => { - if ('path' in picture) { - setForm({ - ...form, - smallPic: picture.path, - }); - } - }) - .catch(() => {}); + }).then((picture) => { + if ('path' in picture) { + setForm({ + ...form, + smallPic: picture.path, + }); + } + }); }; /* diff --git a/src/screens/onboarding/SocialMedia.tsx b/src/screens/onboarding/SocialMedia.tsx index d2a43e7a..2a978f94 100644 --- a/src/screens/onboarding/SocialMedia.tsx +++ b/src/screens/onboarding/SocialMedia.tsx @@ -2,7 +2,6 @@ import {RouteProp} from '@react-navigation/native'; import {StackNavigationProp} from '@react-navigation/stack'; import React from 'react'; import { - Alert, KeyboardAvoidingView, Platform, StatusBar, @@ -22,9 +21,8 @@ import { LinkSocialMedia, RegistrationWizard, } from '../../components'; -import {SOCIAL_LIST} from '../../constants/'; +import {SOCIAL_LIST, MOMENT_CATEGORIES} from '../../constants/'; import {OnboardingStackParams} from '../../routes'; -import {MOMENT_CATEGORIES_MAP} from '../../store/initialStates'; /** * Social Media Screen for displaying social media linkers @@ -55,8 +53,6 @@ const SocialMedia: React.FC<SocialMediaProps> = ({route, navigation}) => { linkers.push(linker); } - const dispatch = useDispatch(); - /** * Just commenting this out, in case we need it in the future */ @@ -69,9 +65,9 @@ const SocialMedia: React.FC<SocialMediaProps> = ({route, navigation}) => { const handleNext = () => { navigation.navigate('CategorySelection', { - categories: MOMENT_CATEGORIES_MAP, screenType: CategorySelectionScreenType.Onboarding, user: {userId: userId, username: username}, + newCustomCategory: undefined, }); }; diff --git a/src/screens/onboarding/WaitlistSuccessScreen.tsx b/src/screens/onboarding/WaitlistSuccessScreen.tsx new file mode 100644 index 00000000..1f603e3a --- /dev/null +++ b/src/screens/onboarding/WaitlistSuccessScreen.tsx @@ -0,0 +1,156 @@ +import {StackNavigationProp} from '@react-navigation/stack'; +import * as React from 'react'; +import { + KeyboardAvoidingView, + Linking, + Platform, + StatusBar, + StyleSheet, + Text, + TouchableOpacity, + View, +} from 'react-native'; +import {ArrowButton, Background, SubmitButton} from '../../components'; +import {OnboardingStackParams} from '../../routes'; +import {BackgroundGradientType} from '../../types'; +import CelebrationLogo from '../../assets/icons/celebration-logo.svg'; +import {SCREEN_HEIGHT} from '../../utils'; +import {TAGG_WEBSITE} from '../../constants'; + +type WaitlistSuccessScreenProp = StackNavigationProp< + OnboardingStackParams, + 'WaitlistSuccess' +>; + +interface WaitlistSuccessScreenProps { + navigation: WaitlistSuccessScreenProp; +} + +const WaitlistSuccessScreen: React.FC<WaitlistSuccessScreenProps> = ({ + navigation, +}) => { + const handleSignIn = () => { + navigation.navigate('InvitationCodeVerification'); + }; + + const Footer = () => ( + <View style={styles.footer}> + <ArrowButton + direction="backward" + onPress={() => navigation.navigate('AddWaitlistUser')} + /> + </View> + ); + return ( + <Background + style={styles.container} + gradientType={BackgroundGradientType.Light}> + <KeyboardAvoidingView + behavior={Platform.OS === 'ios' ? 'padding' : 'height'} + style={styles.container}> + <StatusBar barStyle="light-content" /> + <CelebrationLogo width={100} height={100} /> + <Text style={styles.heading}> + You've successfully joined{'\n'} + the waitlist, we'll let you know{'\n'} + as soon as your invite is{'\n'}ready! + </Text> + <Text style={[styles.subHeading, styles.subHeadOneMargin]}> + To learn more about Tagg you can visit our{'\n'}{' '} + <Text + style={styles.link} + onPress={() => { + Linking.openURL(TAGG_WEBSITE); + }}> + website + </Text> + . Thank you! + </Text> + <Text style={[styles.subHeading, styles.subHeadTwoMargin]}> + Got your invite text? + </Text> + <TouchableOpacity onPress={handleSignIn} style={styles.finalAction}> + <Text style={styles.finalActionLabel}>Sign In</Text> + </TouchableOpacity> + </KeyboardAvoidingView> + <Footer /> + </Background> + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + }, + wizard: { + ...Platform.select({ + ios: { + top: 50, + }, + android: { + bottom: 40, + }, + }), + }, + link: { + textDecorationLine: 'underline', + }, + finalAction: { + backgroundColor: 'white', + justifyContent: 'center', + alignItems: 'center', + width: 150, + height: 40, + borderRadius: 5, + borderWidth: 1, + borderColor: '#fff', + marginBottom: SCREEN_HEIGHT / 20, + marginTop: SCREEN_HEIGHT / 45, + }, + finalActionLabel: { + fontSize: 16, + fontWeight: '500', + color: 'black', + }, + formHeader: { + color: '#fff', + fontSize: 30, + fontWeight: '600', + marginBottom: '16%', + }, + footer: { + width: '100%', + flexDirection: 'row', + justifyContent: 'space-around', + ...Platform.select({ + ios: { + bottom: '20%', + }, + android: { + bottom: '10%', + }, + }), + }, + heading: { + fontSize: 25, + fontWeight: 'bold', + color: 'white', + marginTop: SCREEN_HEIGHT / 25, + textAlign: 'center', + }, + subHeading: { + color: 'white', + textAlign: 'center', + }, + subHeadOneMargin: { + marginTop: SCREEN_HEIGHT / 30, + }, + subHeadTwoMargin: { + marginTop: SCREEN_HEIGHT / 10, + }, +}); + +export default WaitlistSuccessScreen; diff --git a/src/screens/onboarding/index.ts b/src/screens/onboarding/index.ts index ec833929..14d0e405 100644 --- a/src/screens/onboarding/index.ts +++ b/src/screens/onboarding/index.ts @@ -11,3 +11,6 @@ export {default as PasswordResetRequest} from './PasswordResetRequest'; export {default as PasswordReset} from './PasswordReset'; export {default as WelcomeScreen} from './WelcomeScreen'; export {default as CategorySelection} from './CategorySelection'; +export {default as AddWaitlistUserScreen} from './AddWaitlistUserScreen'; +export {default as WaitlistSuccessScreen} from './WaitlistSuccessScreen'; +export {default as CreateCustomCategory} from './CreateCustomCategory'; diff --git a/src/screens/profile/CaptionScreen.tsx b/src/screens/profile/CaptionScreen.tsx index e9eed668..5537d6bf 100644 --- a/src/screens/profile/CaptionScreen.tsx +++ b/src/screens/profile/CaptionScreen.tsx @@ -14,20 +14,24 @@ import {SearchBackground, TaggBigInput} from '../../components'; import {SCREEN_WIDTH, StatusBarHeight} from '../../utils'; import AsyncStorage from '@react-native-community/async-storage'; import {RouteProp} from '@react-navigation/native'; -import {ProfileStackParams} from 'src/routes'; +import {MainStackParams} from 'src/routes'; import {StackNavigationProp} from '@react-navigation/stack'; import {CaptionScreenHeader} from '../../components/'; import {MOMENTS_ENDPOINT} from '../../constants'; import {useDispatch, useSelector} from 'react-redux'; -import {loadUserMoments} from '../../store/actions'; +import { + loadUserMoments, + updateProfileCompletionStage, +} from '../../store/actions'; import {RootState} from '../../store/rootReducer'; +import {postMoment} from '../../services'; /** * Upload Screen to allow users to upload posts to Tagg */ -type CaptionScreenRouteProp = RouteProp<ProfileStackParams, 'CaptionScreen'>; +type CaptionScreenRouteProp = RouteProp<MainStackParams, 'CaptionScreen'>; type CaptionScreenNavigationProp = StackNavigationProp< - ProfileStackParams, + MainStackParams, 'CaptionScreen' >; interface CaptionScreenProps { @@ -47,15 +51,6 @@ const CaptionScreen: React.FC<CaptionScreenProps> = ({route, navigation}) => { setCaption(caption); }; - const checkImageUploadStatus = (statusMap: object) => { - for (let [key, value] of Object.entries(statusMap)) { - if (value != 'Success') { - return false; - } - } - return true; - }; - const navigateToProfile = () => { //Since the logged In User is navigating to own profile, useXId is not required navigation.navigate('Profile', { @@ -66,43 +61,20 @@ const CaptionScreen: React.FC<CaptionScreenProps> = ({route, navigation}) => { const handleShare = async () => { try { - const request = new FormData(); - const uri = image.path; - var fileName = image.filename; - - //Manipulating filename to end with .jpg instead of .heic - if (fileName.endsWith('.heic') || fileName.endsWith('.HEIC')) { - fileName = fileName.split('.')[0] + '.jpg'; - } - request.append('image', { - uri: uri, - name: fileName, - type: 'image/jpg', - }); - request.append('moment', title); - request.append('user_id', userId); - request.append('captions', JSON.stringify({image: caption})); - - const token = await AsyncStorage.getItem('token'); - let response = await fetch(MOMENTS_ENDPOINT, { - method: 'POST', - headers: { - 'Content-Type': 'multipart/form-data', - Authorization: 'Token ' + token, - }, - body: request, - }); - let statusCode = response.status; - let data = await response.json(); - if (statusCode === 200 && checkImageUploadStatus(data)) { - Alert.alert('The picture was uploaded successfully!'); + const data = await postMoment( + image.filename, + image.path, + caption, + title, + userId, + ); + if (data) { dispatch(loadUserMoments(userId)); + dispatch(updateProfileCompletionStage(data)); navigateToProfile(); - } else { - Alert.alert('An error occured while uploading. Please try again!'); } } catch (err) { - Alert.alert('An error occured during authenticaion. Please login again!'); + console.log(err); } }; diff --git a/src/screens/profile/EditProfile.tsx b/src/screens/profile/EditProfile.tsx index ed012313..a6849c7a 100644 --- a/src/screens/profile/EditProfile.tsx +++ b/src/screens/profile/EditProfile.tsx @@ -131,43 +131,51 @@ const EditProfile: React.FC<EditProfileProps> = ({route, navigation}) => { const goToGalleryLargePic = () => { ImagePicker.openPicker({ - smartAlbums: ['Favorites', 'RecentlyAdded', 'SelfPortraits', 'Screenshots', 'UserLibrary'], + smartAlbums: [ + 'Favorites', + 'RecentlyAdded', + 'SelfPortraits', + 'Screenshots', + 'UserLibrary', + ], width: 580, height: 580, cropping: true, cropperToolbarTitle: 'Select Header', mediaType: 'photo', - }) - .then((picture) => { - if ('path' in picture) { - setForm({ - ...form, - largePic: picture.path, - }); - } - }) - .catch(() => {}); + }).then((picture) => { + if ('path' in picture) { + setForm({ + ...form, + largePic: picture.path, + }); + } + }); }; const goToGallerySmallPic = () => { ImagePicker.openPicker({ - smartAlbums: ['Favorites', 'RecentlyAdded', 'SelfPortraits', 'Screenshots', 'UserLibrary'], + smartAlbums: [ + 'Favorites', + 'RecentlyAdded', + 'SelfPortraits', + 'Screenshots', + 'UserLibrary', + ], width: 580, height: 580, cropping: true, cropperToolbarTitle: 'Select Profile Picture', mediaType: 'photo', cropperCircleOverlay: true, - }) - .then((picture) => { - if ('path' in picture) { - setForm({ - ...form, - smallPic: picture.path, - }); - } - }) - .catch(() => {}); + }).then((picture) => { + if ('path' in picture) { + setForm({ + ...form, + smallPic: picture.path, + }); + } + }); }; /* diff --git a/src/screens/profile/MomentUploadPromptScreen.tsx b/src/screens/profile/MomentUploadPromptScreen.tsx new file mode 100644 index 00000000..6111985d --- /dev/null +++ b/src/screens/profile/MomentUploadPromptScreen.tsx @@ -0,0 +1,114 @@ +import * as React from 'react'; +import {RouteProp} from '@react-navigation/native'; +import {StackNavigationProp} from '@react-navigation/stack'; +import {MainStackParams} from '../../routes'; +import CloseIcon from '../../assets/ionicons/close-outline.svg'; +import {StyleSheet, Text, View} from 'react-native'; +import {Moment} from '../../components'; +import {Image} from 'react-native-animatable'; + +type MomentUploadPromptScreenRouteProp = RouteProp< + MainStackParams, + 'MomentUploadPrompt' +>; +type MomentUploadPromptScreenNavigationProp = StackNavigationProp< + MainStackParams, + 'MomentUploadPrompt' +>; + +interface MomentUploadPromptScreenProps { + route: MomentUploadPromptScreenRouteProp; + navigation: MomentUploadPromptScreenNavigationProp; +} + +const MomentUploadPromptScreen: React.FC<MomentUploadPromptScreenProps> = ({ + route, + navigation, +}) => { + const {screenType, momentCategory} = route.params; + return ( + <View style={styles.container}> + <CloseIcon + height={'10%'} + width={'10%'} + color={'white'} + style={styles.closeButton} + onPress={() => { + navigation.goBack(); + }} + /> + + <Text style={styles.text}> + Post your first moment to {'\n'} continue building your digital {'\n'}{' '} + identity! + </Text> + <Image + source={require('../../assets/gifs/dotted-arrow-white.gif')} + style={styles.arrowGif} + /> + <Moment + key={1} + title={momentCategory} + images={[]} + userXId={undefined} + screenType={screenType} + handleMomentCategoryDelete={() => {}} + shouldAllowDeletion={false} + externalStyles={{ + container: styles.momentContainer, + titleText: styles.momentHeaderText, + header: styles.momentHeader, + scrollContainer: styles.momentScrollContainer, + }} + /> + </View> + ); +}; + +const styles = StyleSheet.create({ + container: { + flexDirection: 'column', + justifyContent: 'center', + }, + closeButton: { + position: 'relative', + height: '48%', + aspectRatio: 1, + top: 20, + }, + text: { + justifyContent: 'center', + color: '#fff', + fontWeight: 'bold', + fontSize: 20, + textAlign: 'center', + position: 'relative', + top: '40%', + }, + arrowGif: { + position: 'relative', + width: '25%', + height: '40%', + left: '40%', + aspectRatio: 1.2, + top: '50%', + transform: [{scaleX: -1}, {rotate: '15deg'}], + }, + + //Styles to adjust moment container + momentScrollContainer: { + backgroundColor: 'transparent', + }, + momentContainer: { + top: '62%', + backgroundColor: 'transparent', + }, + momentHeaderText: { + paddingBottom: '5%', + }, + momentHeader: { + backgroundColor: 'transparent', + }, +}); + +export default MomentUploadPromptScreen; diff --git a/src/screens/profile/ProfileScreen.tsx b/src/screens/profile/ProfileScreen.tsx index 1b9a1049..0ea96cd2 100644 --- a/src/screens/profile/ProfileScreen.tsx +++ b/src/screens/profile/ProfileScreen.tsx @@ -3,7 +3,7 @@ import {StatusBar} from 'react-native'; import Animated from 'react-native-reanimated'; import {Content, Cover, TabsGradient} from '../../components'; import {RouteProp, useFocusEffect} from '@react-navigation/native'; -import {ProfileStackParams} from '../../routes/'; +import {MainStackParams} from '../../routes/'; import {resetScreenType} from '../../store/actions'; import {useDispatch, useStore} from 'react-redux'; import {DUMMY_USERID} from '../../store/initialStates'; @@ -13,7 +13,7 @@ import {DUMMY_USERID} from '../../store/initialStates'; * including posts, messaging, and settings */ -type ProfileScreenRouteProps = RouteProp<ProfileStackParams, 'Profile'>; +type ProfileScreenRouteProps = RouteProp<MainStackParams, 'Profile'>; interface ProfileOnboardingProps { route: ProfileScreenRouteProps; diff --git a/src/screens/profile/index.ts b/src/screens/profile/index.ts index b6a13144..9d651729 100644 --- a/src/screens/profile/index.ts +++ b/src/screens/profile/index.ts @@ -5,3 +5,4 @@ export {default as IndividualMoment} from './IndividualMoment'; export {default as MomentCommentsScreen} from './MomentCommentsScreen'; export {default as FriendsListScreen} from './FriendsListScreen'; export {default as EditProfile} from './EditProfile'; +export {default as MomentUploadPromptScreen} from './MomentUploadPromptScreen'; diff --git a/src/screens/search/SearchScreen.tsx b/src/screens/search/SearchScreen.tsx index 78c0c5cc..4505163c 100644 --- a/src/screens/search/SearchScreen.tsx +++ b/src/screens/search/SearchScreen.tsx @@ -1,9 +1,17 @@ import AsyncStorage from '@react-native-community/async-storage'; -import React, {useEffect, useState} from 'react'; -import {Keyboard, ScrollView, StatusBar, StyleSheet} from 'react-native'; +import {useFocusEffect} from '@react-navigation/native'; +import React, {useCallback, useEffect, useState} from 'react'; +import { + Keyboard, + RefreshControl, + ScrollView, + StatusBar, + StyleSheet, +} from 'react-native'; import Animated, {Easing, timing} from 'react-native-reanimated'; +import {useDispatch, useSelector} from 'react-redux'; import { - DiscoverUsers, + Explore, RecentSearches, SearchBackground, SearchBar, @@ -13,6 +21,8 @@ import { TabsGradient, } from '../../components'; import {SEARCH_ENDPOINT, TAGG_TEXT_LIGHT_BLUE} from '../../constants'; +import {loadRecentlySearched, resetScreenType} from '../../store/actions'; +import {RootState} from '../../store/rootReducer'; import {ProfilePreviewType, ScreenType, UserType} from '../../types'; import {SCREEN_HEIGHT, SCREEN_WIDTH, StatusBarHeight} from '../../utils'; const NO_USER: UserType = { @@ -20,18 +30,13 @@ const NO_USER: UserType = { username: '', }; -import {RootState} from '../../store/rootReducer'; -import {useSelector, useDispatch} from 'react-redux'; -import {resetScreenType} from '../../store/actions'; -import {useFocusEffect} from '@react-navigation/native'; - /** * Search Screen for user recommendations and a search * tool to allow user to find other users */ const SearchScreen: React.FC = () => { - const {recentSearches, taggUsers} = useSelector( + const {recentSearches, explores} = useSelector( (state: RootState) => state.taggUsers, ); const [query, setQuery] = useState<string>(''); @@ -42,6 +47,19 @@ const SearchScreen: React.FC = () => { const [searching, setSearching] = useState(false); const top = Animated.useValue(-SCREEN_HEIGHT); const [user, setUser] = useState<UserType>(NO_USER); + const [refreshing, setRefreshing] = useState<boolean>(false); + + const dispatch = useDispatch(); + + const onRefresh = useCallback(() => { + const refrestState = async () => { + dispatch(loadRecentlySearched()); + }; + setRefreshing(true); + refrestState().then(() => { + setRefreshing(false); + }); + }, []); useEffect(() => { if (query.length < 3) { @@ -76,8 +94,6 @@ const SearchScreen: React.FC = () => { loadResults(query); }, [query]); - const dispatch = useDispatch(); - /** * Code under useFocusEffect gets executed every time the screen comes under focus / is being viewed by the user. * This is done to reset the users stored in our store for the Search screen. @@ -135,7 +151,10 @@ const SearchScreen: React.FC = () => { keyboardShouldPersistTaps={'always'} stickyHeaderIndices={[4]} contentContainerStyle={styles.contentContainer} - showsVerticalScrollIndicator={false}> + showsVerticalScrollIndicator={false} + refreshControl={ + <RefreshControl refreshing={refreshing} onRefresh={onRefresh} /> + }> <SearchHeader style={styles.header} {...{top}} /> <SearchBar style={styles.searchBar} @@ -146,13 +165,7 @@ const SearchScreen: React.FC = () => { value={query} {...{top, searching}} /> - {/* Removed for Alpha for now */} - {/* <Explore /> */} - <DiscoverUsers - sectionTitle="Discover Users" - users={taggUsers} - screenType={ScreenType.Search} - /> + <Explore /> <SearchResultsBackground {...{top}}> {results.length === 0 && recents.length !== 0 ? ( <RecentSearches diff --git a/src/services/ExploreServices.ts b/src/services/ExploreServices.ts index 2181ea7d..ca4f1b69 100644 --- a/src/services/ExploreServices.ts +++ b/src/services/ExploreServices.ts @@ -1,4 +1,8 @@ -import {ALL_USERS_ENDPOINT} from '../constants'; +import AsyncStorage from '@react-native-community/async-storage'; +import {getDeviceToken} from 'react-native-device-info'; +import {ALL_USERS_ENDPOINT, DISCOVER_ENDPOINT} from '../constants'; +import {EMPTY_EXPLORE_SECTIONS} from '../store/initialStates'; +import {ExploreSectionType, ProfilePreviewType} from '../types'; export const getAllTaggUsers = async (token: string) => { try { @@ -26,3 +30,31 @@ export const getAllTaggUsers = async (token: string) => { ); } }; + +export const getAllExploreSections = async () => { + try { + const token = await AsyncStorage.getItem('token'); + const response = await fetch(DISCOVER_ENDPOINT, { + method: 'GET', + headers: { + Authorization: 'Token ' + token, + }, + }); + if (response.status !== 200) { + return EMPTY_EXPLORE_SECTIONS; + } + const data = await response.json(); + const exploreSections: Record<ExploreSectionType, ProfilePreviewType[]> = { + 'New to Tagg': data.categories.new_to_tagg, + 'People You May Know': data.categories.people_you_may_know, + 'Trending on Tagg': data.categories.trending_on_tagg, + "Brown '21": data.categories.brown_21, + "Brown '22": data.categories.brown_22, + "Brown '23": data.categories.brown_23, + }; + + return exploreSections; + } catch (error) { + console.log('Unable to fetch explore data'); + } +}; diff --git a/src/services/MomentCategoryService.ts b/src/services/MomentCategoryService.ts index 8bdb70d2..57e64830 100644 --- a/src/services/MomentCategoryService.ts +++ b/src/services/MomentCategoryService.ts @@ -1,12 +1,11 @@ import {Alert} from 'react-native'; -import {MomentCategoryType} from './../types/types'; import {MOMENT_CATEGORY_ENDPOINT} from '../constants'; export const loadMomentCategories: ( userId: string, token: string, -) => Promise<MomentCategoryType[]> = async (userId, token) => { - let categories: MomentCategoryType[] = []; +) => Promise<string[]> = async (userId, token) => { + let categories: string[] = []; try { const response = await fetch(MOMENT_CATEGORY_ENDPOINT + `${userId}/`, { method: 'GET', @@ -17,7 +16,7 @@ export const loadMomentCategories: ( const status = response.status; if (status === 200) { const data = await response.json(); - categories = data['categories']; + categories = data.categories; } else { console.log('Could not load categories!'); return []; @@ -30,10 +29,9 @@ export const loadMomentCategories: ( }; export const postMomentCategories: ( - categories: Array<MomentCategoryType>, + categories: string[], token: string, -) => Promise<boolean> = async (categories, token) => { - let success = false; +) => Promise<number | undefined> = async (categories, token) => { try { const response = await fetch(MOMENT_CATEGORY_ENDPOINT, { method: 'POST', @@ -44,45 +42,16 @@ export const postMomentCategories: ( body: JSON.stringify({categories}), }); const status = response.status; + const data = await response.json(); if (status === 200) { - success = true; + return data['profile_completion_stage']; } else { - Alert.alert('There was a problem creating categories!'); - console.log('Could not post categories!'); + Alert.alert('There was a problem updating categories!'); + console.log('Unable to update categories'); } } catch (err) { console.log(err); - return success; + return undefined; } - return success; -}; - -export const deleteMomentCategories: ( - categories: Array<MomentCategoryType>, - userId: string, - token: string, -) => Promise<boolean> = async (categories, userId, token) => { - let success = false; - try { - const response = await fetch(MOMENT_CATEGORY_ENDPOINT + `${userId}/`, { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - Authorization: 'Token ' + token, - }, - body: JSON.stringify({categories}), - }); - const status = response.status; - if (status === 200) { - Alert.alert(`The category was successfully deleted!`); - success = true; - } else { - Alert.alert('There was a problem while deleteing category!'); - console.log('Could not delete category!'); - } - } catch (err) { - console.log(err); - return success; - } - return success; + return undefined; }; diff --git a/src/services/MomentServices.ts b/src/services/MomentServices.ts index 96643bc3..91ecf712 100644 --- a/src/services/MomentServices.ts +++ b/src/services/MomentServices.ts @@ -2,6 +2,7 @@ import AsyncStorage from '@react-native-community/async-storage'; import {Alert} from 'react-native'; import {COMMENTS_ENDPOINT, MOMENTS_ENDPOINT} from '../constants'; import {MomentType} from '../types'; +import {checkImageUploadStatus} from '../utils'; //Get all comments for a moment export const getMomentComments = async ( @@ -97,6 +98,57 @@ export const getMomentCommentsCount = async ( } }; +export const postMoment: ( + fileName: string, + uri: string, + caption: string, + category: string, + userId: string, +) => Promise<number | undefined> = async ( + fileName, + uri, + caption, + category, + userId, +) => { + try { + const request = new FormData(); + //Manipulating filename to end with .jpg instead of .heic + if (fileName.endsWith('.heic') || fileName.endsWith('.HEIC')) { + fileName = fileName.split('.')[0] + '.jpg'; + } + request.append('image', { + uri: uri, + name: fileName, + type: 'image/jpg', + }); + request.append('moment', category); + request.append('user_id', userId); + request.append('captions', JSON.stringify({image: caption})); + const token = await AsyncStorage.getItem('token'); + let response = await fetch(MOMENTS_ENDPOINT, { + method: 'POST', + headers: { + 'Content-Type': 'multipart/form-data', + Authorization: 'Token ' + token, + }, + body: request, + }); + let statusCode = response.status; + let data = await response.json(); + if (statusCode === 200 && checkImageUploadStatus(data['moments'])) { + Alert.alert('The picture was uploaded successfully!'); + return data['profile_completion_stage']; + } else { + Alert.alert('An error occured while uploading. Please try again!'); + } + } catch (err) { + console.log(err); + Alert.alert('An error occured during authenticaion. Please login again!'); + } + return undefined; +}; + export const loadMoments: ( userId: string, token: string, diff --git a/src/services/UserProfileService.ts b/src/services/UserProfileService.ts index 75042830..793ee44d 100644 --- a/src/services/UserProfileService.ts +++ b/src/services/UserProfileService.ts @@ -38,6 +38,7 @@ export const loadProfileInfo = async (token: string, userId: string) => { snapchat, tiktok, university_class, + profile_completion_stage, } = info; birthday = birthday && moment(birthday).format('YYYY-MM-DD'); return { @@ -49,6 +50,7 @@ export const loadProfileInfo = async (token: string, userId: string) => { snapchat, tiktok, university_class, + profile_completion_stage, }; } else { throw 'Unable to load profile data'; diff --git a/src/services/WaitlistUserService.tsx b/src/services/WaitlistUserService.tsx new file mode 100644 index 00000000..516affe3 --- /dev/null +++ b/src/services/WaitlistUserService.tsx @@ -0,0 +1,45 @@ +import {Alert} from 'react-native'; +import {WAITLIST_USER_ENDPOINT} from '../constants'; + +export const adduserToWaitlist: ( + phone_number: string, + first_name: string, + last_name: string, +) => Promise<boolean> = async (phone_number, first_name, last_name) => { + try { + console.log(phone_number, first_name, last_name); + const response = await fetch(WAITLIST_USER_ENDPOINT, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + phone_number, + first_name, + last_name, + }), + }); + const status = response.status; + const message = await response.json(); + if (status === 200) { + return true; + } else { + if (status === 409) { + Alert.alert('You are already on our waitlist / on our app'); + } else if (status === 400) { + Alert.alert('Some information needed was missing / ill-formatted'); + } else if (status === 500) { + Alert.alert( + 'Something went wrong. Sorry unable to add you to the waitlist 😔', + ); + } + console.log(message); + } + } catch (err) { + Alert.alert( + 'Something went wrong. Sorry unable to add you to the waitlist 😔', + ); + console.log(err); + } + return false; +}; diff --git a/src/services/index.ts b/src/services/index.ts index 7ea5bf5d..56cefddd 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -8,3 +8,4 @@ export * from './BlockUserService'; export * from './MomentCategoryService'; export * from './NotificationService'; export * from './FCMService'; +export * from './WaitlistUserService'; diff --git a/src/store/actions/momentCategories.tsx b/src/store/actions/momentCategories.tsx index a522c3e0..c91e9ec8 100644 --- a/src/store/actions/momentCategories.tsx +++ b/src/store/actions/momentCategories.tsx @@ -1,13 +1,11 @@ import {RootState} from '../rootReducer'; -import { - deleteMomentCategories, - loadMomentCategories, - postMomentCategories, -} from '../../services'; +import {loadMomentCategories, postMomentCategories} from '../../services'; import {Action, ThunkAction} from '@reduxjs/toolkit'; -import {momentCategoriesFetched} from '../reducers'; +import { + momentCategoriesFetched, + profileCompletionStageUpdated, +} from '../reducers'; import {getTokenOrLogout} from '../../utils'; -import {MomentCategoryType} from '../../types'; /** * Load all categories for user @@ -23,7 +21,7 @@ export const loadUserMomentCategories = ( const categories = await loadMomentCategories(userId, token); dispatch({ type: momentCategoriesFetched.type, - payload: {categories, add: true}, + payload: {categories}, }); } catch (error) { console.log(error); @@ -33,29 +31,32 @@ export const loadUserMomentCategories = ( /** * Handle addition / deletion of categories for a user * @param categories List of categories - * @param add boolean, if true, we add new categories, else we delete - * @param userId id of the user for whom categories should be updated + * @param add true if the call to his function is to add categories */ export const updateMomentCategories = ( - categories: Array<MomentCategoryType>, + categories: string[], add: boolean, - userId: string, ): ThunkAction<Promise<void>, RootState, unknown, Action<string>> => async ( dispatch, ) => { try { const token = await getTokenOrLogout(dispatch); let success = false; - if (add) { - success = await postMomentCategories(categories, token); - } else { - success = await deleteMomentCategories(categories, userId, token); - } + let stage: number | undefined = 1; + + stage = await postMomentCategories(categories, token); + success = stage ? true : false; if (success) { dispatch({ type: momentCategoriesFetched.type, - payload: {categories, add}, + payload: {categories}, }); + if (add) { + dispatch({ + type: profileCompletionStageUpdated.type, + payload: {stage}, + }); + } } } catch (error) { console.log(error); diff --git a/src/store/actions/taggUsers.ts b/src/store/actions/taggUsers.ts index 7f841c51..7b6d4d5e 100644 --- a/src/store/actions/taggUsers.ts +++ b/src/store/actions/taggUsers.ts @@ -1,8 +1,7 @@ -import {RootState} from '../rootReducer'; -import {loadRecentlySearchedUsers, getAllTaggUsers} from '../../services'; import {Action, ThunkAction} from '@reduxjs/toolkit'; +import {getAllExploreSections, loadRecentlySearchedUsers} from '../../services'; import {taggUsersFetched} from '../reducers'; -import {getTokenOrLogout} from '../../utils'; +import {RootState} from '../rootReducer'; export const loadRecentlySearched = (): ThunkAction< Promise<void>, @@ -11,12 +10,11 @@ export const loadRecentlySearched = (): ThunkAction< Action<string> > => async (dispatch) => { try { - const token = await getTokenOrLogout(dispatch); const recentSearches = await loadRecentlySearchedUsers(); - const taggUsers = await getAllTaggUsers(token); + const exploreSections = await getAllExploreSections(); dispatch({ type: taggUsersFetched.type, - payload: {recentSearches, taggUsers}, + payload: {recentSearches, explores: exploreSections}, }); } catch (error) { console.log(error); diff --git a/src/store/actions/user.ts b/src/store/actions/user.ts index eee5fcde..8550f3bd 100644 --- a/src/store/actions/user.ts +++ b/src/store/actions/user.ts @@ -2,7 +2,13 @@ import {RootState} from '../rootReducer'; import {UserType} from '../../types/types'; import {loadProfileInfo, loadAvatar, loadCover} from '../../services'; import {Action, ThunkAction} from '@reduxjs/toolkit'; -import {userLoggedIn, userDetailsFetched, socialEdited} from '../reducers'; +import { + userLoggedIn, + userDetailsFetched, + socialEdited, + profileCompletionStageUpdated, + setIsOnboardedUser, +} from '../reducers'; import {getTokenOrLogout} from '../../utils'; /** @@ -50,7 +56,6 @@ export const updateSocial = ( dispatch, ) => { try { - console.log(social); dispatch({ type: socialEdited.type, payload: {social, value}, @@ -60,6 +65,36 @@ export const updateSocial = ( } }; +export const updateProfileCompletionStage = ( + stage: number, +): ThunkAction<Promise<void>, RootState, unknown, Action<string>> => async ( + dispatch, +) => { + try { + dispatch({ + type: profileCompletionStageUpdated.type, + payload: {stage}, + }); + } catch (error) { + console.log(error); + } +}; + +export const updateIsOnboardedUser = ( + isOnboardedUser: boolean, +): ThunkAction<Promise<void>, RootState, unknown, Action<string>> => async ( + dispatch, +) => { + try { + dispatch({ + type: setIsOnboardedUser.type, + payload: {isOnboardedUser}, + }); + } catch (error) { + console.log(error); + } +}; + export const logout = (): ThunkAction< Promise<void>, RootState, diff --git a/src/store/initialStates.ts b/src/store/initialStates.ts index da3ef3b0..87e1ce22 100644 --- a/src/store/initialStates.ts +++ b/src/store/initialStates.ts @@ -1,5 +1,5 @@ import { - MomentCategoryType, + ExploreSectionType, MomentType, NotificationType, ProfilePreviewType, @@ -17,6 +17,7 @@ export const NO_PROFILE: ProfileType = { gender: '', birthday: undefined, university_class: 2021, + profile_completion_stage: 1, snapchat: '', tiktok: '', }; @@ -37,6 +38,7 @@ export const NO_USER_DATA = { profile: <ProfileType>NO_PROFILE, avatar: <string | null>'', cover: <string | null>'', + isOnboardedUser: false, }; export const NO_FRIENDS_DATA = { @@ -57,9 +59,21 @@ export const NO_SOCIAL_ACCOUNTS: Record<string, SocialAccountType> = { Twitter: {posts: []}, }; +export const EMPTY_EXPLORE_SECTIONS: Record< + ExploreSectionType, + ProfilePreviewType[] +> = { + 'People You May Know': EMPTY_PROFILE_PREVIEW_LIST, + 'New to Tagg': EMPTY_PROFILE_PREVIEW_LIST, + 'Trending on Tagg': EMPTY_PROFILE_PREVIEW_LIST, + "Brown '21": EMPTY_PROFILE_PREVIEW_LIST, + "Brown '22": EMPTY_PROFILE_PREVIEW_LIST, + "Brown '23": EMPTY_PROFILE_PREVIEW_LIST, +}; + export const NO_TAGG_USERS = { recentSearches: EMPTY_PROFILE_PREVIEW_LIST, - taggUsers: EMPTY_PROFILE_PREVIEW_LIST, + explores: EMPTY_EXPLORE_SECTIONS, }; export const NO_SOCIALS = { @@ -70,23 +84,7 @@ export const NO_BLOCKED_USERS = { blockedUsers: EMPTY_PROFILE_PREVIEW_LIST, }; -export const MOMENT_CATEGORIES_MAP: Record<MomentCategoryType, boolean> = { - Friends: false, - Adventure: false, - 'Photo Dump': false, - Food: false, - Music: false, - Art: false, - Sports: false, - Fashion: false, - Travel: false, - Pets: false, - Fitness: false, - DIY: false, - Nature: false, - 'Early Life': false, - Beauty: false, -}; +export const EMPTY_MOMENT_CATEGORIES: string[] = []; /** * The dummy userId and username serve the purpose of preventing app crash @@ -99,7 +97,7 @@ export const DUMMY_USERNAME = 'tagg_userX'; export const EMPTY_USER_X = <UserXType>{ friends: EMPTY_PROFILE_PREVIEW_LIST, moments: EMPTY_MOMENTS_LIST, - momentCategories: MOMENT_CATEGORIES_MAP, + momentCategories: EMPTY_MOMENT_CATEGORIES, socialAccounts: NO_SOCIAL_ACCOUNTS, user: NO_USER, profile: NO_PROFILE, @@ -124,5 +122,5 @@ export const EMPTY_SCREEN_TO_USERS_LIST: Record< }; export const INITIAL_CATEGORIES_STATE = { - momentCategories: MOMENT_CATEGORIES_MAP, + momentCategories: EMPTY_MOMENT_CATEGORIES, }; diff --git a/src/store/reducers/momentCategoryReducer.tsx b/src/store/reducers/momentCategoryReducer.tsx index d1f448f9..b6909b87 100644 --- a/src/store/reducers/momentCategoryReducer.tsx +++ b/src/store/reducers/momentCategoryReducer.tsx @@ -1,19 +1,16 @@ import {createSlice} from '@reduxjs/toolkit'; import {INITIAL_CATEGORIES_STATE} from '../initialStates'; -import {MomentCategoryType} from '../../types'; const momentCategoriesSlice = createSlice({ name: 'momentCategories', initialState: INITIAL_CATEGORIES_STATE, reducers: { /** - * One stop to add / delete / update categories for a user + * Replace a new copy of moment categories for a user */ momentCategoriesFetched: (state, action) => { - const categories: Array<MomentCategoryType> = action.payload.categories; - for (let category of categories) { - state.momentCategories[category] = action.payload.add; - } + const categories: string[] = action.payload.categories; + state.momentCategories = categories; }, }, }); diff --git a/src/store/reducers/taggUsersReducer.ts b/src/store/reducers/taggUsersReducer.ts index ff30f7a0..33e2e18d 100644 --- a/src/store/reducers/taggUsersReducer.ts +++ b/src/store/reducers/taggUsersReducer.ts @@ -6,8 +6,8 @@ const taggUsersSlice = createSlice({ initialState: NO_TAGG_USERS, reducers: { taggUsersFetched: (state, action) => { - state.recentSearches = action.payload.taggUsers; - state.taggUsers = action.payload.taggUsers; + state.recentSearches = action.payload.recentSearches; + state.explores = action.payload.explores; }, }, }); diff --git a/src/store/reducers/userReducer.ts b/src/store/reducers/userReducer.ts index 2fd5c462..2e71e38e 100644 --- a/src/store/reducers/userReducer.ts +++ b/src/store/reducers/userReducer.ts @@ -41,6 +41,14 @@ const userDataSlice = createSlice({ break; } }, + + profileCompletionStageUpdated: (state, action) => { + state.profile.profile_completion_stage = action.payload.stage; + }, + + setIsOnboardedUser: (state, action) => { + state.isOnboardedUser = action.payload.isOnboardedUser; + }, }, }); @@ -48,5 +56,7 @@ export const { userLoggedIn, userDetailsFetched, socialEdited, + profileCompletionStageUpdated, + setIsOnboardedUser, } = userDataSlice.actions; export const userDataReducer = userDataSlice.reducer; diff --git a/src/store/reducers/userXReducer.ts b/src/store/reducers/userXReducer.ts index fa1598b2..3b00cf88 100644 --- a/src/store/reducers/userXReducer.ts +++ b/src/store/reducers/userXReducer.ts @@ -1,4 +1,4 @@ -import {MomentCategoryType, ScreenType} from '../../types/types'; +import {ScreenType} from '../../types/types'; import {EMPTY_SCREEN_TO_USERS_LIST, EMPTY_USER_X} from '../initialStates'; import {createSlice} from '@reduxjs/toolkit'; @@ -24,12 +24,10 @@ const userXSlice = createSlice({ }, userXMomentCategoriesFetched: (state, action) => { - const categories: Array<MomentCategoryType> = action.payload.data; - for (let category of categories) { - state[<ScreenType>action.payload.screenType][ - action.payload.userId - ].momentCategories[category] = true; - } + const categories: string[] = action.payload.data; + state[<ScreenType>action.payload.screenType][ + action.payload.userId + ].momentCategories = categories; }, userXMomentsFetched: (state, action) => { diff --git a/src/types/types.ts b/src/types/types.ts index b3148bc4..093adbe4 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -19,6 +19,7 @@ export interface ProfileType { website: string; gender: string; university_class: number; + profile_completion_stage: number; birthday: Date | undefined; snapchat: string; tiktok: string; @@ -102,6 +103,14 @@ export enum ScreenType { Notifications, } +export type ExploreSectionType = + | 'People You May Know' + | 'New to Tagg' + | 'Trending on Tagg' + | "Brown '21" + | "Brown '22" + | "Brown '23"; + /** * Redux store to have a Record of ScreenType (Search, Profile, Home etc) mapped to * A Record of userIXd mapped to UserXType @@ -113,7 +122,7 @@ export interface UserXType { friends: ProfilePreviewType[]; moments: MomentType[]; socialAccounts: Record<string, SocialAccountType>; - momentCategories: Record<MomentCategoryType, boolean>; + momentCategories: string[]; user: UserType; profile: ProfileType; avatar: string; @@ -129,26 +138,6 @@ export enum VerificationScreenType { } /** - * Default moment categories - */ -export type MomentCategoryType = - | 'Friends' - | 'Adventure' - | 'Photo Dump' - | 'Food' - | 'Music' - | 'Art' - | 'Sports' - | 'Fashion' - | 'Travel' - | 'Pets' - | 'Fitness' - | 'DIY' - | 'Nature' - | 'Early Life' - | 'Beauty'; - -/** * Two types for category selection screen */ export enum CategorySelectionScreenType { diff --git a/src/utils/common.ts b/src/utils/common.ts index a2f88e8b..dbe8f270 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -47,3 +47,28 @@ export const getDateAge: ( return 'unknown'; } }; + +export const moveCategory: ( + categories: string[], + category: string, + moveUp: boolean, +) => string[] = (categories, category, moveUp) => { + const i = categories.indexOf(category); + const swapTarget = moveUp ? i - 1 : i + 1; + if ((moveUp && i === 0) || (!moveUp && i > categories.length)) { + return categories; + } + const tmp = categories[i]; + categories[i] = categories[swapTarget]; + categories[swapTarget] = tmp; + return categories; +}; + +export const checkImageUploadStatus = (statusMap: object) => { + for (let [key, value] of Object.entries(statusMap)) { + if (value != 'Success') { + return false; + } + } + return true; +}; diff --git a/src/utils/users.ts b/src/utils/users.ts index bcb43cbc..c54ea715 100644 --- a/src/utils/users.ts +++ b/src/utils/users.ts @@ -51,6 +51,11 @@ export const userLogin = async (dispatch: AppDispatch, user: UserType) => { } else { return; } + } else { + await Promise.all([ + AsyncStorage.setItem('userId', user.userId), + AsyncStorage.setItem('username', user.username), + ]); } await loadData(dispatch, localUser); } catch (error) { diff --git a/tsconfig.json b/tsconfig.json index 2edf373a..cd6ac9a1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ /* Basic Options */ "target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ - "lib": ["es6"], /* Specify library files to be included in the compilation. */ + "lib": ["es6", "ES2016"], /* Specify library files to be included in the compilation. */ "allowJs": true, /* Allow javascript files to be compiled. */ // "checkJs": true, /* Report errors in .js files. */ "jsx": "react-native", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ |