diff options
-rw-r--r-- | src/assets/images/private-profile.png | bin | 0 -> 3750 bytes | |||
-rw-r--r-- | src/assets/images/private-profile@2x.png | bin | 0 -> 7999 bytes | |||
-rw-r--r-- | src/assets/images/private-profile@3x.png | bin | 0 -> 10081 bytes | |||
-rw-r--r-- | src/components/common/TaggPrompt.tsx | 2 | ||||
-rw-r--r-- | src/components/profile/Content.tsx | 292 | ||||
-rw-r--r-- | src/components/profile/FriendsCount.tsx | 21 | ||||
-rw-r--r-- | src/components/profile/PrivateProfile.tsx | 34 | ||||
-rw-r--r-- | src/components/profile/ProfileBody.tsx | 19 | ||||
-rw-r--r-- | src/components/profile/PublicProfile.tsx | 280 | ||||
-rw-r--r-- | src/components/taggs/Tagg.tsx | 5 | ||||
-rw-r--r-- | src/components/taggs/TaggsBar.tsx | 14 | ||||
-rw-r--r-- | src/constants/strings.ts | 1 | ||||
-rw-r--r-- | src/screens/suggestedPeople/SPBody.tsx | 11 | ||||
-rw-r--r-- | src/services/UserFriendsService.ts | 9 | ||||
-rw-r--r-- | src/store/actions/userFriends.ts | 23 | ||||
-rw-r--r-- | src/types/types.ts | 8 | ||||
-rw-r--r-- | src/utils/users.ts | 32 |
17 files changed, 433 insertions, 318 deletions
diff --git a/src/assets/images/private-profile.png b/src/assets/images/private-profile.png Binary files differnew file mode 100644 index 00000000..820ba287 --- /dev/null +++ b/src/assets/images/private-profile.png diff --git a/src/assets/images/private-profile@2x.png b/src/assets/images/private-profile@2x.png Binary files differnew file mode 100644 index 00000000..cdcf44d5 --- /dev/null +++ b/src/assets/images/private-profile@2x.png diff --git a/src/assets/images/private-profile@3x.png b/src/assets/images/private-profile@3x.png Binary files differnew file mode 100644 index 00000000..f909fe2a --- /dev/null +++ b/src/assets/images/private-profile@3x.png diff --git a/src/components/common/TaggPrompt.tsx b/src/components/common/TaggPrompt.tsx index d65e30c6..6169b3f1 100644 --- a/src/components/common/TaggPrompt.tsx +++ b/src/components/common/TaggPrompt.tsx @@ -66,7 +66,7 @@ const styles = StyleSheet.create({ justifyContent: 'center', alignItems: 'center', backgroundColor: 'white', - height: SCREEN_HEIGHT / 4.5, + height: SCREEN_HEIGHT / 4, }, closeButton: { position: 'relative', diff --git a/src/components/profile/Content.tsx b/src/components/profile/Content.tsx index 1a5a205c..a22b9728 100644 --- a/src/components/profile/Content.tsx +++ b/src/components/profile/Content.tsx @@ -1,128 +1,74 @@ -import {useFocusEffect, useNavigation} from '@react-navigation/native'; import React, {useCallback, useEffect, useState} from 'react'; import { - Alert, LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent, RefreshControl, StyleSheet, - Text, - View, } from 'react-native'; -import {TouchableOpacity} from 'react-native-gesture-handler'; import Animated from 'react-native-reanimated'; import {useDispatch, useSelector, useStore} from 'react-redux'; -import Cover from './Cover'; -import GreyPlusLogo from '../../assets/icons/grey-plus-logo.svg'; -import {COVER_HEIGHT, TAGG_LIGHT_BLUE} from '../../constants'; -import { - UPLOAD_MOMENT_PROMPT_THREE_HEADER, - UPLOAD_MOMENT_PROMPT_THREE_MESSAGE, - UPLOAD_MOMENT_PROMPT_TWO_HEADER, - UPLOAD_MOMENT_PROMPT_TWO_MESSAGE, -} from '../../constants/strings'; +import {COVER_HEIGHT} from '../../constants'; import { blockUnblockUser, - deleteUserMomentsForCategory, loadFriendsData, - updateMomentCategories, updateUserXFriends, } from '../../store/actions'; import { - EMPTY_MOMENTS_LIST, EMPTY_PROFILE_PREVIEW_LIST, NO_PROFILE, NO_USER, } from '../../store/initialStates'; import {RootState} from '../../store/rootreducer'; -import {CategorySelectionScreenType, MomentType, ScreenType} from '../../types'; +import {ContentProps} from '../../types'; import { + canViewProfile, fetchUserX, getUserAsProfilePreviewType, - moveCategory, - normalize, SCREEN_HEIGHT, userLogin, } from '../../utils'; -import {TaggPrompt} from '../common'; -import {Moment} from '../moments'; -import TaggsBar from '../taggs/TaggsBar'; +import Cover from './Cover'; +import PrivateProfile from './PrivateProfile'; import ProfileBody from './ProfileBody'; import ProfileCutout from './ProfileCutout'; import ProfileHeader from './ProfileHeader'; - -interface ContentProps { - y: Animated.Value<number>; - userXId: string | undefined; - screenType: ScreenType; -} +import PublicProfile from './PublicProfile'; +import TaggsBar from '../taggs/TaggsBar'; const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => { const dispatch = useDispatch(); - - const {user = NO_USER, profile = NO_PROFILE} = userXId - ? useSelector((state: RootState) => state.userX[screenType][userXId]) - : useSelector((state: RootState) => state.user); - - const {friends = EMPTY_PROFILE_PREVIEW_LIST} = userXId - ? useSelector((state: RootState) => state.userX[screenType][userXId]) - : useSelector((state: RootState) => state.friends); - + const state: RootState = useStore().getState(); const { - friends: friendsLoggedInUser = EMPTY_PROFILE_PREVIEW_LIST, - } = useSelector((state: RootState) => state.friends); - - const {moments = EMPTY_MOMENTS_LIST} = userXId - ? useSelector((state: RootState) => state.userX[screenType][userXId]) - : useSelector((state: RootState) => state.moments); - - const {momentCategories = []} = userXId - ? useSelector((state: RootState) => state.userX[screenType][userXId]) - : useSelector((state: RootState) => state.momentCategories); - + user = NO_USER, + profile = NO_PROFILE, + } = useSelector((state: RootState) => + userXId ? state.userX[screenType][userXId] : state.user, + ); const {blockedUsers = EMPTY_PROFILE_PREVIEW_LIST} = useSelector( (state: RootState) => state.blocked, ); const {user: loggedInUser = NO_USER} = useSelector( (state: RootState) => state.user, ); - const state = useStore().getState(); - - const navigation = useNavigation(); /** * States */ - const [imagesMap, setImagesMap] = useState<Map<string, MomentType[]>>( - new Map(), - ); - const [isFriend, setIsFriend] = useState<boolean>(false); const [isBlocked, setIsBlocked] = useState<boolean>(false); const [profileBodyHeight, setProfileBodyHeight] = useState(0); const [shouldBounce, setShouldBounce] = useState<boolean>(true); const [refreshing, setRefreshing] = useState<boolean>(false); - const [isStageTwoPromptClosed, setIsStageTwoPromptClosed] = useState<boolean>( - false, - ); - const [isStageOnePromptClosed, setIsStageOnePromptClosed] = useState<boolean>( - false, - ); - const [ - isStageThreePromptClosed, - setIsStageThreePromptClosed, - ] = useState<boolean>(false); - const onRefresh = useCallback(() => { const refrestState = async () => { + setRefreshing(true); if (!userXId) { await userLogin(dispatch, loggedInUser); } else { await fetchUserX(dispatch, user, screenType); } }; - setRefreshing(true); refrestState().then(() => { setRefreshing(false); }); @@ -133,83 +79,11 @@ const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => { setProfileBodyHeight(height); }; - const createImagesMap = useCallback(() => { - var map = new Map(); - moments.forEach(function (imageObject) { - var moment_category = imageObject.moment_category; - if (map.has(moment_category)) { - map.get(moment_category).push(imageObject); - } else { - map.set(moment_category, [imageObject]); - } - }); - setImagesMap(map); - }, [moments]); - - useEffect(() => { - createImagesMap(); - }, [createImagesMap]); - - const move = (direction: 'up' | 'down', title: string) => { - let categories = [...momentCategories]; - categories = moveCategory(categories, title, direction === 'up'); - dispatch(updateMomentCategories(categories, false)); - }; - - /** - * Prompt user to perform an activity based on their profile completion stage - * To fire 2 seconds after the screen comes in focus - * 1 means STAGE_1: - * The user must upload a moment, so take them to a screen guiding them to post a moment - * 2 means STAGE_2: - * The user must create another category so show a prompt on top of the screen - * 3 means STAGE_3: - * The user must upload a moment to the second category, so show a prompt on top of the screen - * Else, profile is complete and no prompt needs to be shown - */ - useFocusEffect( - useCallback(() => { - const navigateToMomentUploadPrompt = () => { - switch (profile.profile_completion_stage) { - case 1: - if ( - momentCategories && - momentCategories[0] && - !isStageOnePromptClosed - ) { - navigation.navigate('MomentUploadPrompt', { - screenType, - momentCategory: momentCategories[0], - }); - setIsStageOnePromptClosed(true); - } - break; - case 2: - setIsStageTwoPromptClosed(false); - break; - case 3: - setIsStageThreePromptClosed(false); - break; - default: - break; - } - }; - if (!userXId) { - setTimeout(navigateToMomentUploadPrompt, 2000); - } - }, [ - profile.profile_completion_stage, - momentCategories, - userXId, - isStageOnePromptClosed, - ]), - ); - useEffect(() => { const isActuallyBlocked = blockedUsers.some( (cur_user) => user.username === cur_user.username, ); - if (isBlocked != isActuallyBlocked) { + if (isBlocked !== isActuallyBlocked) { setIsBlocked(isActuallyBlocked); } }, [blockedUsers, user]); @@ -234,37 +108,6 @@ const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => { } }; - /** - * Handle deletion of a category - * Confirm with user before deleting the category - * @param category category to be deleted - */ - const handleCategoryDeletion = (category: string) => { - Alert.alert( - 'Category Deletion', - `Are you sure that you want to delete the category ${category} ?`, - [ - { - text: 'Cancel', - style: 'cancel', - }, - { - text: 'Yes', - onPress: () => { - dispatch( - updateMomentCategories( - momentCategories.filter((mc) => mc !== category), - false, - ), - ); - dispatch(deleteUserMomentsForCategory(category)); - }, - }, - ], - {cancelable: true}, - ); - }; - const handleScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => { /** * Set the new y position @@ -285,6 +128,7 @@ const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => { return ( <Animated.ScrollView + contentContainerStyle={styles.contentContainer} style={styles.container} onScroll={(e) => handleScroll(e)} bounces={shouldBounce} @@ -304,7 +148,6 @@ const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => { onLayout, userXId, screenType, - isFriend, handleBlockUnblock, isBlocked, }} @@ -313,111 +156,22 @@ const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => { {...{y, profileBodyHeight, userXId, screenType}} whiteRing={undefined} /> - <View style={styles.momentsContainer}> - {userXId && moments.length === 0 && ( - <View style={styles.plusIconContainer}> - <GreyPlusLogo width={90} height={90} /> - <Text style={styles.noMomentsText}>{`Looks like ${ - profile.name.split(' ')[0] - } has not posted any moments yet`}</Text> - </View> - )} - {!userXId && - profile.profile_completion_stage === 2 && - !isStageTwoPromptClosed && ( - <TaggPrompt - messageHeader={UPLOAD_MOMENT_PROMPT_TWO_HEADER} - messageBody={UPLOAD_MOMENT_PROMPT_TWO_MESSAGE} - logoType="" - onClose={() => { - setIsStageTwoPromptClosed(true); - }} - /> - )} - {!userXId && - profile.profile_completion_stage === 3 && - !isStageThreePromptClosed && ( - <TaggPrompt - messageHeader={UPLOAD_MOMENT_PROMPT_THREE_HEADER} - messageBody={UPLOAD_MOMENT_PROMPT_THREE_MESSAGE} - logoType="" - onClose={() => { - setIsStageThreePromptClosed(true); - }} - /> - )} - {momentCategories.map( - (title, index) => - (!userXId || imagesMap.get(title)) && ( - <Moment - key={index} - title={title} - images={imagesMap.get(title)} - userXId={userXId} - screenType={screenType} - handleMomentCategoryDelete={handleCategoryDeletion} - shouldAllowDeletion={momentCategories.length > 1} - showUpButton={index !== 0} - showDownButton={index !== momentCategories.length - 1} - move={move} - /> - ), - )} - {!userXId && ( - <TouchableOpacity - onPress={() => - navigation.push('CategorySelection', { - screenType: CategorySelectionScreenType.Profile, - user: loggedInUser, - }) - } - style={styles.createCategoryButton}> - <Text style={styles.createCategoryButtonLabel}> - Create a new category - </Text> - </TouchableOpacity> - )} - </View> + {canViewProfile(state, userXId, screenType) ? ( + <PublicProfile {...{y, userXId, screenType}} /> + ) : ( + <PrivateProfile /> + )} </Animated.ScrollView> ); }; const styles = StyleSheet.create({ container: { - flex: 1, backgroundColor: '#fff', - }, - momentsContainer: { - backgroundColor: '#f2f2f2', - paddingBottom: SCREEN_HEIGHT / 10, flex: 1, - flexDirection: 'column', - }, - createCategoryButton: { - backgroundColor: TAGG_LIGHT_BLUE, - justifyContent: 'center', - alignItems: 'center', - width: '70%', - height: 30, - marginTop: '15%', - alignSelf: 'center', - }, - createCategoryButtonLabel: { - fontSize: normalize(16), - fontWeight: '500', - color: 'white', - }, - plusIconContainer: { - flexDirection: 'column', - justifyContent: 'center', - alignItems: 'center', - marginVertical: '10%', }, - noMomentsText: { - fontSize: normalize(14), - fontWeight: 'bold', - color: 'gray', - marginVertical: '8%', + contentContainer: { + flexGrow: 1, }, }); diff --git a/src/components/profile/FriendsCount.tsx b/src/components/profile/FriendsCount.tsx index 851dbc3b..4790743b 100644 --- a/src/components/profile/FriendsCount.tsx +++ b/src/components/profile/FriendsCount.tsx @@ -1,11 +1,11 @@ +import {useNavigation} from '@react-navigation/native'; import React from 'react'; -import {View, Text, StyleSheet, ViewProps} from 'react-native'; +import {StyleSheet, Text, View, ViewProps} from 'react-native'; import {TouchableOpacity} from 'react-native-gesture-handler'; -import {useNavigation} from '@react-navigation/native'; +import {useSelector, useStore} from 'react-redux'; import {RootState} from '../../store/rootReducer'; -import {useSelector} from 'react-redux'; import {ScreenType} from '../../types'; -import {normalize} from '../../utils'; +import {canViewProfile, normalize} from '../../utils'; interface FriendsCountProps extends ViewProps { userXId: string | undefined; @@ -17,9 +17,9 @@ const FriendsCount: React.FC<FriendsCountProps> = ({ userXId, screenType, }) => { - const {friends} = userXId - ? useSelector((state: RootState) => state.userX[screenType][userXId]) - : useSelector((state: RootState) => state.friends); + const {friends} = useSelector((state: RootState) => + userXId ? state.userX[screenType][userXId] : state.friends, + ); const count = friends ? friends.length : 0; const displayedCount: string = @@ -32,15 +32,18 @@ const FriendsCount: React.FC<FriendsCountProps> = ({ : `${count / 1e6}m`; const navigation = useNavigation(); + const state: RootState = useStore().getState(); return ( <TouchableOpacity - style={{right: '20%'}} onPress={() => - navigation.push('FriendsListScreen', { + navigation.navigate('FriendsListScreen', { userXId, screenType, }) + } + disabled={ + !canViewProfile(state, userXId, screenType) || friends.length === 0 }> <View style={[styles.container, style]}> <Text style={styles.count}>{displayedCount}</Text> diff --git a/src/components/profile/PrivateProfile.tsx b/src/components/profile/PrivateProfile.tsx new file mode 100644 index 00000000..bc75a18a --- /dev/null +++ b/src/components/profile/PrivateProfile.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import {Image, StyleSheet, Text, View} from 'react-native'; +import {PRIVATE_ACCOUNT} from '../../constants/strings'; +import {normalize, SCREEN_HEIGHT} from '../../utils'; + +const PrivateProfile: React.FC = () => { + return ( + <View style={styles.container}> + <Image source={require('../../assets/images/private-profile.png')} /> + <View style={styles.privateAccountTextContainer}> + <Text style={styles.privateAccountTextStyle}>{PRIVATE_ACCOUNT}</Text> + </View> + </View> + ); +}; + +const styles = StyleSheet.create({ + container: { + alignItems: 'center', + justifyContent: 'center', + backgroundColor: '#F9F9F9', + height: SCREEN_HEIGHT * 0.4, + paddingBottom: SCREEN_HEIGHT * 0.1, + }, + privateAccountTextContainer: {marginTop: '8%'}, + privateAccountTextStyle: { + fontWeight: '600', + fontSize: normalize(18), + lineHeight: normalize(25), + color: '#828282', + }, +}); + +export default PrivateProfile; diff --git a/src/components/profile/ProfileBody.tsx b/src/components/profile/ProfileBody.tsx index 646be3e0..b49e71a3 100644 --- a/src/components/profile/ProfileBody.tsx +++ b/src/components/profile/ProfileBody.tsx @@ -2,21 +2,17 @@ import React from 'react'; import {LayoutChangeEvent, Linking, StyleSheet, Text, View} from 'react-native'; import {normalize} from 'react-native-elements'; import {useDispatch, useSelector, useStore} from 'react-redux'; -import { - TAGG_DARK_BLUE, - TAGG_LIGHT_BLUE, - TOGGLE_BUTTON_TYPE, -} from '../../constants'; +import {TAGG_DARK_BLUE, TOGGLE_BUTTON_TYPE} from '../../constants'; import { acceptFriendRequest, declineFriendRequest, updateUserXFriends, updateUserXProfileAllScreens, } from '../../store/actions'; -import {NO_PROFILE, NO_USER} from '../../store/initialStates'; +import {NO_PROFILE} from '../../store/initialStates'; import {RootState} from '../../store/rootReducer'; import {ScreenType} from '../../types'; -import {getUserAsProfilePreviewType, SCREEN_WIDTH} from '../../utils'; +import {getUserAsProfilePreviewType} from '../../utils'; import {FriendsButton} from '../common'; import ToggleButton from './ToggleButton'; @@ -34,12 +30,8 @@ const ProfileBody: React.FC<ProfileBodyProps> = ({ userXId, screenType, }) => { - const {profile = NO_PROFILE, user} = userXId - ? useSelector((state: RootState) => state.userX[screenType][userXId]) - : useSelector((state: RootState) => state.user); - - const {user: loggedInUser = NO_USER} = useSelector( - (state: RootState) => state.user, + const {profile = NO_PROFILE, user} = useSelector((state: RootState) => + userXId ? state.userX[screenType][userXId] : state.user, ); const { @@ -119,6 +111,7 @@ const styles = StyleSheet.create({ flex: 1, paddingTop: '3.5%', paddingBottom: '2%', + width: '50%', }, container: { paddingVertical: '1%', diff --git a/src/components/profile/PublicProfile.tsx b/src/components/profile/PublicProfile.tsx new file mode 100644 index 00000000..9683d8f2 --- /dev/null +++ b/src/components/profile/PublicProfile.tsx @@ -0,0 +1,280 @@ +import {useFocusEffect, useNavigation} from '@react-navigation/native'; +import React, {useCallback, useEffect, useState} from 'react'; +import {Alert, StyleSheet, Text, View} from 'react-native'; +import {TouchableOpacity} from 'react-native-gesture-handler'; +import {useDispatch, useSelector} from 'react-redux'; +import GreyPlusLogo from '../../assets/icons/grey-plus-logo.svg'; +import {TAGG_LIGHT_BLUE} from '../../constants'; +import { + UPLOAD_MOMENT_PROMPT_THREE_HEADER, + UPLOAD_MOMENT_PROMPT_THREE_MESSAGE, + UPLOAD_MOMENT_PROMPT_TWO_HEADER, + UPLOAD_MOMENT_PROMPT_TWO_MESSAGE, +} from '../../constants/strings'; +import { + deleteUserMomentsForCategory, + updateMomentCategories, +} from '../../store/actions'; +import { + EMPTY_MOMENTS_LIST, + NO_PROFILE, + NO_USER, +} from '../../store/initialStates'; +import {RootState} from '../../store/rootreducer'; +import { + CategorySelectionScreenType, + ContentProps, + MomentType, +} from '../../types'; +import {moveCategory, normalize, SCREEN_HEIGHT} from '../../utils'; +import {TaggPrompt} from '../common'; +import {Moment} from '../moments'; + +const PublicProfile: React.FC<ContentProps> = ({userXId, screenType}) => { + const dispatch = useDispatch(); + + const {profile = NO_PROFILE} = useSelector((state: RootState) => + userXId ? state.userX[screenType][userXId] : state.user, + ); + + const {moments = EMPTY_MOMENTS_LIST} = useSelector((state: RootState) => + userXId ? state.userX[screenType][userXId] : state.moments, + ); + + const {momentCategories = []} = useSelector((state: RootState) => + userXId ? state.userX[screenType][userXId] : state.momentCategories, + ); + + const {user: loggedInUser = NO_USER} = useSelector( + (state: RootState) => state.user, + ); + + const navigation = useNavigation(); + + /** + * States + */ + const [imagesMap, setImagesMap] = useState<Map<string, MomentType[]>>( + new Map(), + ); + + const [isStageTwoPromptClosed, setIsStageTwoPromptClosed] = useState(false); + const [isStageOnePromptClosed, setIsStageOnePromptClosed] = useState(false); + const [isStageThreePromptClosed, setIsStageThreePromptClosed] = useState( + false, + ); + + const move = (direction: 'up' | 'down', title: string) => { + let categories = [...momentCategories]; + categories = moveCategory(categories, title, direction === 'up'); + dispatch(updateMomentCategories(categories, false)); + }; + + /** + * Prompt user to perform an activity based on their profile completion stage + * To fire 2 seconds after the screen comes in focus + * 1 means STAGE_1: + * The user must upload a moment, so take them to a screen guiding them to post a moment + * 2 means STAGE_2: + * The user must create another category so show a prompt on top of the screen + * 3 means STAGE_3: + * The user must upload a moment to the second category, so show a prompt on top of the screen + * Else, profile is complete and no prompt needs to be shown + */ + useFocusEffect( + useCallback(() => { + const navigateToMomentUploadPrompt = () => { + switch (profile.profile_completion_stage) { + case 1: + if ( + momentCategories && + momentCategories[0] && + !isStageOnePromptClosed + ) { + navigation.navigate('MomentUploadPrompt', { + screenType, + momentCategory: momentCategories[0], + }); + setIsStageOnePromptClosed(true); + } + break; + case 2: + setIsStageTwoPromptClosed(false); + break; + case 3: + setIsStageThreePromptClosed(false); + break; + default: + break; + } + }; + if (!userXId) { + setTimeout(navigateToMomentUploadPrompt, 2000); + } + }, [ + userXId, + profile.profile_completion_stage, + momentCategories, + isStageOnePromptClosed, + navigation, + screenType, + ]), + ); + + /** + * Handle deletion of a category + * Confirm with user before deleting the category + * @param category category to be deleted + */ + const handleCategoryDeletion = (category: string) => { + Alert.alert( + 'Category Deletion', + `Are you sure that you want to delete the category ${category} ?`, + [ + { + text: 'Cancel', + style: 'cancel', + }, + { + text: 'Yes', + onPress: () => { + dispatch( + updateMomentCategories( + momentCategories.filter((mc) => mc !== category), + false, + ), + ); + dispatch(deleteUserMomentsForCategory(category)); + }, + }, + ], + {cancelable: true}, + ); + }; + + const createImagesMap = useCallback(() => { + let map = new Map(); + moments.forEach(function (imageObject) { + let moment_category = imageObject.moment_category; + if (map.has(moment_category)) { + map.get(moment_category).push(imageObject); + } else { + map.set(moment_category, [imageObject]); + } + }); + setImagesMap(map); + }, [moments]); + + useEffect(() => { + createImagesMap(); + }, [createImagesMap]); + + return ( + <View style={styles.momentsContainer}> + {userXId && moments.length === 0 && ( + <View style={styles.plusIconContainer}> + <GreyPlusLogo width={90} height={90} /> + <Text style={styles.noMomentsText}>{`Looks like ${ + profile.name.split(' ')[0] + } has not posted any moments yet`}</Text> + </View> + )} + {!userXId && + profile.profile_completion_stage === 2 && + !isStageTwoPromptClosed && ( + <TaggPrompt + messageHeader={UPLOAD_MOMENT_PROMPT_TWO_HEADER} + messageBody={UPLOAD_MOMENT_PROMPT_TWO_MESSAGE} + logoType="tagg" + onClose={() => { + setIsStageTwoPromptClosed(true); + }} + /> + )} + {!userXId && + profile.profile_completion_stage === 3 && + !isStageThreePromptClosed && ( + <TaggPrompt + messageHeader={UPLOAD_MOMENT_PROMPT_THREE_HEADER} + messageBody={UPLOAD_MOMENT_PROMPT_THREE_MESSAGE} + logoType="tagg" + onClose={() => { + setIsStageThreePromptClosed(true); + }} + /> + )} + {momentCategories.map( + (title, index) => + (!userXId || imagesMap.get(title)) && ( + <Moment + key={index} + title={title} + images={imagesMap.get(title)} + userXId={userXId} + screenType={screenType} + handleMomentCategoryDelete={handleCategoryDeletion} + shouldAllowDeletion={momentCategories.length > 1} + showUpButton={index !== 0} + showDownButton={index !== momentCategories.length - 1} + move={move} + /> + ), + )} + {!userXId && ( + <TouchableOpacity + onPress={() => + navigation.navigate('CategorySelection', { + screenType: CategorySelectionScreenType.Profile, + user: loggedInUser, + }) + } + style={styles.createCategoryButton}> + <Text style={styles.createCategoryButtonLabel}> + Create a new category + </Text> + </TouchableOpacity> + )} + </View> + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#fff', + }, + momentsContainer: { + backgroundColor: '#f2f2f2', + paddingBottom: SCREEN_HEIGHT * 0.15, + flex: 1, + flexDirection: 'column', + }, + createCategoryButton: { + backgroundColor: TAGG_LIGHT_BLUE, + justifyContent: 'center', + alignItems: 'center', + width: '70%', + height: 30, + marginTop: '15%', + alignSelf: 'center', + }, + createCategoryButtonLabel: { + fontSize: normalize(16), + fontWeight: '500', + color: 'white', + }, + plusIconContainer: { + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + marginVertical: '10%', + }, + noMomentsText: { + fontSize: normalize(14), + fontWeight: 'bold', + color: 'gray', + marginVertical: '8%', + }, +}); + +export default PublicProfile; diff --git a/src/components/taggs/Tagg.tsx b/src/components/taggs/Tagg.tsx index 547a77cb..4e4987fb 100644 --- a/src/components/taggs/Tagg.tsx +++ b/src/components/taggs/Tagg.tsx @@ -34,6 +34,7 @@ interface TaggProps { userXId: string | undefined; user: UserType; whiteRing: boolean | undefined; + allowNavigation?: boolean; } const Tagg: React.FC<TaggProps> = ({ @@ -45,6 +46,7 @@ const Tagg: React.FC<TaggProps> = ({ userXId, user, whiteRing, + allowNavigation = true, }) => { const navigation = useNavigation(); const [modalVisible, setModalVisible] = useState(false); @@ -145,7 +147,8 @@ const Tagg: React.FC<TaggProps> = ({ <View style={whiteRing ? styles.spcontainer : styles.container}> <TouchableOpacity style={styles.iconTap} - onPress={modalOrAuthBrowserOrPass}> + onPress={modalOrAuthBrowserOrPass} + disabled={!allowNavigation}> <SocialIcon style={styles.icon} social={social} whiteRing /> {pickTheRightRingHere()} </TouchableOpacity> diff --git a/src/components/taggs/TaggsBar.tsx b/src/components/taggs/TaggsBar.tsx index f952c53f..567b58de 100644 --- a/src/components/taggs/TaggsBar.tsx +++ b/src/components/taggs/TaggsBar.tsx @@ -2,7 +2,7 @@ import React, {Fragment, useEffect, useState} from 'react'; import {StyleSheet} from 'react-native'; import Animated from 'react-native-reanimated'; import {useSafeAreaInsets} from 'react-native-safe-area-context'; -import {useDispatch, useSelector} from 'react-redux'; +import {useDispatch, useSelector, useStore} from 'react-redux'; import { INTEGRATED_SOCIAL_LIST, PROFILE_CUTOUT_BOTTOM_Y, @@ -12,6 +12,7 @@ import {getLinkedSocials} from '../../services'; import {loadIndividualSocial, updateSocial} from '../../store/actions'; import {RootState} from '../../store/rootReducer'; import {ScreenType} from '../../types'; +import {canViewProfile} from '../../utils'; import Tagg from './Tagg'; const {View, ScrollView, interpolate, Extrapolate} = Animated; @@ -33,10 +34,11 @@ const TaggsBar: React.FC<TaggsBarProps> = ({ }) => { let [taggs, setTaggs] = useState<Object[]>([]); let [taggsNeedUpdate, setTaggsNeedUpdate] = useState(true); - - const {user} = userXId - ? useSelector((state: RootState) => state.userX[screenType][userXId]) - : useSelector((state: RootState) => state.user); + const {user} = useSelector((state: RootState) => + userXId ? state.userX[screenType][userXId] : state.user, + ); + const state: RootState = useStore().getState(); + const allowTaggsNavigation = canViewProfile(state, userXId, screenType); const dispatch = useDispatch(); @@ -79,6 +81,7 @@ const TaggsBar: React.FC<TaggsBarProps> = ({ setTaggsNeedUpdate={setTaggsNeedUpdate} setSocialDataNeedUpdate={handleSocialUpdate} whiteRing={whiteRing ? whiteRing : undefined} + allowNavigation={allowTaggsNavigation} />, ); i++; @@ -96,6 +99,7 @@ const TaggsBar: React.FC<TaggsBarProps> = ({ userXId={userXId} user={user} whiteRing={whiteRing ? whiteRing : undefined} + allowNavigation={allowTaggsNavigation} />, ); i++; diff --git a/src/constants/strings.ts b/src/constants/strings.ts index 66d4a6d9..019d0bea 100644 --- a/src/constants/strings.ts +++ b/src/constants/strings.ts @@ -55,6 +55,7 @@ export const MARKED_AS_MSG = (str: string) => `Marked as ${str}`; export const MOMENT_DELETED_MSG = 'Moment deleted....Some moments have to go, to create space for greater ones'; export const NO_NEW_NOTIFICATIONS = 'You have no new notifications'; export const NO_RESULTS_FOUND = 'No Results Found!'; +export const PRIVATE_ACCOUNT = 'This account is private'; export const SUCCESS_BADGES_UPDATE = 'Badges updated successfully!' export const SUCCESS_CATEGORY_DELETE = 'Category successfully deleted, but its memory will live on'; export const SUCCESS_INVITATION_CODE = 'Welcome to Tagg!'; diff --git a/src/screens/suggestedPeople/SPBody.tsx b/src/screens/suggestedPeople/SPBody.tsx index c4195fac..824f8b1c 100644 --- a/src/screens/suggestedPeople/SPBody.tsx +++ b/src/screens/suggestedPeople/SPBody.tsx @@ -4,17 +4,25 @@ import {StyleSheet, Text, View} from 'react-native'; import {Image} from 'react-native-animatable'; import {TouchableOpacity} from 'react-native-gesture-handler'; import Animated from 'react-native-reanimated'; +import {useStore} from 'react-redux'; import RequestedButton from '../../assets/ionicons/requested-button.svg'; import {TaggsBar} from '../../components'; import {BadgesDropdown, MutualFriends} from '../../components/suggestedPeople'; import {BADGE_DATA} from '../../constants/badges'; +import {RootState} from '../../store/rootReducer'; import { ProfilePreviewType, ScreenType, SuggestedPeopleDataType, UniversityBadge, } from '../../types'; -import {isIPhoneX, normalize, SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils'; +import { + canViewProfile, + isIPhoneX, + normalize, + SCREEN_HEIGHT, + SCREEN_WIDTH, +} from '../../utils'; interface SPBodyProps { item: SuggestedPeopleDataType; @@ -48,6 +56,7 @@ const SPBody: React.FC<SPBodyProps> = ({ }[] >([]); const navigation = useNavigation(); + const state: RootState = useStore().getState(); useEffect(() => { const newBadges: {badge: UniversityBadge; img: any}[] = []; const findBadgeIcons = (badge: UniversityBadge) => { diff --git a/src/services/UserFriendsService.ts b/src/services/UserFriendsService.ts index da39380f..5c41e988 100644 --- a/src/services/UserFriendsService.ts +++ b/src/services/UserFriendsService.ts @@ -1,17 +1,12 @@ -//Abstracted common friends api calls out here - import AsyncStorage from '@react-native-community/async-storage'; import {Alert} from 'react-native'; -import {ContactType, FriendshipStatusType} from '../types'; import { FRIENDS_ENDPOINT, INVITE_FRIEND_ENDPOINT, USERS_FROM_CONTACTS_ENDPOINT, } from '../constants'; -import { - ERROR_SOMETHING_WENT_WRONG, - ERROR_SOMETHING_WENT_WRONG_REFRESH, -} from '../constants/strings'; +import {ERROR_SOMETHING_WENT_WRONG_REFRESH} from '../constants/strings'; +import {ContactType, FriendshipStatusType} from '../types'; export const loadFriends = async (userId: string, token: string) => { try { diff --git a/src/store/actions/userFriends.ts b/src/store/actions/userFriends.ts index 9da3cb4a..4183d0f6 100644 --- a/src/store/actions/userFriends.ts +++ b/src/store/actions/userFriends.ts @@ -1,25 +1,24 @@ -import {getTokenOrLogout, userXInStore} from '../../utils'; -import {RootState} from '../rootReducer'; -import { - FriendshipStatusType, - ProfilePreviewType, - ScreenType, - UserType, -} from '../../types/types'; +import {Action, ThunkAction} from '@reduxjs/toolkit'; import { acceptFriendRequestService, addFriendService, + deleteFriendshipService, friendOrUnfriendUser, loadFriends, - deleteFriendshipService, } from '../../services'; -import {Action, ThunkAction} from '@reduxjs/toolkit'; import { - userFriendsFetched, + FriendshipStatusType, + ProfilePreviewType, + ScreenType, + UserType, +} from '../../types/types'; +import {getTokenOrLogout, userXInStore} from '../../utils'; +import { updateFriends, + userFriendsFetched, userXFriendshipEdited, - userLoggedIn, } from '../reducers'; +import {RootState} from '../rootReducer'; export const loadFriendsData = ( userId: string, diff --git a/src/types/types.ts b/src/types/types.ts index 94fc966e..87d61b2e 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -1,3 +1,5 @@ +import Animated from 'react-native-reanimated'; + export interface UserType { userId: string; username: string; @@ -206,6 +208,11 @@ export interface CommentNotificationType { export interface ThreadNotificationType extends CommentNotificationType { parent_comment: string; } +export interface ContentProps { + y: Animated.Value<number>; + userXId: string | undefined; + screenType: ScreenType; +} export type NotificationType = { actor: ProfilePreviewType; @@ -253,6 +260,7 @@ export type SuggestedPeopleDataType = { social_links: string[]; suggested_people_url: string; friendship: FriendshipType; + is_private: boolean; }; export type FriendshipType = { diff --git a/src/utils/users.ts b/src/utils/users.ts index d5e44b36..f9d6d6b7 100644 --- a/src/utils/users.ts +++ b/src/utils/users.ts @@ -174,3 +174,35 @@ export const defaultUserProfile = () => { const defaultImage = require('../assets/images/avatar-placeholder.png'); return defaultImage; }; + +/** + * Used to determine whether the logged-in user is able to view userX's private + * information or not. + * + * @param state redux store's root state + * @param userXId target userX's id + * @param screenType current screen type + * @returns true if abel to view private info, false otherwise + */ +export const canViewProfile = ( + state: RootState, + userXId: string | undefined, + screenType: ScreenType, +) => { + // own profile + if (!userXId || state.user.user.userId === userXId) { + return true; + } + // not private + if (!(userXId && state.userX[screenType][userXId].profile.is_private)) { + return true; + } + // is friend + if ( + userXId && + state.userX[screenType][userXId].profile.friendship_status === 'friends' + ) { + return true; + } + return false; +}; |