aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIvan Chen <ivan@thetaggid.com>2020-12-29 20:21:24 -0500
committerGitHub <noreply@github.com>2020-12-29 20:21:24 -0500
commitbd2f89805d0bb1c2f1d08fe8d91099aa4f109d35 (patch)
treeac7219e034a0c4035096c6df8dbe6b92446b5111
parentec478d4981c726856485b49b49ac33b0d9e6a903 (diff)
[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>
-rw-r--r--src/components/notifications/Notification.tsx150
-rw-r--r--src/components/notifications/index.ts1
-rw-r--r--src/components/profile/ProfilePreview.tsx35
-rw-r--r--src/constants/api.ts4
-rw-r--r--src/routes/index.ts2
-rw-r--r--src/routes/main/MainStackNavigator.tsx (renamed from src/routes/profile/ProfileStackNavigator.tsx)7
-rw-r--r--src/routes/main/MainStackScreen.tsx (renamed from src/routes/profile/ProfileStackScreen.tsx)76
-rw-r--r--src/routes/main/index.ts2
-rw-r--r--src/routes/profile/index.ts2
-rw-r--r--src/routes/tabs/NavigationBar.tsx15
-rw-r--r--src/screens/main/Notifications.tsx13
-rw-r--r--src/screens/main/NotificationsScreen.tsx167
-rw-r--r--src/screens/main/index.ts2
-rw-r--r--src/screens/profile/IndividualMoment.tsx2
-rw-r--r--src/screens/profile/MomentCommentsScreen.tsx2
-rw-r--r--src/services/NotificationService.ts32
-rw-r--r--src/services/UserProfileService.ts13
-rw-r--r--src/services/index.ts1
-rw-r--r--src/store/actions/index.ts1
-rw-r--r--src/store/actions/notifications.ts21
-rw-r--r--src/store/actions/user.ts2
-rw-r--r--src/store/actions/userX.ts2
-rw-r--r--src/store/initialStates.ts17
-rw-r--r--src/store/reducers/index.ts1
-rw-r--r--src/store/reducers/userNotificationsReducer.ts15
-rw-r--r--src/store/rootReducer.ts2
-rw-r--r--src/types/types.ts11
-rw-r--r--src/utils/common.ts19
-rw-r--r--src/utils/users.ts14
29 files changed, 530 insertions, 101 deletions
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<NotificationProps> = (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<string | undefined>(undefined);
+ const [momentURI, setMomentURI] = useState<string | undefined>(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 (
+ <TouchableWithoutFeedback
+ style={[styles.container, {backgroundColor}]}
+ onPress={onNotificationTap}>
+ <View style={styles.avatarContainer}>
+ <Image
+ style={styles.avatar}
+ source={
+ avatarURI
+ ? {uri: avatarURI, cache: 'only-if-cached'}
+ : require('../../assets/images/avatar-placeholder.png')
+ }
+ />
+ </View>
+ <View style={styles.contentContainer}>
+ <Text style={styles.actorName}>
+ {first_name} {last_name}
+ </Text>
+ <Text>{verbage}</Text>
+ </View>
+ {/* TODO: Still WIP */}
+ {/* {notification_type === 'CMT' && notification_object && (
+ <Image
+ style={styles.moment}
+ source={{uri: momentURI, cache: 'only-if-cached'}}
+ />
+ )} */}
+ </TouchableWithoutFeedback>
+ );
+};
+
+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<ProfilePreviewProps> = ({
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/profile/ProfileStackNavigator.tsx b/src/routes/main/MainStackNavigator.tsx
index bc0a9560..c156c725 100644
--- a/src/routes/profile/ProfileStackNavigator.tsx
+++ b/src/routes/main/MainStackNavigator.tsx
@@ -4,7 +4,7 @@
import {createStackNavigator} from '@react-navigation/stack';
import {CategorySelectionScreenType, MomentType, ScreenType} from '../../types';
-export type ProfileStackParams = {
+export type MainStackParams = {
Search: {
screenType: ScreenType;
};
@@ -45,6 +45,9 @@ export type ProfileStackParams = {
categories: Array<string>;
screenType: CategorySelectionScreenType;
};
+ Notifications: {
+ screenType: ScreenType;
+ };
};
-export const ProfileStack = createStackNavigator<ProfileStackParams>();
+export const MainStack = createStackNavigator<MainStackParams>();
diff --git a/src/routes/profile/ProfileStackScreen.tsx b/src/routes/main/MainStackScreen.tsx
index 4fc9f0c7..cd053bde 100644
--- a/src/routes/profile/ProfileStackScreen.tsx
+++ b/src/routes/main/MainStackScreen.tsx
@@ -9,12 +9,14 @@ import {
FollowersListScreen,
EditProfile,
CategorySelection,
+ NotificationsScreen,
} from '../../screens';
-import {ProfileStack, ProfileStackParams} from './ProfileStackNavigator';
+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)
@@ -27,16 +29,29 @@ import {StackNavigationOptions} from '@react-navigation/stack';
* EditProfile : To edit logged in user's information.
*/
-type ProfileStackRouteProps = RouteProp<ProfileStackParams, 'Profile'>;
+type MainStackRouteProps = RouteProp<MainStackParams, 'Profile'>;
-interface ProfileStackProps {
- route: ProfileStackRouteProps;
+interface MainStackProps {
+ route: MainStackRouteProps;
}
-const ProfileStackScreen: React.FC<ProfileStackProps> = ({route}) => {
+const MainStackScreen: React.FC<MainStackProps> = ({route}) => {
const {screenType} = route.params;
- const isProfileStack = screenType === ScreenType.Profile;
+ // 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'},
@@ -61,13 +76,13 @@ const ProfileStackScreen: React.FC<ProfileStackProps> = ({route}) => {
};
return (
- <ProfileStack.Navigator
+ <MainStack.Navigator
screenOptions={{
headerShown: false,
}}
mode="card"
- initialRouteName={isProfileStack ? 'Profile' : 'Search'}>
- <ProfileStack.Screen
+ initialRouteName={initialRouteName}>
+ <MainStack.Screen
name="Profile"
component={ProfileScreen}
options={{
@@ -82,16 +97,26 @@ const ProfileStackScreen: React.FC<ProfileStackProps> = ({route}) => {
screenType,
}}
/>
- {!isProfileStack ? (
- <ProfileStack.Screen
+ {isSearchTab && (
+ <MainStack.Screen
name="Search"
component={SearchScreen}
initialParams={{screenType}}
/>
- ) : (
- <React.Fragment />
)}
- <ProfileStack.Screen
+ {isNotificationsTab && (
+ <MainStack.Screen
+ name="Notifications"
+ component={NotificationsScreen}
+ initialParams={{screenType}}
+ />
+ )}
+ <MainStack.Screen
+ name="CaptionScreen"
+ component={CaptionScreen}
+ options={{...modalStyle, gestureEnabled: false}}
+ />
+ <MainStack.Screen
name="SocialMediaTaggs"
component={SocialMediaTaggs}
options={{
@@ -104,7 +129,7 @@ const ProfileStackScreen: React.FC<ProfileStackProps> = ({route}) => {
}}
initialParams={{screenType}}
/>
- <ProfileStack.Screen
+ <MainStack.Screen
name="CategorySelection"
component={CategorySelection}
options={{
@@ -115,16 +140,7 @@ const ProfileStackScreen: React.FC<ProfileStackProps> = ({route}) => {
headerTitle: '',
}}
/>
- {isProfileStack ? (
- <ProfileStack.Screen
- name="CaptionScreen"
- component={CaptionScreen}
- options={{...modalStyle, gestureEnabled: false}}
- />
- ) : (
- <React.Fragment />
- )}
- <ProfileStack.Screen
+ <MainStack.Screen
name="IndividualMoment"
component={IndividualMoment}
options={{
@@ -132,7 +148,7 @@ const ProfileStackScreen: React.FC<ProfileStackProps> = ({route}) => {
}}
initialParams={{screenType}}
/>
- <ProfileStack.Screen
+ <MainStack.Screen
name="MomentCommentsScreen"
component={MomentCommentsScreen}
options={{
@@ -140,7 +156,7 @@ const ProfileStackScreen: React.FC<ProfileStackProps> = ({route}) => {
}}
initialParams={{screenType}}
/>
- <ProfileStack.Screen
+ <MainStack.Screen
name="FollowersListScreen"
component={FollowersListScreen}
options={{
@@ -148,7 +164,7 @@ const ProfileStackScreen: React.FC<ProfileStackProps> = ({route}) => {
}}
initialParams={{screenType}}
/>
- <ProfileStack.Screen
+ <MainStack.Screen
name="EditProfile"
component={EditProfile}
options={{
@@ -159,8 +175,8 @@ const ProfileStackScreen: React.FC<ProfileStackProps> = ({route}) => {
headerTintColor: 'white',
}}
/>
- </ProfileStack.Navigator>
+ </MainStack.Navigator>
);
};
-export default ProfileStackScreen;
+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/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 */}
- {/* <Tabs.Screen name="Home" component={Home} />
- <Tabs.Screen name="Notifications" component={Notifications} />
- <Tabs.Screen name="Upload" component={Upload} /> */}
+ <Tabs.Screen
+ name="Notifications"
+ component={MainStackScreen}
+ initialParams={{screenType: ScreenType.Notifications}}
+ />
<Tabs.Screen
name="Search"
- component={Profile}
+ component={MainStackScreen}
initialParams={{screenType: ScreenType.Search}}
/>
<Tabs.Screen
name="Profile"
- component={Profile}
+ component={MainStackScreen}
initialParams={{screenType: ScreenType.Profile}}
/>
</Tabs.Navigator>
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 <ComingSoon />;
-};
-
-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<moment.Moment | undefined>(
+ 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}) => (
+ <Notification item={item} screenType={ScreenType.Notifications} />
+ );
+
+ const renderSectionHeader = ({section: {title}}) => (
+ <View style={styles.sectionHeaderContainer}>
+ <Text style={styles.sectionHeader}>{title}</Text>
+ </View>
+ );
+
+ return (
+ <SafeAreaView>
+ <View style={styles.header}>
+ <Text style={styles.headerText}>Notifications</Text>
+ <View style={styles.underline} />
+ </View>
+ <SectionList
+ contentContainerStyle={styles.container}
+ sections={sectionedNotifications}
+ keyExtractor={(item, index) => index.toString()}
+ renderItem={renderNotification}
+ renderSectionHeader={renderSectionHeader}
+ refreshControl={
+ <RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
+ }
+ />
+ </SafeAreaView>
+ );
+};
+
+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<void>,
+ RootState,
+ unknown,
+ Action<string>
+> => 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 = <MomentType[]>[];
+export const EMPTY_NOTIFICATIONS_LIST = <NotificationType[]>[];
+
export const NO_USER: UserType = {
userId: '',
username: '',
@@ -34,6 +38,10 @@ export const NO_USER_DATA = {
cover: <string | null>'',
};
+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()),