aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/assets/gifs/loading-animation.gifbin0 -> 285396 bytes
-rw-r--r--src/assets/navigationIcons/new-notifications.pngbin0 -> 92197 bytes
-rw-r--r--src/components/common/NavigationIcon.tsx5
-rw-r--r--src/components/common/TaggLoadingIndicator.tsx40
-rw-r--r--src/components/profile/ProfileBody.tsx3
-rw-r--r--src/routes/Routes.tsx7
-rw-r--r--src/routes/tabs/NavigationBar.tsx43
-rw-r--r--src/screens/main/NotificationsScreen.tsx36
-rw-r--r--src/screens/profile/CaptionScreen.tsx43
-rw-r--r--src/screens/profile/EditProfile.tsx17
-rw-r--r--src/screens/profile/SocialMediaTaggs.tsx10
-rw-r--r--src/services/MomentServices.ts10
-rw-r--r--src/store/actions/user.ts16
-rw-r--r--src/store/initialStates.ts5
-rw-r--r--src/store/reducers/userReducer.ts5
-rw-r--r--src/utils/common.ts22
16 files changed, 197 insertions, 65 deletions
diff --git a/src/assets/gifs/loading-animation.gif b/src/assets/gifs/loading-animation.gif
new file mode 100644
index 00000000..6a69b07b
--- /dev/null
+++ b/src/assets/gifs/loading-animation.gif
Binary files differ
diff --git a/src/assets/navigationIcons/new-notifications.png b/src/assets/navigationIcons/new-notifications.png
new file mode 100644
index 00000000..e8d7e70f
--- /dev/null
+++ b/src/assets/navigationIcons/new-notifications.png
Binary files differ
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;
+};