diff options
author | Ivan Chen <ivan@thetaggid.com> | 2021-01-20 15:52:37 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-01-20 15:52:37 -0500 |
commit | 7b45e8d238f392183f3c1742f22495a2f9c6fb7f (patch) | |
tree | bab6621bf916e9efa0ce863cf49b649bd6ba6548 /src | |
parent | a64a0c53f108f8ea9c9ac0ba67c34ff82007271e (diff) | |
parent | 623bc69013c8fd173e49fb059eaba23c9219e0eb (diff) |
Merge pull request #192 from ashmgarv/tma515-new-no-icon
[TMA - 515] New notifications icon and reordering of bottom navigation stack
Diffstat (limited to 'src')
-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/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/store/actions/user.ts | 16 | ||||
-rw-r--r-- | src/store/initialStates.ts | 1 | ||||
-rw-r--r-- | src/store/reducers/userReducer.ts | 5 | ||||
-rw-r--r-- | src/utils/common.ts | 22 |
9 files changed, 122 insertions, 13 deletions
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/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/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 4da53c2e..2a5b76db 100644 --- a/src/store/initialStates.ts +++ b/src/store/initialStates.ts @@ -43,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; +}; |