aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/assets/gifs/dotted-arrow-white.gifbin0 -> 11793 bytes
-rw-r--r--src/components/common/TaggPopup.tsx19
-rw-r--r--src/components/common/TaggPrompt.tsx79
-rw-r--r--src/components/common/index.ts1
-rw-r--r--src/components/moments/Moment.tsx28
-rw-r--r--src/components/profile/Content.tsx84
-rw-r--r--src/routes/main/MainStackNavigator.tsx20
-rw-r--r--src/routes/main/MainStackScreen.tsx10
-rw-r--r--src/routes/tabs/NavigationBar.tsx6
-rw-r--r--src/screens/onboarding/CategorySelection.tsx20
-rw-r--r--src/screens/onboarding/SocialMedia.tsx1
-rw-r--r--src/screens/profile/CaptionScreen.tsx64
-rw-r--r--src/screens/profile/MomentUploadPromptScreen.tsx114
-rw-r--r--src/screens/profile/index.ts1
-rw-r--r--src/services/MomentCategoryService.ts10
-rw-r--r--src/services/MomentServices.ts52
-rw-r--r--src/services/UserProfileService.ts2
-rw-r--r--src/store/actions/momentCategories.tsx20
-rw-r--r--src/store/actions/user.ts39
-rw-r--r--src/store/initialStates.ts2
-rw-r--r--src/store/reducers/userReducer.ts10
-rw-r--r--src/types/types.ts1
-rw-r--r--src/utils/common.ts9
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
new file mode 100644
index 00000000..a3f0a153
--- /dev/null
+++ b/src/assets/gifs/dotted-arrow-white.gif
Binary files differ
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;
+};