diff options
Diffstat (limited to 'src/screens')
| -rw-r--r-- | src/screens/main/NotificationsScreen.tsx | 9 | ||||
| -rw-r--r-- | src/screens/onboarding/AddWaitlistUserScreen.tsx | 238 | ||||
| -rw-r--r-- | src/screens/onboarding/CategorySelection.tsx | 202 | ||||
| -rw-r--r-- | src/screens/onboarding/CreateCustomCategory.tsx | 123 | ||||
| -rw-r--r-- | src/screens/onboarding/InvitationCodeVerification.tsx | 29 | ||||
| -rw-r--r-- | src/screens/onboarding/Login.tsx | 15 | ||||
| -rw-r--r-- | src/screens/onboarding/ProfileOnboarding.tsx | 52 | ||||
| -rw-r--r-- | src/screens/onboarding/SocialMedia.tsx | 8 | ||||
| -rw-r--r-- | src/screens/onboarding/WaitlistSuccessScreen.tsx | 156 | ||||
| -rw-r--r-- | src/screens/onboarding/index.ts | 3 | ||||
| -rw-r--r-- | src/screens/profile/CaptionScreen.tsx | 64 | ||||
| -rw-r--r-- | src/screens/profile/EditProfile.tsx | 52 | ||||
| -rw-r--r-- | src/screens/profile/MomentUploadPromptScreen.tsx | 114 | ||||
| -rw-r--r-- | src/screens/profile/ProfileScreen.tsx | 4 | ||||
| -rw-r--r-- | src/screens/profile/index.ts | 1 | ||||
| -rw-r--r-- | src/screens/search/SearchScreen.tsx | 51 |
16 files changed, 945 insertions, 176 deletions
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 |
