From bd2f89805d0bb1c2f1d08fe8d91099aa4f109d35 Mon Sep 17 00:00:00 2001 From: Ivan Chen Date: Tue, 29 Dec 2020 20:21:24 -0500 Subject: [TMA-461] Notifications Screen (#151) * renamed ProfileStack to MainStack, created initial notifications data type * cleaned up code * added notifications to redux * finished sectioned list * updated types to make more sense * finished sectioned notifications by date * updated notification type and tested mock backend integration * finished read or unread logic * minor changes * another minor fix * finished integration * moved stuff * added ability to navigate to user profile Co-authored-by: Husam Salhab <47015061+hsalhab@users.noreply.github.com> --- src/components/notifications/Notification.tsx | 150 ++++++++++++++++++++ src/components/notifications/index.ts | 1 + src/components/profile/ProfilePreview.tsx | 35 ++--- src/constants/api.ts | 4 +- src/routes/index.ts | 2 +- src/routes/main/MainStackNavigator.tsx | 53 +++++++ src/routes/main/MainStackScreen.tsx | 182 +++++++++++++++++++++++++ src/routes/main/index.ts | 2 + src/routes/profile/ProfileStackNavigator.tsx | 50 ------- src/routes/profile/ProfileStackScreen.tsx | 166 ---------------------- src/routes/profile/index.ts | 2 - src/routes/tabs/NavigationBar.tsx | 15 +- src/screens/main/Notifications.tsx | 13 -- src/screens/main/NotificationsScreen.tsx | 167 +++++++++++++++++++++++ src/screens/main/index.ts | 2 +- src/screens/profile/IndividualMoment.tsx | 2 +- src/screens/profile/MomentCommentsScreen.tsx | 2 +- src/services/NotificationService.ts | 32 +++++ src/services/UserProfileService.ts | 13 +- src/services/index.ts | 1 + src/store/actions/index.ts | 1 + src/store/actions/notifications.ts | 21 +++ src/store/actions/user.ts | 2 +- src/store/actions/userX.ts | 2 +- src/store/initialStates.ts | 17 ++- src/store/reducers/index.ts | 1 + src/store/reducers/userNotificationsReducer.ts | 15 ++ src/store/rootReducer.ts | 2 + src/types/types.ts | 11 ++ src/utils/common.ts | 19 +++ src/utils/users.ts | 14 +- 31 files changed, 714 insertions(+), 285 deletions(-) create mode 100644 src/components/notifications/Notification.tsx create mode 100644 src/components/notifications/index.ts create mode 100644 src/routes/main/MainStackNavigator.tsx create mode 100644 src/routes/main/MainStackScreen.tsx create mode 100644 src/routes/main/index.ts delete mode 100644 src/routes/profile/ProfileStackNavigator.tsx delete mode 100644 src/routes/profile/ProfileStackScreen.tsx delete mode 100644 src/routes/profile/index.ts delete mode 100644 src/screens/main/Notifications.tsx create mode 100644 src/screens/main/NotificationsScreen.tsx create mode 100644 src/services/NotificationService.ts create mode 100644 src/store/actions/notifications.ts create mode 100644 src/store/reducers/userNotificationsReducer.ts (limited to 'src') diff --git a/src/components/notifications/Notification.tsx b/src/components/notifications/Notification.tsx new file mode 100644 index 00000000..f533e42d --- /dev/null +++ b/src/components/notifications/Notification.tsx @@ -0,0 +1,150 @@ +import {useNavigation} from '@react-navigation/native'; +import React, {useEffect, useState} from 'react'; +import {Image, StyleSheet, Text, View} from 'react-native'; +import {TouchableWithoutFeedback} from 'react-native-gesture-handler'; +import {useDispatch, useStore} from 'react-redux'; +import {loadAvatar} from '../../services'; +import {RootState} from '../../store/rootReducer'; +import {NotificationType, ScreenType} from '../../types'; +import {fetchUserX, SCREEN_HEIGHT, userXInStore} from '../../utils'; + +interface NotificationProps { + item: NotificationType; + screenType: ScreenType; +} + +const Notification: React.FC = (props) => { + const { + item: { + actor: {id, username, first_name, last_name}, + verbage, + notification_type, + notification_object, + unread, + }, + screenType, + } = props; + const navigation = useNavigation(); + const state: RootState = useStore().getState(); + const dispatch = useDispatch(); + + const [avatarURI, setAvatarURI] = useState(undefined); + const [momentURI, setMomentURI] = useState(undefined); + const backgroundColor = unread ? '#DCF1F1' : 'rgba(0,0,0,0)'; + + useEffect(() => { + let mounted = true; + const loadAvatarImage = async (user_id: string) => { + const response = await loadAvatar(user_id, true); + if (mounted) { + setAvatarURI(response); + } + }; + loadAvatarImage(id); + return () => { + mounted = false; + }; + }, [id]); + + // TODO: this should be moment thumbnail, waiting for that to complete + // useEffect(() => { + // let mounted = true; + // const loadMomentImage = async (user_id: string) => { + // const response = await loadAvatar(user_id, true); + // if (mounted) { + // setMomentURI(response); + // } + // }; + // loadMomentImage(id); + // return () => { + // mounted = false; + // }; + // }, [id, notification_object]); + + const onNotificationTap = async () => { + switch (notification_type) { + case 'FLO': + if (!userXInStore(state, screenType, id)) { + await fetchUserX( + dispatch, + {userId: id, username: username}, + screenType, + ); + } + navigation.push('Profile', { + userXId: id, + screenType, + }); + break; + default: + break; + } + }; + + return ( + + + + + + + {first_name} {last_name} + + {verbage} + + {/* TODO: Still WIP */} + {/* {notification_type === 'CMT' && notification_object && ( + + )} */} + + ); +}; + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + height: SCREEN_HEIGHT / 12, + flex: 1, + alignItems: 'center', + }, + avatarContainer: { + marginLeft: '5%', + flex: 1, + justifyContent: 'center', + }, + avatar: { + height: 42, + width: 42, + borderRadius: 20, + }, + contentContainer: { + flex: 5, + marginLeft: '5%', + height: '80%', + flexDirection: 'column', + justifyContent: 'space-around', + }, + actorName: { + fontWeight: 'bold', + }, + moment: { + position: 'absolute', + height: 42, + width: 42, + right: '5%', + }, +}); + +export default Notification; diff --git a/src/components/notifications/index.ts b/src/components/notifications/index.ts new file mode 100644 index 00000000..0260ce24 --- /dev/null +++ b/src/components/notifications/index.ts @@ -0,0 +1 @@ +export {default as Notification} from './Notification'; diff --git a/src/components/profile/ProfilePreview.tsx b/src/components/profile/ProfilePreview.tsx index cc18e457..49c79e2d 100644 --- a/src/components/profile/ProfilePreview.tsx +++ b/src/components/profile/ProfilePreview.tsx @@ -14,12 +14,14 @@ import RNFetchBlob from 'rn-fetch-blob'; import AsyncStorage from '@react-native-community/async-storage'; import {PROFILE_PHOTO_THUMBNAIL_ENDPOINT} from '../../constants'; import {UserType, PreviewType} from '../../types'; -import {isUserBlocked} from '../../services'; +import {isUserBlocked, loadAvatar} from '../../services'; import {useSelector, useDispatch, useStore} from 'react-redux'; import {RootState} from '../../store/rootreducer'; import {logout} from '../../store/actions'; import {fetchUserX, userXInStore} from '../../utils'; +import {SearchResultsBackground} from '../search'; import NavigationBar from 'src/routes/tabs'; + const NO_USER: UserType = { userId: '', username: '', @@ -52,34 +54,13 @@ const ProfilePreview: React.FC = ({ const dispatch = useDispatch(); useEffect(() => { let mounted = true; - const loadAvatar = async () => { - try { - const token = await AsyncStorage.getItem('token'); - if (!token) { - setUser(NO_USER); - return; - } - const response = await RNFetchBlob.config({ - fileCache: true, - appendExt: 'jpg', - }).fetch('GET', PROFILE_PHOTO_THUMBNAIL_ENDPOINT + `${id}/`, { - Authorization: 'Token ' + token, - }); - const status = response.info().status; - if (status === 200) { - if (mounted) { - setAvatarURI(response.path()); - } - return; - } - if (mounted) { - setAvatarURI(''); - } - } catch (error) { - console.log(error); + const loadAvatarImage = async () => { + const response = await loadAvatar(id, true); + if (mounted) { + setAvatarURI(response); } }; - loadAvatar(); + loadAvatarImage(); return () => { mounted = false; }; diff --git a/src/constants/api.ts b/src/constants/api.ts index e1658993..2118492d 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -11,7 +11,8 @@ export const VERIFY_OTP_ENDPOINT: string = API_URL + 'verify-otp/'; export const PROFILE_INFO_ENDPOINT: string = API_URL + 'user-profile-info/'; export const HEADER_PHOTO_ENDPOINT: string = API_URL + 'header-pic/'; export const PROFILE_PHOTO_ENDPOINT: string = API_URL + 'profile-pic/'; -export const PROFILE_PHOTO_THUMBNAIL_ENDPOINT: string = API_URL + 'profile-thumbnail/'; +export const PROFILE_PHOTO_THUMBNAIL_ENDPOINT: string = + API_URL + 'profile-thumbnail/'; export const GET_IG_POSTS_ENDPOINT: string = API_URL + 'posts-ig/'; export const GET_FB_POSTS_ENDPOINT: string = API_URL + 'posts-fb/'; export const GET_TWITTER_POSTS_ENDPOINT: string = API_URL + 'posts-twitter/'; @@ -28,6 +29,7 @@ export const REPORT_ISSUE_ENDPOINT: string = API_URL + 'report/'; export const BLOCK_USER_ENDPOINT: string = API_URL + 'block/'; export const PASSWORD_RESET_ENDPOINT: string = API_URL + 'password-reset/'; export const MOMENT_CATEGORY_ENDPOINT: string = API_URL + 'moment-category/'; +export const NOTIFICATIONS_ENDPOINT: string = API_URL + 'notifications/'; // Register as FCM device export const FCM_ENDPOINT: string = API_URL + 'fcm/'; diff --git a/src/routes/index.ts b/src/routes/index.ts index 3b74e130..ed61d92f 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,3 +1,3 @@ export * from './onboarding'; -export * from './profile'; +export * from './main'; export {default} from './Routes'; diff --git a/src/routes/main/MainStackNavigator.tsx b/src/routes/main/MainStackNavigator.tsx new file mode 100644 index 00000000..c156c725 --- /dev/null +++ b/src/routes/main/MainStackNavigator.tsx @@ -0,0 +1,53 @@ +/** + * Note the name userXId here, it refers to the id of the user being visited + */ +import {createStackNavigator} from '@react-navigation/stack'; +import {CategorySelectionScreenType, MomentType, ScreenType} from '../../types'; + +export type MainStackParams = { + Search: { + screenType: ScreenType; + }; + Profile: { + userXId: string | undefined; + screenType: ScreenType; + }; + SocialMediaTaggs: { + socialMediaType: string; + userXId: string | undefined; + screenType: ScreenType; + }; + CaptionScreen: { + title: string; + image: object; + screenType: ScreenType; + }; + IndividualMoment: { + moment: MomentType; + userXId: string | undefined; + screenType: ScreenType; + }; + MomentCommentsScreen: { + moment_id: string; + userXId: string | undefined; + screenType: ScreenType; + }; + FollowersListScreen: { + isFollowers: boolean; + userXId: string | undefined; + screenType: ScreenType; + }; + EditProfile: { + userId: string; + username: string; + }; + CategorySelection: { + categories: Array; + screenType: CategorySelectionScreenType; + }; + Notifications: { + screenType: ScreenType; + }; +}; + +export const MainStack = createStackNavigator(); diff --git a/src/routes/main/MainStackScreen.tsx b/src/routes/main/MainStackScreen.tsx new file mode 100644 index 00000000..cd053bde --- /dev/null +++ b/src/routes/main/MainStackScreen.tsx @@ -0,0 +1,182 @@ +import React from 'react'; +import { + IndividualMoment, + CaptionScreen, + SocialMediaTaggs, + SearchScreen, + ProfileScreen, + MomentCommentsScreen, + FollowersListScreen, + EditProfile, + CategorySelection, + NotificationsScreen, +} from '../../screens'; +import {MainStack, MainStackParams} from './MainStackNavigator'; +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) + * Profile : To display the logged in user's profile when the userXId passed in to it is (undefined | null | empty string) else displays profile of the user being visited. + * Search : To display the search screen. Search for a user on this screen, click on a result tile and navigate to the same. + * When you click on the search icon after looking at a user's profile, the stack gets reset and you come back to the top of the stack (First screen : Search in this case) + * SocialMediaTaggs : To display user data for any social media account set up by the user. + * IndividualMoment : To display individual images uploaded by the user (Navigate to comments from this screen, click on a commenter's profile pic / username, look at a user's profile. Click on the profile icon again to come back to your own profile). + * MomentCommentsScreen : Displays comments posted by users on an image uploaded by the user. + * EditProfile : To edit logged in user's information. + */ + +type MainStackRouteProps = RouteProp; + +interface MainStackProps { + route: MainStackRouteProps; +} + +const MainStackScreen: React.FC = ({route}) => { + const {screenType} = route.params; + + // const isProfileTab = screenType === ScreenType.Profile; + const isSearchTab = screenType === ScreenType.Search; + const isNotificationsTab = screenType === ScreenType.Notifications; + + const initialRouteName = (() => { + switch (screenType) { + case ScreenType.Profile: + return 'Profile'; + case ScreenType.Search: + return 'Search'; + case ScreenType.Notifications: + return 'Notifications'; + } + })(); + + const modalStyle: StackNavigationOptions = { + cardStyle: {backgroundColor: 'transparent'}, + gestureDirection: 'vertical', + cardOverlayEnabled: true, + cardStyleInterpolator: ({current: {progress}}) => ({ + cardStyle: { + opacity: progress.interpolate({ + inputRange: [0, 0.5, 0.9, 1], + outputRange: [0, 0.25, 0.7, 1], + }), + }, + overlayStyle: { + backgroundColor: '#505050', + opacity: progress.interpolate({ + inputRange: [0, 1], + outputRange: [0, 0.9], + extrapolate: 'clamp', + }), + }, + }), + }; + + return ( + + + {isSearchTab && ( + + )} + {isNotificationsTab && ( + + )} + + + + + + + + + ); +}; + +export default MainStackScreen; diff --git a/src/routes/main/index.ts b/src/routes/main/index.ts new file mode 100644 index 00000000..945c3fb0 --- /dev/null +++ b/src/routes/main/index.ts @@ -0,0 +1,2 @@ +export * from './MainStackNavigator'; +export * from './MainStackScreen'; diff --git a/src/routes/profile/ProfileStackNavigator.tsx b/src/routes/profile/ProfileStackNavigator.tsx deleted file mode 100644 index bc0a9560..00000000 --- a/src/routes/profile/ProfileStackNavigator.tsx +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Note the name userXId here, it refers to the id of the user being visited - */ -import {createStackNavigator} from '@react-navigation/stack'; -import {CategorySelectionScreenType, MomentType, ScreenType} from '../../types'; - -export type ProfileStackParams = { - Search: { - screenType: ScreenType; - }; - Profile: { - userXId: string | undefined; - screenType: ScreenType; - }; - SocialMediaTaggs: { - socialMediaType: string; - userXId: string | undefined; - screenType: ScreenType; - }; - CaptionScreen: { - title: string; - image: object; - screenType: ScreenType; - }; - IndividualMoment: { - moment: MomentType; - userXId: string | undefined; - screenType: ScreenType; - }; - MomentCommentsScreen: { - moment_id: string; - userXId: string | undefined; - screenType: ScreenType; - }; - FollowersListScreen: { - isFollowers: boolean; - userXId: string | undefined; - screenType: ScreenType; - }; - EditProfile: { - userId: string; - username: string; - }; - CategorySelection: { - categories: Array; - screenType: CategorySelectionScreenType; - }; -}; - -export const ProfileStack = createStackNavigator(); diff --git a/src/routes/profile/ProfileStackScreen.tsx b/src/routes/profile/ProfileStackScreen.tsx deleted file mode 100644 index 4fc9f0c7..00000000 --- a/src/routes/profile/ProfileStackScreen.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import React from 'react'; -import { - IndividualMoment, - CaptionScreen, - SocialMediaTaggs, - SearchScreen, - ProfileScreen, - MomentCommentsScreen, - FollowersListScreen, - EditProfile, - CategorySelection, -} from '../../screens'; -import {ProfileStack, ProfileStackParams} from './ProfileStackNavigator'; -import {RouteProp} from '@react-navigation/native'; -import {ScreenType} from '../../types'; -import {AvatarHeaderHeight} from '../../utils'; -import {StackNavigationOptions} from '@react-navigation/stack'; - -/** - * Trying to explain the purpose of each route on the stack (ACTUALLY A STACK) - * Profile : To display the logged in user's profile when the userXId passed in to it is (undefined | null | empty string) else displays profile of the user being visited. - * Search : To display the search screen. Search for a user on this screen, click on a result tile and navigate to the same. - * When you click on the search icon after looking at a user's profile, the stack gets reset and you come back to the top of the stack (First screen : Search in this case) - * SocialMediaTaggs : To display user data for any social media account set up by the user. - * IndividualMoment : To display individual images uploaded by the user (Navigate to comments from this screen, click on a commenter's profile pic / username, look at a user's profile. Click on the profile icon again to come back to your own profile). - * MomentCommentsScreen : Displays comments posted by users on an image uploaded by the user. - * EditProfile : To edit logged in user's information. - */ - -type ProfileStackRouteProps = RouteProp; - -interface ProfileStackProps { - route: ProfileStackRouteProps; -} - -const ProfileStackScreen: React.FC = ({route}) => { - const {screenType} = route.params; - - const isProfileStack = screenType === ScreenType.Profile; - - const modalStyle: StackNavigationOptions = { - cardStyle: {backgroundColor: 'transparent'}, - gestureDirection: 'vertical', - cardOverlayEnabled: true, - cardStyleInterpolator: ({current: {progress}}) => ({ - cardStyle: { - opacity: progress.interpolate({ - inputRange: [0, 0.5, 0.9, 1], - outputRange: [0, 0.25, 0.7, 1], - }), - }, - overlayStyle: { - backgroundColor: '#505050', - opacity: progress.interpolate({ - inputRange: [0, 1], - outputRange: [0, 0.9], - extrapolate: 'clamp', - }), - }, - }), - }; - - return ( - - - {!isProfileStack ? ( - - ) : ( - - )} - - - {isProfileStack ? ( - - ) : ( - - )} - - - - - - ); -}; - -export default ProfileStackScreen; diff --git a/src/routes/profile/index.ts b/src/routes/profile/index.ts deleted file mode 100644 index 05a6b24a..00000000 --- a/src/routes/profile/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './ProfileStackNavigator'; -export * from './ProfileStackScreen'; diff --git a/src/routes/tabs/NavigationBar.tsx b/src/routes/tabs/NavigationBar.tsx index f3043696..9d7d4b12 100644 --- a/src/routes/tabs/NavigationBar.tsx +++ b/src/routes/tabs/NavigationBar.tsx @@ -2,7 +2,7 @@ import {createBottomTabNavigator} from '@react-navigation/bottom-tabs'; import React, {Fragment} from 'react'; import {NavigationIcon} from '../../components'; import {ScreenType} from '../../types'; -import Profile from '../profile/ProfileStackScreen'; +import MainStackScreen from '../main/MainStackScreen'; const Tabs = createBottomTabNavigator(); @@ -39,18 +39,19 @@ const NavigationBar: React.FC = () => { bottom: '1%', }, }}> - {/* Removed for Alpha for now */} - {/* - - */} + diff --git a/src/screens/main/Notifications.tsx b/src/screens/main/Notifications.tsx deleted file mode 100644 index ca8c41c3..00000000 --- a/src/screens/main/Notifications.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react'; -import {ComingSoon} from '../../components'; - -/** - * Navigation Screen for displaying other users' - * actions on the logged in user's posts - */ - -const Notifications: React.FC = () => { - return ; -}; - -export default Notifications; diff --git a/src/screens/main/NotificationsScreen.tsx b/src/screens/main/NotificationsScreen.tsx new file mode 100644 index 00000000..2343215f --- /dev/null +++ b/src/screens/main/NotificationsScreen.tsx @@ -0,0 +1,167 @@ +import AsyncStorage from '@react-native-community/async-storage'; +import moment from 'moment'; +import React, {useCallback, useEffect, useState} from 'react'; +import { + RefreshControl, + SectionList, + StyleSheet, + Text, + View, +} from 'react-native'; +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 {RootState} from '../../store/rootReducer'; +import {NotificationType, ScreenType} from '../../types'; +import {getDateAge, SCREEN_HEIGHT} from '../../utils'; + +const NotificationsScreen: React.FC = () => { + const [refreshing, setRefreshing] = useState(false); + // used for figuring out which ones are unread + const [lastViewed, setLastViewed] = useState( + undefined, + ); + const {notifications} = useSelector( + (state: RootState) => state.notifications, + ); + const [sectionedNotifications, setSectionedNotifications] = useState< + {title: 'Today' | 'Yesterday' | 'This Week'; data: NotificationType[]}[] + >([]); + + const dispatch = useDispatch(); + + const onRefresh = useCallback(() => { + const refrestState = async () => { + dispatch(loadUserNotifications()); + }; + setRefreshing(true); + refrestState().then(() => { + setRefreshing(false); + }); + }, [dispatch]); + + // handles storing and fetching the "previously viewed" information + useEffect(() => { + const getAndUpdateLastViewed = async () => { + const key = 'notificationLastViewed'; + const previousLastViewed = await AsyncStorage.getItem(key); + setLastViewed( + previousLastViewed == null + ? moment.unix(0) + : moment(previousLastViewed), + ); + await AsyncStorage.setItem(key, moment().toString()); + }; + getAndUpdateLastViewed(); + }, [notifications]); + + // handles sectioning notifications to "date age" + // mark notifications as read or unread + useEffect(() => { + const sortedNotifications = (notifications ?? []) + .slice() + .sort((a, b) => (a.timestamp < b.timestamp ? 1 : -1)); + let todays = []; + let yesterdays = []; + let thisWeeks = []; + for (const n of sortedNotifications) { + const notificationDate = moment(n.timestamp); + const dateAge = getDateAge(notificationDate); + if (dateAge === 'unknown') { + continue; + } + const unread = lastViewed ? lastViewed.diff(notificationDate) < 0 : false; + const newN = {...n, unread}; + switch (dateAge) { + case 'today': + todays.push(newN); + continue; + case 'yesterday': + yesterdays.push(newN); + continue; + case 'thisWeek': + thisWeeks.push(newN); + continue; + default: + continue; + } + } + setSectionedNotifications([ + { + title: 'Today', + data: todays, + }, + { + title: 'Yesterday', + data: yesterdays, + }, + { + title: 'This Week', + data: thisWeeks, + }, + ]); + }, [lastViewed, notifications]); + + const renderNotification = ({item}: {item: NotificationType}) => ( + + ); + + const renderSectionHeader = ({section: {title}}) => ( + + {title} + + ); + + return ( + + + Notifications + + + index.toString()} + renderItem={renderNotification} + renderSectionHeader={renderSectionHeader} + refreshControl={ + + } + /> + + ); +}; + +const styles = StyleSheet.create({ + header: { + marginLeft: '5%', + marginTop: '5%', + alignSelf: 'flex-start', + flexDirection: 'column', + }, + headerText: { + fontWeight: 'bold', + fontSize: 16, + }, + underline: { + borderWidth: 2, + borderColor: '#8F01FF', + }, + container: { + paddingBottom: '20%', + minHeight: (SCREEN_HEIGHT * 8) / 10, + }, + sectionHeaderContainer: { + width: '100%', + backgroundColor: '#f3f2f2', + }, + sectionHeader: { + marginLeft: '5%', + marginTop: '5%', + marginBottom: '2%', + fontSize: 16, + }, +}); + +export default NotificationsScreen; diff --git a/src/screens/main/index.ts b/src/screens/main/index.ts index b15f76da..a5a723d7 100644 --- a/src/screens/main/index.ts +++ b/src/screens/main/index.ts @@ -1,3 +1,3 @@ export {default as Home} from './Home'; -export {default as Notifications} from './Notifications'; +export {default as NotificationsScreen} from './NotificationsScreen'; export {default as Upload} from './Upload'; diff --git a/src/screens/profile/IndividualMoment.tsx b/src/screens/profile/IndividualMoment.tsx index 469c648e..91742324 100644 --- a/src/screens/profile/IndividualMoment.tsx +++ b/src/screens/profile/IndividualMoment.tsx @@ -4,7 +4,7 @@ import {StackNavigationProp} from '@react-navigation/stack'; import React from 'react'; import {FlatList, StyleSheet, View} from 'react-native'; import {useSelector} from 'react-redux'; -import {ProfileStackParams} from 'src/routes/profile/ProfileStack'; +import {ProfileStackParams} from 'src/routes/main/ProfileStack'; import { IndividualMomentTitleBar, MomentPostContent, diff --git a/src/screens/profile/MomentCommentsScreen.tsx b/src/screens/profile/MomentCommentsScreen.tsx index 2cc809a3..ebe4da28 100644 --- a/src/screens/profile/MomentCommentsScreen.tsx +++ b/src/screens/profile/MomentCommentsScreen.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import {RouteProp, useNavigation} from '@react-navigation/native'; -import {ProfileStackParams} from '../../routes/profile'; +import {ProfileStackParams} from '../../routes/main'; import {CenteredView, CommentTile} from '../../components'; import {CommentType} from '../../types'; import {ScrollView, StyleSheet, Text, View} from 'react-native'; diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts new file mode 100644 index 00000000..a62b0df9 --- /dev/null +++ b/src/services/NotificationService.ts @@ -0,0 +1,32 @@ +import AsyncStorage from '@react-native-community/async-storage'; +import {NOTIFICATIONS_ENDPOINT} from '../constants'; +import {NotificationType} from '../types'; + +export const getNotificationsData: () => Promise< + NotificationType[] +> = async () => { + try { + const token = await AsyncStorage.getItem('token'); + const response = await fetch(NOTIFICATIONS_ENDPOINT, { + method: 'GET', + headers: { + Authorization: 'Token ' + token, + }, + }); + if (response.status === 200) { + const data: any[] = await response.json(); + let typedData: NotificationType[] = []; + for (const o of data) { + typedData.push({ + ...o.notification, + unread: false, + }); + } + return typedData; + } + return []; + } catch (error) { + console.log('Unable to fetch notifications'); + return []; + } +}; diff --git a/src/services/UserProfileService.ts b/src/services/UserProfileService.ts index c67174f9..8c88f0ab 100644 --- a/src/services/UserProfileService.ts +++ b/src/services/UserProfileService.ts @@ -1,3 +1,4 @@ +import {transformFromAstAsync} from '@babel/core'; import AsyncStorage from '@react-native-community/async-storage'; import moment from 'moment'; import {Alert} from 'react-native'; @@ -14,6 +15,7 @@ import { TAGG_CUSTOMER_SUPPORT, VERIFY_OTP_ENDPOINT, SEND_OTP_ENDPOINT, + PROFILE_PHOTO_THUMBNAIL_ENDPOINT, } from '../constants'; export const loadProfileInfo = async (token: string, userId: string) => { @@ -41,12 +43,16 @@ export const loadProfileInfo = async (token: string, userId: string) => { } }; -export const loadAvatar = async (token: string, userId: string) => { +export const loadAvatar = async (userId: string, thumbnail: boolean) => { try { + const token = await AsyncStorage.getItem('token'); + const url = thumbnail + ? PROFILE_PHOTO_THUMBNAIL_ENDPOINT + : PROFILE_PHOTO_ENDPOINT; const response = await RNFetchBlob.config({ fileCache: true, appendExt: 'jpg', - }).fetch('GET', PROFILE_PHOTO_ENDPOINT + `${userId}/`, { + }).fetch('GET', url + `${userId}/`, { Authorization: 'Token ' + token, }); const status = response.info().status; @@ -57,6 +63,7 @@ export const loadAvatar = async (token: string, userId: string) => { } } catch (error) { console.log(error); + return ''; } }; @@ -209,7 +216,7 @@ export const handlePasswordCodeVerification = async ( export const handlePasswordReset = async (value: string, password: string) => { try { const token = await AsyncStorage.getItem('token'); - const response = await fetch(PASSWORD_RESET_ENDPOINT + `reset/`, { + const response = await fetch(PASSWORD_RESET_ENDPOINT + 'reset/', { method: 'POST', headers: { Authorization: 'Token ' + token, diff --git a/src/services/index.ts b/src/services/index.ts index 81a09b92..7e6b836a 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -6,4 +6,5 @@ export * from './UserFollowServices'; export * from './ReportingService'; export * from './BlockUserService'; export * from './MomentCategoryService'; +export * from './NotificationService'; export * from './FCMService'; diff --git a/src/store/actions/index.ts b/src/store/actions/index.ts index f9fd5e9c..285ca4de 100644 --- a/src/store/actions/index.ts +++ b/src/store/actions/index.ts @@ -6,3 +6,4 @@ export * from './socials'; export * from './taggUsers'; export * from './userBlock'; export * from './userX'; +export * from './notifications'; diff --git a/src/store/actions/notifications.ts b/src/store/actions/notifications.ts new file mode 100644 index 00000000..bace1776 --- /dev/null +++ b/src/store/actions/notifications.ts @@ -0,0 +1,21 @@ +import {Action, ThunkAction} from '@reduxjs/toolkit'; +import {getNotificationsData} from '../../services'; +import {userNotificationsFetched} from '../reducers'; +import {RootState} from '../rootReducer'; + +export const loadUserNotifications = (): ThunkAction< + Promise, + RootState, + unknown, + Action +> => async (dispatch) => { + try { + const notifications = await getNotificationsData(); + dispatch({ + type: userNotificationsFetched.type, + payload: notifications, + }); + } catch (error) { + console.log(error); + } +}; diff --git a/src/store/actions/user.ts b/src/store/actions/user.ts index e77b8513..eee5fcde 100644 --- a/src/store/actions/user.ts +++ b/src/store/actions/user.ts @@ -26,7 +26,7 @@ export const loadUserData = ( const token = await getTokenOrLogout(dispatch); const [profile, avatar, cover] = await Promise.all([ loadProfileInfo(token, user.userId), - loadAvatar(token, user.userId), + loadAvatar(user.userId, false), loadCover(token, user.userId), ]); dispatch({ diff --git a/src/store/actions/userX.ts b/src/store/actions/userX.ts index 87162eb1..e313546e 100644 --- a/src/store/actions/userX.ts +++ b/src/store/actions/userX.ts @@ -52,7 +52,7 @@ export const loadUserX = ( payload: {screenType, userId, data}, }), ); - loadAvatar(token, userId).then((data) => + loadAvatar(userId, false).then((data) => dispatch({ type: userXAvatarFetched.type, payload: {screenType, userId, data}, diff --git a/src/store/initialStates.ts b/src/store/initialStates.ts index 8f4a2e84..b75569d6 100644 --- a/src/store/initialStates.ts +++ b/src/store/initialStates.ts @@ -1,11 +1,13 @@ -import {MomentCategoryType, MomentType} from '../types'; import { - ProfileType, - SocialAccountType, + MomentCategoryType, + MomentType, + NotificationType, ProfilePreviewType, + ProfileType, ScreenType, - UserXType, + SocialAccountType, UserType, + UserXType, } from '../types'; export const NO_PROFILE: ProfileType = { @@ -20,6 +22,8 @@ export const NO_PROFILE: ProfileType = { export const EMPTY_MOMENTS_LIST = []; +export const EMPTY_NOTIFICATIONS_LIST = []; + export const NO_USER: UserType = { userId: '', username: '', @@ -34,6 +38,10 @@ export const NO_USER_DATA = { cover: '', }; +export const NO_NOTIFICATIONS = { + notifications: EMPTY_NOTIFICATIONS_LIST, +}; + export const NO_FOLLOW_DATA = { followers: EMPTY_PROFILE_PREVIEW_LIST, following: EMPTY_PROFILE_PREVIEW_LIST, @@ -113,6 +121,7 @@ export const EMPTY_SCREEN_TO_USERS_LIST: Record< > = { [ScreenType.Profile]: EMPTY_USERX_LIST, [ScreenType.Search]: EMPTY_USERX_LIST, + [ScreenType.Notifications]: EMPTY_USERX_LIST, }; export const INITIAL_CATEGORIES_STATE = { diff --git a/src/store/reducers/index.ts b/src/store/reducers/index.ts index e09b41ee..f525eb81 100644 --- a/src/store/reducers/index.ts +++ b/src/store/reducers/index.ts @@ -6,3 +6,4 @@ export * from './taggUsersReducer'; export * from './userBlockReducer'; export * from './userXReducer'; export * from './momentCategoryReducer'; +export * from './userNotificationsReducer'; diff --git a/src/store/reducers/userNotificationsReducer.ts b/src/store/reducers/userNotificationsReducer.ts new file mode 100644 index 00000000..4fc196ca --- /dev/null +++ b/src/store/reducers/userNotificationsReducer.ts @@ -0,0 +1,15 @@ +import {createSlice} from '@reduxjs/toolkit'; +import {NO_NOTIFICATIONS} from '../initialStates'; + +const userNotificationsSlice = createSlice({ + name: 'userNotifications', + initialState: NO_NOTIFICATIONS, + reducers: { + userNotificationsFetched: (state, action) => { + state.notifications = action.payload; + }, + }, +}); + +export const {userNotificationsFetched} = userNotificationsSlice.actions; +export const userNotificationsReducer = userNotificationsSlice.reducer; diff --git a/src/store/rootReducer.ts b/src/store/rootReducer.ts index 8f002de0..7940b1fe 100644 --- a/src/store/rootReducer.ts +++ b/src/store/rootReducer.ts @@ -8,6 +8,7 @@ import { userBlockReducer, userXReducer, momentCategoriesReducer, + userNotificationsReducer, } from './reducers'; /** @@ -18,6 +19,7 @@ const rootReducer = combineReducers({ user: userDataReducer, follow: userFollowReducer, moments: userMomentsReducer, + notifications: userNotificationsReducer, socialAccounts: userSocialsReducer, taggUsers: taggUsersReducer, blocked: userBlockReducer, diff --git a/src/types/types.ts b/src/types/types.ts index bda43190..fc0af522 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -84,6 +84,7 @@ export interface CommentType { comment_id: string; comment: string; date_time: string; + moment_id: string; commenter: ProfilePreviewType; } @@ -97,6 +98,7 @@ export type PreviewType = export enum ScreenType { Profile, Search, + Notifications, } /** @@ -170,3 +172,12 @@ export type TaggPopupType = { messageBody: string; next?: TaggPopupType; }; + +export type NotificationType = { + actor: ProfilePreviewType; + verbage: string; + notification_type: 'DFT' | 'FLO' | 'CMT'; + notification_object: CommentType | undefined; + timestamp: string; + unread: boolean; +}; diff --git a/src/utils/common.ts b/src/utils/common.ts index ae83ad9c..27411149 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -1,3 +1,4 @@ +import moment from 'moment'; import {Linking} from 'react-native'; import {BROWSABLE_SOCIAL_URLS, TOGGLE_BUTTON_TYPE} from '../constants'; @@ -23,3 +24,21 @@ export const handleOpenSocialUrlOnBrowser = ( Linking.openURL(BROWSABLE_SOCIAL_URLS[social] + `${handle}/`); } }; + +export const getDateAge: ( + date: moment.Moment, +) => 'today' | 'yesterday' | 'thisWeek' | 'unknown' = (date: moment.Moment) => { + const today = moment().startOf('day'); + const yesterday = moment().subtract(1, 'days').startOf('day'); + const weekOld = moment().subtract(7, 'days').startOf('day'); + if (date.isSame(today, 'd')) { + return 'today'; + } else if (date.isSame(yesterday, 'd')) { + return 'yesterday'; + } else if (date.isAfter(weekOld)) { + return 'thisWeek'; + } else { + // this can be longer than a week or in the future + return 'unknown'; + } +}; diff --git a/src/utils/users.ts b/src/utils/users.ts index 4f93347d..be92d184 100644 --- a/src/utils/users.ts +++ b/src/utils/users.ts @@ -1,10 +1,6 @@ -import {loadUserMomentCategories} from './../store/actions/momentCategories'; -import {loadUserX} from './../store/actions/userX'; -import {RootState} from './../store/rootReducer'; import AsyncStorage from '@react-native-community/async-storage'; -import {AppDispatch} from './../store/configureStore'; -import {UserType, ScreenType} from './../types/types'; import {INTEGRATED_SOCIAL_LIST} from '../constants'; +import {loadSocialPosts} from '../services'; import { loadAllSocials, loadBlockedList, @@ -12,10 +8,15 @@ import { loadRecentlySearched, loadUserData, loadUserMoments, + loadUserNotifications, } from '../store/actions'; import {NO_SOCIAL_ACCOUNTS} from '../store/initialStates'; -import {loadSocialPosts} from '../services'; import {userLoggedIn} from '../store/reducers'; +import {loadUserMomentCategories} from './../store/actions/momentCategories'; +import {loadUserX} from './../store/actions/userX'; +import {AppDispatch} from './../store/configureStore'; +import {RootState} from './../store/rootReducer'; +import {ScreenType, UserType} from './../types/types'; const loadData = async (dispatch: AppDispatch, user: UserType) => { await Promise.all([ @@ -23,6 +24,7 @@ const loadData = async (dispatch: AppDispatch, user: UserType) => { dispatch(loadFollowData(user.userId)), dispatch(loadUserMomentCategories(user.userId)), dispatch(loadUserMoments(user.userId)), + dispatch(loadUserNotifications()), dispatch(loadAllSocials(user.userId)), dispatch(loadBlockedList(user.userId)), dispatch(loadRecentlySearched()), -- cgit v1.2.3-70-g09d2