diff options
Diffstat (limited to 'src')
23 files changed, 499 insertions, 93 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/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 be6f78a8..446bc07b 100644 --- a/src/components/moments/Moment.tsx +++ b/src/components/moments/Moment.tsx @@ -1,6 +1,13 @@ import {useNavigation} from '@react-navigation/native'; import React from 'react'; -import {Alert, StyleSheet, View} from 'react-native'; +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'; @@ -20,6 +27,7 @@ interface MomentProps { screenType: ScreenType; handleMomentCategoryDelete: (_: string) => void; shouldAllowDeletion: boolean; + externalStyles?: Record<string, StyleProp<ViewStyle>>; } const Moment: React.FC<MomentProps> = ({ @@ -29,6 +37,7 @@ const Moment: React.FC<MomentProps> = ({ screenType, handleMomentCategoryDelete, shouldAllowDeletion, + externalStyles, }) => { const navigation = useNavigation(); @@ -63,9 +72,11 @@ const Moment: React.FC<MomentProps> = ({ }); }; 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 ? ( <> <PlusIcon @@ -90,7 +101,7 @@ const Moment: React.FC<MomentProps> = ({ <ScrollView horizontal showsHorizontalScrollIndicator={false} - style={styles.scrollContainer}> + style={[styles.scrollContainer, externalStyles?.scrollContainer]}> {images && images.map((imageObj: MomentType) => ( <MomentTile @@ -107,7 +118,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> @@ -126,12 +137,9 @@ 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: { diff --git a/src/components/profile/Content.tsx b/src/components/profile/Content.tsx index 5fa05588..227e6783 100644 --- a/src/components/profile/Content.tsx +++ b/src/components/profile/Content.tsx @@ -43,8 +43,9 @@ import { } 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>; @@ -97,6 +98,14 @@ const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => { const [shouldBounce, setShouldBounce] = useState<boolean>(true); const [refreshing, setRefreshing] = useState<boolean>(false); + //These two booleans are used to see if user closed the pormpt displayed to them + const [isStageTwoPromptClosed, setIsStageTwoPromptClosed] = useState<boolean>( + false, + ); + const [isStageThreePromptClosed, setIsStageThreePromptClosed] = useState< + boolean + >(false); + const onRefresh = useCallback(() => { const refrestState = async () => { if (!userXId) { @@ -134,6 +143,43 @@ const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => { }, [createImagesMap]); /** + * 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]) { + navigation.navigate('MomentUploadPrompt', { + screenType, + momentCategory: momentCategories[0], + }); + } + break; + case 2: + setIsStageTwoPromptClosed(false); + break; + case 3: + setIsStageThreePromptClosed(false); + break; + default: + break; + } + }; + setTimeout(navigateToMomentUploadPrompt, 2000); + }, [profile.profile_completion_stage, momentCategories]), + ); + + /** * This hook is called on load of profile and when you update the friends list. */ useEffect(() => { @@ -227,10 +273,8 @@ const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => { onPress: () => { dispatch( updateMomentCategories( - momentCategories.filter( - (mc) => mc !== category, - loggedInUser.userId, - ), + momentCategories.filter((mc) => mc !== category), + false, ), ); dispatch(deleteUserMomentsForCategory(category)); @@ -296,6 +340,34 @@ const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => { } has not posted any moments yet`}</Text> </View> )} + {!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)) && ( @@ -310,7 +382,7 @@ const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => { /> ), )} - {!userXId && ( + {!userXId && profile.profile_completion_stage !== 1 && ( <TouchableOpacity onPress={() => navigation.push('CategorySelection', { diff --git a/src/routes/main/MainStackNavigator.tsx b/src/routes/main/MainStackNavigator.tsx index 950f3ffc..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 {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: { @@ -40,11 +46,19 @@ export type MainStackParams = { userId: string; username: string; }; - CategorySelection: {}; + CategorySelection: { + 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 4ad5bf40..6f7bd413 100644 --- a/src/routes/main/MainStackScreen.tsx +++ b/src/routes/main/MainStackScreen.tsx @@ -10,6 +10,7 @@ import { CategorySelection, FriendsListScreen, NotificationsScreen, + MomentUploadPromptScreen, CreateCustomCategory, } from '../../screens'; import {MainStack, MainStackParams} from './MainStackNavigator'; @@ -17,7 +18,6 @@ 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'; /** * Trying to explain the purpose of each route on the stack (ACTUALLY A STACK) @@ -169,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/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/onboarding/CategorySelection.tsx b/src/screens/onboarding/CategorySelection.tsx index 540b106f..5589ea9e 100644 --- a/src/screens/onboarding/CategorySelection.tsx +++ b/src/screens/onboarding/CategorySelection.tsx @@ -17,7 +17,10 @@ import {Background, MomentCategory} from '../../components'; import {MOMENT_CATEGORIES} from '../../constants'; import {OnboardingStackParams} from '../../routes'; import {fcmService, postMomentCategories} from '../../services'; -import {updateMomentCategories} from '../../store/actions/momentCategories'; +import { + updateMomentCategories, + updateIsOnboardedUser, +} from '../../store/actions/'; import {RootState} from '../../store/rootReducer'; import {BackgroundGradientType, CategorySelectionScreenType} from '../../types'; import {getTokenOrLogout, SCREEN_WIDTH, userLogin} from '../../utils'; @@ -94,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 at least a category to begin creating moments!', - next: undefined, - }, + 'Use pictures and videos to share \ndifferent aspects of you', + next: undefined, }, }); } @@ -166,13 +164,17 @@ const CategorySelection: React.FC<CategorySelectionProps> = ({ } 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(momentCategories.concat(selectedCategories)), + updateMomentCategories( + momentCategories.concat(selectedCategories), + true, + ), ); navigation.goBack(); } diff --git a/src/screens/onboarding/SocialMedia.tsx b/src/screens/onboarding/SocialMedia.tsx index 32beb4bc..2a978f94 100644 --- a/src/screens/onboarding/SocialMedia.tsx +++ b/src/screens/onboarding/SocialMedia.tsx @@ -67,6 +67,7 @@ const SocialMedia: React.FC<SocialMediaProps> = ({route, navigation}) => { navigation.navigate('CategorySelection', { screenType: CategorySelectionScreenType.Onboarding, user: {userId: userId, username: username}, + newCustomCategory: undefined, }); }; 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/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/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/services/MomentCategoryService.ts b/src/services/MomentCategoryService.ts index 32c721ae..57e64830 100644 --- a/src/services/MomentCategoryService.ts +++ b/src/services/MomentCategoryService.ts @@ -31,8 +31,7 @@ export const loadMomentCategories: ( export const postMomentCategories: ( 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', @@ -43,15 +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 updating categories!'); console.log('Unable to update categories'); } } catch (err) { console.log(err); - return success; + return undefined; } - 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/store/actions/momentCategories.tsx b/src/store/actions/momentCategories.tsx index 987fc9e5..c91e9ec8 100644 --- a/src/store/actions/momentCategories.tsx +++ b/src/store/actions/momentCategories.tsx @@ -1,7 +1,10 @@ import {RootState} from '../rootReducer'; import {loadMomentCategories, postMomentCategories} from '../../services'; import {Action, ThunkAction} from '@reduxjs/toolkit'; -import {momentCategoriesFetched} from '../reducers'; +import { + momentCategoriesFetched, + profileCompletionStageUpdated, +} from '../reducers'; import {getTokenOrLogout} from '../../utils'; /** @@ -28,21 +31,32 @@ export const loadUserMomentCategories = ( /** * Handle addition / deletion of categories for a user * @param categories List of categories - * @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: string[], + add: boolean, ): ThunkAction<Promise<void>, RootState, unknown, Action<string>> => async ( dispatch, ) => { try { const token = await getTokenOrLogout(dispatch); - const success = await postMomentCategories(categories, token); + let success = false; + let stage: number | undefined = 1; + + stage = await postMomentCategories(categories, token); + success = stage ? true : false; if (success) { dispatch({ type: momentCategoriesFetched.type, payload: {categories}, }); + if (add) { + dispatch({ + type: profileCompletionStageUpdated.type, + payload: {stage}, + }); + } } } 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 09607758..de97b129 100644 --- a/src/store/initialStates.ts +++ b/src/store/initialStates.ts @@ -16,6 +16,7 @@ export const NO_PROFILE: ProfileType = { gender: '', birthday: undefined, university_class: 2021, + profile_completion_stage: 1, snapchat: '', tiktok: '', }; @@ -36,6 +37,7 @@ export const NO_USER_DATA = { profile: <ProfileType>NO_PROFILE, avatar: <string | null>'', cover: <string | null>'', + isOnboardedUser: false, }; export const NO_FRIENDS_DATA = { 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/types/types.ts b/src/types/types.ts index ee5103a2..10e5de9a 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; diff --git a/src/utils/common.ts b/src/utils/common.ts index a2f88e8b..f13181c1 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -47,3 +47,12 @@ export const getDateAge: ( return 'unknown'; } }; + +export const checkImageUploadStatus = (statusMap: object) => { + for (let [key, value] of Object.entries(statusMap)) { + if (value != 'Success') { + return false; + } + } + return true; +}; |