diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/assets/gifs/loading-animation.gif | bin | 0 -> 285396 bytes | |||
-rw-r--r-- | src/assets/navigationIcons/new-notifications.png | bin | 0 -> 92197 bytes | |||
-rw-r--r-- | src/components/common/NavigationIcon.tsx | 5 | ||||
-rw-r--r-- | src/components/common/TaggLoadingIndicator.tsx | 40 | ||||
-rw-r--r-- | src/components/profile/ProfileBody.tsx | 3 | ||||
-rw-r--r-- | src/routes/Routes.tsx | 7 | ||||
-rw-r--r-- | src/routes/tabs/NavigationBar.tsx | 43 | ||||
-rw-r--r-- | src/screens/main/NotificationsScreen.tsx | 36 | ||||
-rw-r--r-- | src/screens/profile/CaptionScreen.tsx | 43 | ||||
-rw-r--r-- | src/screens/profile/EditProfile.tsx | 17 | ||||
-rw-r--r-- | src/screens/profile/SocialMediaTaggs.tsx | 10 | ||||
-rw-r--r-- | src/services/MomentServices.ts | 10 | ||||
-rw-r--r-- | src/store/actions/user.ts | 16 | ||||
-rw-r--r-- | src/store/initialStates.ts | 5 | ||||
-rw-r--r-- | src/store/reducers/userReducer.ts | 5 | ||||
-rw-r--r-- | src/utils/common.ts | 22 |
16 files changed, 197 insertions, 65 deletions
diff --git a/src/assets/gifs/loading-animation.gif b/src/assets/gifs/loading-animation.gif Binary files differnew file mode 100644 index 00000000..6a69b07b --- /dev/null +++ b/src/assets/gifs/loading-animation.gif diff --git a/src/assets/navigationIcons/new-notifications.png b/src/assets/navigationIcons/new-notifications.png Binary files differnew file mode 100644 index 00000000..e8d7e70f --- /dev/null +++ b/src/assets/navigationIcons/new-notifications.png diff --git a/src/components/common/NavigationIcon.tsx b/src/components/common/NavigationIcon.tsx index 8fff18f4..4bf35360 100644 --- a/src/components/common/NavigationIcon.tsx +++ b/src/components/common/NavigationIcon.tsx @@ -10,6 +10,7 @@ import { interface NavigationIconProps extends TouchableOpacityProps { tab: 'Home' | 'Search' | 'Upload' | 'Notifications' | 'Profile'; disabled?: boolean; + newIcon?: boolean; } const NavigationIcon = (props: NavigationIconProps) => { @@ -32,7 +33,9 @@ const NavigationIcon = (props: NavigationIconProps) => { break; case 'Notifications': imgSrc = props.disabled - ? require('../../assets/navigationIcons/notifications.png') + ? props.newIcon + ? require('../../assets/navigationIcons/new-notifications.png') + : require('../../assets/navigationIcons/notifications.png') : require('../../assets/navigationIcons/notifications-clicked.png'); break; case 'Profile': diff --git a/src/components/common/TaggLoadingIndicator.tsx b/src/components/common/TaggLoadingIndicator.tsx index cfb99e80..91c68622 100644 --- a/src/components/common/TaggLoadingIndicator.tsx +++ b/src/components/common/TaggLoadingIndicator.tsx @@ -1,27 +1,53 @@ import * as React from 'react'; -import {ActivityIndicator, StyleSheet, View} from 'react-native'; +import {Image, StyleSheet, View} from 'react-native'; +import {SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils'; -type TaggLoadingIndicatorProps = { - color: string; -}; -const TaggLoadingIndicator: React.FC<TaggLoadingIndicatorProps> = ({color}) => { +interface TaggLoadingIndicatorProps { + fullscreen: boolean; +} + +const TaggLoadingIndicator: React.FC<TaggLoadingIndicatorProps> = ({ + fullscreen = false, +}) => { return ( - <View style={[styles.container, styles.horizontal]}> - <ActivityIndicator size="large" color={color} /> + <View + style={[ + fullscreen ? styles.fullscreen : {}, + styles.container, + styles.horizontal, + ]}> + <Image + source={require('../../assets/gifs/loading-animation.gif')} + style={styles.icon} + /> </View> ); }; const styles = StyleSheet.create({ + fullscreen: { + zIndex: 999, + position: 'absolute', + height: SCREEN_HEIGHT, + width: SCREEN_WIDTH, + }, container: { flex: 1, justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'rgba(0,0,0,0.3)', }, horizontal: { flexDirection: 'row', justifyContent: 'space-around', padding: 10, }, + icon: { + alignSelf: 'center', + justifyContent: 'center', + width: '40%', + aspectRatio: 2, + }, }); export default TaggLoadingIndicator; diff --git a/src/components/profile/ProfileBody.tsx b/src/components/profile/ProfileBody.tsx index 64aec09c..6284ff59 100644 --- a/src/components/profile/ProfileBody.tsx +++ b/src/components/profile/ProfileBody.tsx @@ -162,10 +162,11 @@ const styles = StyleSheet.create({ fontWeight: '600', fontSize: 16.5, marginBottom: '1%', + marginTop: '-3%', }, biography: { fontSize: 16, - marginBottom: '0.5%', + marginBottom: '1.5%', }, website: { fontSize: 16, diff --git a/src/routes/Routes.tsx b/src/routes/Routes.tsx index 38a987f7..a14f1576 100644 --- a/src/routes/Routes.tsx +++ b/src/routes/Routes.tsx @@ -5,6 +5,8 @@ import {useSelector, useDispatch} from 'react-redux'; import {RootState} from '../store/rootReducer'; import {userLogin} from '../utils'; import SplashScreen from 'react-native-splash-screen'; +import messaging from '@react-native-firebase/messaging'; +import {updateNewNotificationReceived} from '../store/actions'; const Routes: React.FC = () => { const { @@ -24,7 +26,12 @@ const Routes: React.FC = () => { * SplashScreen is the actual react-native's splash screen. * We can hide / show it depending on our application needs. */ + useEffect(() => { + messaging().onMessage(() => { + dispatch(updateNewNotificationReceived(true)); + }); + if (!userId) { userLogin(dispatch, {userId: '', username: ''}); } else { diff --git a/src/routes/tabs/NavigationBar.tsx b/src/routes/tabs/NavigationBar.tsx index 3757c56b..7d29ab67 100644 --- a/src/routes/tabs/NavigationBar.tsx +++ b/src/routes/tabs/NavigationBar.tsx @@ -1,15 +1,36 @@ import {createBottomTabNavigator} from '@react-navigation/bottom-tabs'; -import React, {Fragment} from 'react'; +import React, {Fragment, useEffect, useState} from 'react'; import {useSelector} from 'react-redux'; import {NavigationIcon} from '../../components'; +import {NO_NOTIFICATIONS} from '../../store/initialStates'; import {RootState} from '../../store/rootReducer'; import {ScreenType} from '../../types'; +import {haveUnreadNotifications} from '../../utils'; import MainStackScreen from '../main/MainStackScreen'; const Tabs = createBottomTabNavigator(); const NavigationBar: React.FC = () => { - const {isOnboardedUser} = useSelector((state: RootState) => state.user); + const {isOnboardedUser, newNotificationReceived} = useSelector( + (state: RootState) => state.user, + ); + + const {notifications: {notifications} = NO_NOTIFICATIONS} = useSelector( + (state: RootState) => state, + ); + + const [unreadNotificationsPresent, setUnreadNotificationsPresent] = useState< + boolean + >(false); + + useEffect(() => { + const determine = async () => { + setUnreadNotificationsPresent( + await haveUnreadNotifications(notifications), + ); + }; + determine(); + }, [notifications]); return ( <Tabs.Navigator @@ -23,7 +44,15 @@ const NavigationBar: React.FC = () => { case 'Upload': return <NavigationIcon tab="Upload" disabled={!focused} />; case 'Notifications': - return <NavigationIcon tab="Notifications" disabled={!focused} />; + return ( + <NavigationIcon + newIcon={ + newNotificationReceived || unreadNotificationsPresent + } + tab="Notifications" + disabled={!focused} + /> + ); case 'Profile': return <NavigationIcon tab="Profile" disabled={!focused} />; default: @@ -44,14 +73,14 @@ const NavigationBar: React.FC = () => { }, }}> <Tabs.Screen - name="Notifications" + name="Search" component={MainStackScreen} - initialParams={{screenType: ScreenType.Notifications}} + initialParams={{screenType: ScreenType.Search}} /> <Tabs.Screen - name="Search" + name="Notifications" component={MainStackScreen} - initialParams={{screenType: ScreenType.Search}} + initialParams={{screenType: ScreenType.Notifications}} /> <Tabs.Screen name="Profile" diff --git a/src/screens/main/NotificationsScreen.tsx b/src/screens/main/NotificationsScreen.tsx index 219a0be9..4bdee942 100644 --- a/src/screens/main/NotificationsScreen.tsx +++ b/src/screens/main/NotificationsScreen.tsx @@ -1,4 +1,5 @@ import AsyncStorage from '@react-native-community/async-storage'; +import {useFocusEffect} from '@react-navigation/native'; import moment from 'moment'; import React, {useCallback, useEffect, useState} from 'react'; import { @@ -11,16 +12,21 @@ import { import {SafeAreaView} from 'react-native-safe-area-context'; import {useDispatch, useSelector} from 'react-redux'; import {Notification} from '../../components/notifications'; -import {loadUserNotifications} from '../../store/actions'; +import { + loadUserNotifications, + updateNewNotificationReceived, +} from '../../store/actions'; import {RootState} from '../../store/rootReducer'; import {NotificationType, ScreenType} from '../../types'; import {getDateAge, SCREEN_HEIGHT} from '../../utils'; const NotificationsScreen: React.FC = () => { - const {user: loggedInUser} = useSelector((state: RootState) => state.user); const {moments: loggedInUserMoments} = useSelector( (state: RootState) => state.moments, ); + const {newNotificationReceived} = 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>( @@ -35,7 +41,7 @@ const NotificationsScreen: React.FC = () => { const dispatch = useDispatch(); - const onRefresh = useCallback(() => { + const refreshNotifications = () => { const refrestState = async () => { dispatch(loadUserNotifications()); }; @@ -43,7 +49,29 @@ const NotificationsScreen: React.FC = () => { refrestState().then(() => { setRefreshing(false); }); - }, [dispatch]); + }; + + const onRefresh = useCallback(() => { + refreshNotifications(); + }, [refreshNotifications]); + + useFocusEffect( + useCallback(() => { + const resetNewNotificationFlag = () => { + if (newNotificationReceived) { + dispatch(updateNewNotificationReceived(false)); + } + }; + + //Called everytime screen is focused + if (newNotificationReceived) { + refreshNotifications(); + } + + //Called when user leaves the screen + return () => resetNewNotificationFlag(); + }, [newNotificationReceived]), + ); // handles storing and fetching the "previously viewed" information useEffect(() => { diff --git a/src/screens/profile/CaptionScreen.tsx b/src/screens/profile/CaptionScreen.tsx index bc85d338..91aaa617 100644 --- a/src/screens/profile/CaptionScreen.tsx +++ b/src/screens/profile/CaptionScreen.tsx @@ -1,7 +1,8 @@ import {RouteProp} from '@react-navigation/native'; import {StackNavigationProp} from '@react-navigation/stack'; -import React from 'react'; +import React, {Fragment, useState} from 'react'; import { + Alert, Image, Keyboard, KeyboardAvoidingView, @@ -15,6 +16,8 @@ import {useDispatch, useSelector} from 'react-redux'; import {MainStackParams} from 'src/routes'; import {SearchBackground, TaggBigInput} from '../../components'; import {CaptionScreenHeader} from '../../components/'; +import TaggLoadingIndicator from '../../components/common/TaggLoadingIndicator'; +import {ERROR_UPLOAD, SUCCESS_PIC_UPLOAD} from '../../constants/strings'; import {postMoment} from '../../services'; import { loadUserMoments, @@ -42,7 +45,8 @@ const CaptionScreen: React.FC<CaptionScreenProps> = ({route, navigation}) => { user: {userId}, } = useSelector((state: RootState) => state.user); const dispatch = useDispatch(); - const [caption, setCaption] = React.useState(''); + const [caption, setCaption] = useState(''); + const [loading, setLoading] = useState(false); const handleCaptionUpdate = (caption: string) => { setCaption(caption); @@ -57,26 +61,29 @@ const CaptionScreen: React.FC<CaptionScreenProps> = ({route, navigation}) => { }; const handleShare = async () => { - try { - const data = await postMoment( - image.filename, - image.path, - caption, - title, - userId, - ); - if (data) { - dispatch(loadUserMoments(userId)); - dispatch(updateProfileCompletionStage(data)); - navigateToProfile(); - } - } catch (err) { - console.log(err); - } + setLoading(true); + postMoment(image.filename, image.path, caption, title, userId).then( + (data) => { + setLoading(false); + if (data) { + dispatch(loadUserMoments(userId)); + dispatch(updateProfileCompletionStage(data)); + navigateToProfile(); + setTimeout(() => { + Alert.alert(SUCCESS_PIC_UPLOAD); + }, 500); + } else { + setTimeout(() => { + Alert.alert(ERROR_UPLOAD); + }, 500); + } + }, + ); }; return ( <SearchBackground> + {loading ? <TaggLoadingIndicator fullscreen /> : <Fragment />} <TouchableWithoutFeedback onPress={Keyboard.dismiss}> <KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} diff --git a/src/screens/profile/EditProfile.tsx b/src/screens/profile/EditProfile.tsx index 3fea14bf..3b3fa36e 100644 --- a/src/screens/profile/EditProfile.tsx +++ b/src/screens/profile/EditProfile.tsx @@ -1,6 +1,5 @@ -import React, {useCallback, useEffect, useState} from 'react'; +import React, {Fragment, useCallback, useEffect, useState} from 'react'; import {RouteProp} from '@react-navigation/native'; -import moment from 'moment'; import {StackNavigationProp} from '@react-navigation/stack'; import { Text, @@ -21,7 +20,6 @@ import { TaggBigInput, TaggInput, TaggDropDown, - BirthDatePicker, TabsGradient, SocialIcon, } from '../../components'; @@ -46,6 +44,7 @@ import { ERROR_UPLOAD_LARGE_PROFILE_PIC, ERROR_UPLOAD_SMALL_PROFILE_PIC, } from '../../constants/strings'; +import TaggLoadingIndicator from '../../components/common/TaggLoadingIndicator'; type EditProfileNavigationProp = StackNavigationProp< ProfileStackParams, @@ -72,6 +71,7 @@ const EditProfile: React.FC<EditProfileProps> = ({route, navigation}) => { } = useSelector((state: RootState) => state.user); const [needsUpdate, setNeedsUpdate] = useState(false); + const [loading, setLoading] = useState(false); const dispatch = useDispatch(); useEffect(() => { @@ -379,7 +379,10 @@ const EditProfile: React.FC<EditProfileProps> = ({route, navigation}) => { title={'Save'} buttonStyle={{backgroundColor: 'transparent'}} titleStyle={{fontWeight: 'bold'}} - onPress={handleSubmit} + onPress={() => { + setLoading(true); + handleSubmit().then(() => setLoading(false)); + }} /> ), }); @@ -387,6 +390,7 @@ const EditProfile: React.FC<EditProfileProps> = ({route, navigation}) => { return ( <Background centered gradientType={BackgroundGradientType.Light}> + {loading ? <TaggLoadingIndicator fullscreen /> : <Fragment />} <SafeAreaView> <KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'}> @@ -444,9 +448,7 @@ const EditProfile: React.FC<EditProfileProps> = ({route, navigation}) => { blurOnSubmit={false} valid={form.isValidBio} attemptedSubmit={form.attemptedSubmit} - invalidWarning={ - 'Bio must be less than 150 characters' - } + invalidWarning={'Bio must be less than 150 characters'} width={280} value={form.bio} /> @@ -477,7 +479,6 @@ const EditProfile: React.FC<EditProfileProps> = ({route, navigation}) => { 'Custom field can only contain letters and hyphens' } onChangeText={handleCustomGenderUpdate} - onSubmitEditing={() => handleSubmit()} placeholder="Enter your gender" returnKeyType="done" style={styles.customGenderInput} diff --git a/src/screens/profile/SocialMediaTaggs.tsx b/src/screens/profile/SocialMediaTaggs.tsx index 81d271d1..1b6bb389 100644 --- a/src/screens/profile/SocialMediaTaggs.tsx +++ b/src/screens/profile/SocialMediaTaggs.tsx @@ -1,13 +1,7 @@ import {RouteProp} from '@react-navigation/native'; import {StackNavigationProp} from '@react-navigation/stack'; import React, {useEffect, useState} from 'react'; -import { - ActivityIndicator, - ScrollView, - StatusBar, - StyleSheet, - View, -} from 'react-native'; +import {ScrollView, StatusBar, StyleSheet, View} from 'react-native'; import LinearGradient from 'react-native-linear-gradient'; import { AvatarTitle, @@ -16,7 +10,7 @@ import { TaggPost, TwitterTaggPost, } from '../../components'; -import {AVATAR_GRADIENT, TAGG_DARK_BLUE} from '../../constants'; +import {AVATAR_GRADIENT} from '../../constants'; import {ProfileStackParams} from '../../routes'; import {SimplePostType, TwitterPostType, SocialAccountType} from '../../types'; import {AvatarHeaderHeight, SCREEN_HEIGHT} from '../../utils'; diff --git a/src/services/MomentServices.ts b/src/services/MomentServices.ts index 514b674c..735f2ed2 100644 --- a/src/services/MomentServices.ts +++ b/src/services/MomentServices.ts @@ -6,11 +6,7 @@ import { MOMENTS_ENDPOINT, MOMENT_THUMBNAIL_ENDPOINT, } from '../constants'; -import { - ERROR_FAILED_TO_COMMENT, - ERROR_UPLOAD, - SUCCESS_PIC_UPLOAD, -} from '../constants/strings'; +import {ERROR_FAILED_TO_COMMENT} from '../constants/strings'; import {MomentType} from '../types'; import {checkImageUploadStatus} from '../utils'; @@ -139,14 +135,10 @@ export const postMoment: ( let statusCode = response.status; let data = await response.json(); if (statusCode === 200 && checkImageUploadStatus(data.moments)) { - Alert.alert(SUCCESS_PIC_UPLOAD); return data.profile_completion_stage; - } else { - Alert.alert(ERROR_UPLOAD); } } catch (err) { console.log(err); - Alert.alert(ERROR_UPLOAD); } return undefined; }; diff --git a/src/store/actions/user.ts b/src/store/actions/user.ts index 8550f3bd..0b1ea789 100644 --- a/src/store/actions/user.ts +++ b/src/store/actions/user.ts @@ -8,6 +8,7 @@ import { socialEdited, profileCompletionStageUpdated, setIsOnboardedUser, + setNewNotificationReceived, } from '../reducers'; import {getTokenOrLogout} from '../../utils'; @@ -95,6 +96,21 @@ export const updateIsOnboardedUser = ( } }; +export const updateNewNotificationReceived = ( + newNotificationReceived: boolean, +): ThunkAction<Promise<void>, RootState, unknown, Action<string>> => async ( + dispatch, +) => { + try { + dispatch({ + type: setNewNotificationReceived.type, + payload: {newNotificationReceived}, + }); + } 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 08dc7077..2a5b76db 100644 --- a/src/store/initialStates.ts +++ b/src/store/initialStates.ts @@ -17,7 +17,9 @@ export const NO_PROFILE: ProfileType = { gender: '', birthday: undefined, university_class: 2021, - profile_completion_stage: 1, + + //Default to an invalid value and ignore it gracefully while showing tutorials / popups. + profile_completion_stage: -1, snapchat: '', tiktok: '', friendship_status: 'no_record', @@ -41,6 +43,7 @@ export const NO_USER_DATA = { avatar: <string | null>'', cover: <string | null>'', isOnboardedUser: false, + newNotificationReceived: false, }; export const NO_FRIENDS_DATA = { diff --git a/src/store/reducers/userReducer.ts b/src/store/reducers/userReducer.ts index 2e71e38e..ce497677 100644 --- a/src/store/reducers/userReducer.ts +++ b/src/store/reducers/userReducer.ts @@ -49,6 +49,10 @@ const userDataSlice = createSlice({ setIsOnboardedUser: (state, action) => { state.isOnboardedUser = action.payload.isOnboardedUser; }, + + setNewNotificationReceived: (state, action) => { + state.newNotificationReceived = action.payload.newNotificationReceived; + }, }, }); @@ -58,5 +62,6 @@ export const { socialEdited, profileCompletionStageUpdated, setIsOnboardedUser, + setNewNotificationReceived, } = userDataSlice.actions; export const userDataReducer = userDataSlice.reducer; diff --git a/src/utils/common.ts b/src/utils/common.ts index 6314cc13..8efe1f6a 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -1,6 +1,8 @@ +import {NotificationType} from './../types/types'; import moment from 'moment'; -import {AsyncStorage, Linking} from 'react-native'; +import {Linking} from 'react-native'; import {BROWSABLE_SOCIAL_URLS, TOGGLE_BUTTON_TYPE} from '../constants'; +import AsyncStorage from '@react-native-community/async-storage'; export const getToggleButtonText: ( buttonType: string, @@ -72,3 +74,21 @@ export const checkImageUploadStatus = (statusMap: object) => { } return true; }; + +export const haveUnreadNotifications = async ( + notifications: NotificationType[], +): Promise<boolean> => { + for (const n of notifications) { + const notificationDate = moment(n.timestamp); + const prevLastViewed = await AsyncStorage.getItem('notificationLastViewed'); + const lastViewed: moment.Moment | undefined = + prevLastViewed == null ? moment.unix(0) : moment(prevLastViewed); + const dateAge = getDateAge(notificationDate); + if (dateAge === 'unknown') { + continue; + } + const unread = lastViewed ? lastViewed.diff(notificationDate) < 0 : false; + if (unread) return true; + } + return false; +}; |