diff options
author | Ashm Walia <40498934+ashmgarv@users.noreply.github.com> | 2020-12-04 08:50:24 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-12-04 11:50:24 -0500 |
commit | 0fd892ad288f2e1eaaa4fdf5e1fd6f15dbd45860 (patch) | |
tree | d7d53d94c6c4026ac9b325508ebce4706d412ac4 | |
parent | f620102190629e0b6f180d3ce056d850b1db5aaa (diff) |
[TMA - 398 AND TMA-430] Replace Providers with Redux Store (#125)
* First
* WIP
* Thunk
* Some more comments
* sc
* recent searches and follounfollow
* Edit profile dummy
* Block / unblock and some cleanup
* Replace auth provider
* Sc
* Delete AP after rebase
* Discover users
* Cleanup
* More cleanup
* Replace profile provider
* Fixed build failure
* Fixed a bug reported
* Prevent app crash when backend server is down
75 files changed, 1615 insertions, 1115 deletions
diff --git a/package.json b/package.json index 91f30119..6dde38d1 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@react-navigation/bottom-tabs": "^5.7.2", "@react-navigation/native": "^5.6.1", "@react-navigation/stack": "^5.6.2", + "@reduxjs/toolkit": "^1.4.0", "@types/react-native-vector-icons": "^6.4.5", "moment": "^2.29.1", "react": "16.13.1", @@ -39,6 +40,7 @@ "react-native-svg": "^12.1.0", "react-native-vector-icons": "^7.0.0", "react-promise-tracker": "^2.1.0", + "react-redux": "^7.2.2", "reanimated-bottom-sheet": "^1.0.0-alpha.22", "rn-fetch-blob": "^0.12.0" }, @@ -49,6 +51,7 @@ "@types/jest": "^24.0.24", "@types/react-native": "^0.62.0", "@types/react-native-snap-carousel": "^3.8.2", + "@types/react-redux": "^7.1.11", "@types/react-test-renderer": "16.9.2", "@typescript-eslint/eslint-plugin": "^2.27.0", "@typescript-eslint/parser": "^2.27.0", diff --git a/src/App.tsx b/src/App.tsx index 2e6865fd..e1cd83cc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,14 +1,19 @@ import React from 'react'; import {NavigationContainer} from '@react-navigation/native'; -import Routes, {AuthProvider} from './routes'; +import Routes from './routes'; +import {Provider} from 'react-redux'; +import store from './store/configureStore'; const App = () => { return ( - <AuthProvider> + /** + * This is the provider from the redux store, it acts as the root provider for our application + */ + <Provider store={store}> <NavigationContainer> <Routes /> </NavigationContainer> - </AuthProvider> + </Provider> ); }; diff --git a/src/components/comments/AddComment.tsx b/src/components/comments/AddComment.tsx index ac1628da..f8c0b6bc 100644 --- a/src/components/comments/AddComment.tsx +++ b/src/components/comments/AddComment.tsx @@ -7,9 +7,11 @@ import { View, } from 'react-native'; import AsyncStorage from '@react-native-community/async-storage'; -import {AuthContext} from '../../routes'; import {TaggBigInput} from '../onboarding'; import {postMomentComment} from '../../services'; +import {logout} from '../../store/actions'; +import {useSelector, useDispatch} from 'react-redux'; +import {RootState} from '../../store/rootreducer'; /** * This file provides the add comment view for a user. @@ -27,11 +29,12 @@ const AddComment: React.FC<AddCommentProps> = ({ moment_id, }) => { const [comment, setComment] = React.useState(''); + + const dispatch = useDispatch(); const { avatar, - user: {userId, username}, - logout, - } = React.useContext(AuthContext); + user: {userId}, + } = useSelector((state: RootState) => state.user); const handleCommentUpdate = (comment: string) => { setComment(comment); @@ -41,7 +44,7 @@ const AddComment: React.FC<AddCommentProps> = ({ try { const token = await AsyncStorage.getItem('token'); if (!token) { - logout(); + dispatch(logout()); return; } const postedComment = await postMomentComment( diff --git a/src/components/comments/CommentTile.tsx b/src/components/comments/CommentTile.tsx index bee590f5..da78a4dc 100644 --- a/src/components/comments/CommentTile.tsx +++ b/src/components/comments/CommentTile.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {Text, View} from 'react-native-animatable'; import {ProfilePreview} from '../profile'; -import {CommentType} from '../../types'; +import {CommentType, ScreenType} from '../../types'; import {StyleSheet} from 'react-native'; import {getTimePosted} from '../../utils'; import ClockIcon from '../../assets/icons/clock-icon-01.svg'; @@ -12,9 +12,13 @@ import ClockIcon from '../../assets/icons/clock-icon-01.svg'; interface CommentTileProps { comment_object: CommentType; + screenType: ScreenType; } -const CommentTile: React.FC<CommentTileProps> = ({comment_object}) => { +const CommentTile: React.FC<CommentTileProps> = ({ + comment_object, + screenType, +}) => { const timePosted = getTimePosted(comment_object.date_time); return ( <View style={styles.container}> @@ -26,6 +30,7 @@ const CommentTile: React.FC<CommentTileProps> = ({comment_object}) => { last_name: '', }} previewType={'Comment'} + screenType={screenType} /> <View style={styles.body}> <Text style={styles.comment}>{comment_object.comment}</Text> diff --git a/src/components/comments/CommentsCount.tsx b/src/components/comments/CommentsCount.tsx index a9d5b6d6..d210c39a 100644 --- a/src/components/comments/CommentsCount.tsx +++ b/src/components/comments/CommentsCount.tsx @@ -3,6 +3,7 @@ import {Text} from 'react-native-animatable'; import {StyleSheet, TouchableOpacity} from 'react-native'; import CommentIcon from '../../assets/icons/moment-comment-icon.svg'; import {useNavigation} from '@react-navigation/native'; +import {ScreenType} from '../../types'; /** * Provides a view for the comment icon and the comment count. @@ -11,20 +12,20 @@ import {useNavigation} from '@react-navigation/native'; type CommentsCountProps = { comments_count: string; - isProfileView: boolean; moment_id: string; + screenType: ScreenType; }; const CommentsCount: React.FC<CommentsCountProps> = ({ comments_count, - isProfileView, moment_id, + screenType, }) => { const navigation = useNavigation(); const navigateToCommentsScreen = async () => { navigation.push('MomentCommentsScreen', { - isProfileView: isProfileView, - moment_id: moment_id, + moment_id, + screenType, }); }; return ( diff --git a/src/components/common/AvatarTitle.tsx b/src/components/common/AvatarTitle.tsx index 65ae7486..a38f46fa 100644 --- a/src/components/common/AvatarTitle.tsx +++ b/src/components/common/AvatarTitle.tsx @@ -2,12 +2,9 @@ import React from 'react'; import {Image, StyleSheet, View} from 'react-native'; import LinearGradient from 'react-native-linear-gradient'; import {TAGGS_GRADIENT} from '../../constants'; -import {AuthContext, ProfileContext} from '../../routes/'; -import {loadAvatar} from '../../services'; -import AsyncStorage from '@react-native-community/async-storage'; type AvatarTitleProps = { - avatar: string; + avatar: string | null; }; const AvatarTitle: React.FC<AvatarTitleProps> = ({avatar}) => { return ( diff --git a/src/components/common/TaggLoadingIndicator.tsx b/src/components/common/TaggLoadingIndicator.tsx new file mode 100644 index 00000000..cfb99e80 --- /dev/null +++ b/src/components/common/TaggLoadingIndicator.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import {ActivityIndicator, StyleSheet, View} from 'react-native'; + +type TaggLoadingIndicatorProps = { + color: string; +}; +const TaggLoadingIndicator: React.FC<TaggLoadingIndicatorProps> = ({color}) => { + return ( + <View style={[styles.container, styles.horizontal]}> + <ActivityIndicator size="large" color={color} /> + </View> + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + }, + horizontal: { + flexDirection: 'row', + justifyContent: 'space-around', + padding: 10, + }, +}); + +export default TaggLoadingIndicator; diff --git a/src/components/common/index.ts b/src/components/common/index.ts index 0feeaab8..f6521497 100644 --- a/src/components/common/index.ts +++ b/src/components/common/index.ts @@ -15,4 +15,4 @@ export {default as ComingSoon} from './ComingSoon'; export {default as PostCarousel} from './PostCarousel'; export {default as TaggDatePicker} from './TaggDatePicker'; export {default as BottomDrawer} from './BottomDrawer'; -export * from './post'; +export {default as TaggLoadingTndicator} from './TaggLoadingIndicator'; diff --git a/src/components/common/post/Post.tsx b/src/components/common/post/Post.tsx deleted file mode 100644 index 9fa167f2..00000000 --- a/src/components/common/post/Post.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import {StyleSheet, View, Image} from 'react-native'; -import {PostType} from '../../../types'; -import PostHeader from './PostHeader'; -import {SCREEN_WIDTH} from '../../../utils'; - -interface PostProps { - post: PostType; -} -const Post: React.FC<PostProps> = ({post: {owner, social, data}}) => { - return ( - <> - <PostHeader post={data} owner={owner} social={social} /> - <View style={styles.image}> - {data && <Image style={styles.image} source={{uri: data.media_url}} />} - </View> - </> - ); -}; - -const styles = StyleSheet.create({ - image: { - width: SCREEN_WIDTH, - height: SCREEN_WIDTH, - backgroundColor: '#eee', - }, -}); -export default Post; diff --git a/src/components/common/post/PostHeader.tsx b/src/components/common/post/PostHeader.tsx deleted file mode 100644 index 0e9c708b..00000000 --- a/src/components/common/post/PostHeader.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React from 'react'; -import {UserType, InstagramPostType} from '../../../types'; -import {View, StyleSheet, Image, Text} from 'react-native'; -import {AuthContext} from '../../../routes/authentication'; -import SocialIcon from '../SocialIcon'; -import moment from 'moment'; - -const AVATAR_DIM = 35; -interface PostHeaderProps { - owner: UserType; - post: InstagramPostType | undefined; - social: string; -} -const PostHeader: React.FC<PostHeaderProps> = ({ - owner: {username}, - post, - social, -}) => { - const {avatar} = React.useContext(AuthContext); - - return ( - <View style={styles.container}> - <View style={styles.topRow}> - <Image - style={styles.avatar} - source={ - avatar - ? {uri: avatar} - : require('../../../assets/images/avatar-placeholder.png') - } - /> - <Text style={styles.username}>{username}</Text> - {post && <SocialIcon style={styles.icon} social={social} />} - </View> - {post && ( - <Text style={styles.timestamp}> - {moment(post.timestamp).format('LL')} at{' '} - {moment(post.timestamp).format('LT')} - </Text> - )} - </View> - ); -}; - -const styles = StyleSheet.create({ - container: { - flexDirection: 'column', - justifyContent: 'space-between', - padding: 10, - backgroundColor: 'white', - }, - topRow: { - flexDirection: 'row', - alignItems: 'center', - }, - avatar: { - width: AVATAR_DIM, - height: AVATAR_DIM, - borderRadius: AVATAR_DIM / 2, - marginRight: 10, - }, - icon: { - width: AVATAR_DIM, - height: AVATAR_DIM, - borderRadius: AVATAR_DIM / 2, - marginLeft: '55%', - }, - username: { - fontSize: 18, - }, - timestamp: { - color: '#6A757D', - fontSize: 11, - marginLeft: AVATAR_DIM + 10, - }, -}); - -export default PostHeader; diff --git a/src/components/common/post/index.ts b/src/components/common/post/index.ts deleted file mode 100644 index 358a59d5..00000000 --- a/src/components/common/post/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {default as Post} from './Post'; diff --git a/src/components/moments/Moment.tsx b/src/components/moments/Moment.tsx index 9e138ef3..0c8febcf 100644 --- a/src/components/moments/Moment.tsx +++ b/src/components/moments/Moment.tsx @@ -10,15 +10,21 @@ import {TAGG_TEXT_LIGHT_BLUE} from '../../constants'; import {SCREEN_WIDTH} from '../../utils'; import ImagePicker from 'react-native-image-crop-picker'; import MomentTile from './MomentTile'; -import {MomentType} from 'src/types'; +import {MomentType, ScreenType} from 'src/types'; interface MomentProps { title: string; images: MomentType[] | undefined; - isProfileView: boolean; + userXId: string; + screenType: ScreenType; } -const Moment: React.FC<MomentProps> = ({title, images, isProfileView}) => { +const Moment: React.FC<MomentProps> = ({ + title, + images, + userXId, + screenType, +}) => { const navigation = useNavigation(); const navigateToImagePicker = () => { @@ -32,6 +38,7 @@ const Moment: React.FC<MomentProps> = ({title, images, isProfileView}) => { .then((picture) => { if ('path' in picture) { navigation.navigate('CaptionScreen', { + screenType, title: title, image: picture, }); @@ -45,7 +52,7 @@ const Moment: React.FC<MomentProps> = ({title, images, isProfileView}) => { <View style={styles.container}> <View style={styles.header}> <Text style={styles.titleText}>{title}</Text> - {!isProfileView ? ( + {!userXId ? ( <PlusIcon width={21} height={21} @@ -64,10 +71,11 @@ const Moment: React.FC<MomentProps> = ({title, images, isProfileView}) => { <MomentTile key={imageObj.moment_id} moment={imageObj} - isProfileView={isProfileView} + userXId={userXId} + screenType={screenType} /> ))} - {(images === undefined || images.length === 0) && !isProfileView && ( + {(images === undefined || images.length === 0) && !userXId && ( <TouchableOpacity onPress={() => navigateToImagePicker()}> <LinearGradient colors={['rgba(105, 141, 211, 1)', 'rgba(105, 141, 211, 0.3)']}> diff --git a/src/components/moments/MomentTile.tsx b/src/components/moments/MomentTile.tsx index 787957e0..cc24c531 100644 --- a/src/components/moments/MomentTile.tsx +++ b/src/components/moments/MomentTile.tsx @@ -1,26 +1,29 @@ import {useNavigation} from '@react-navigation/native'; import React from 'react'; import {StyleSheet, View, Image, TouchableOpacity} from 'react-native'; -import {MomentType} from 'src/types'; -import {ProfileContext} from '../../routes'; +import {MomentType, ScreenType} from 'src/types'; interface MomentTileProps { moment: MomentType; - isProfileView: boolean; + userXId: string; + screenType: ScreenType; } -const MomentTile: React.FC<MomentTileProps> = ({moment, isProfileView}) => { +const MomentTile: React.FC<MomentTileProps> = ({ + moment, + userXId, + screenType, +}) => { const navigation = useNavigation(); - //Username is needed by the IndividualMoment screen - const { - user: {username}, - } = React.useContext(ProfileContext); - const {path_hash} = moment; return ( <TouchableOpacity onPress={() => { - navigation.push('IndividualMoment', {moment, isProfileView, username}); + navigation.push('IndividualMoment', { + moment, + screenType, + userXId, + }); }}> <View style={styles.image}> <Image style={styles.image} source={{uri: path_hash}} /> diff --git a/src/components/profile/Avatar.tsx b/src/components/profile/Avatar.tsx index aca3bf4d..903d0d18 100644 --- a/src/components/profile/Avatar.tsx +++ b/src/components/profile/Avatar.tsx @@ -1,16 +1,20 @@ -import React from 'react'; +import React, {useContext} from 'react'; import {Image, StyleSheet} from 'react-native'; -import {AuthContext, ProfileContext} from '../../routes/'; +import {useSelector} from 'react-redux'; +import {RootState} from '../../store/rootreducer'; +import {ScreenType} from '../../types'; const PROFILE_DIM = 100; interface AvatarProps { style: object; - isProfileView: boolean; + userXId: string; + screenType: ScreenType; } -const Avatar: React.FC<AvatarProps> = ({style, isProfileView}) => { - const {avatar} = isProfileView - ? React.useContext(ProfileContext) - : React.useContext(AuthContext); +const Avatar: React.FC<AvatarProps> = ({style, screenType, userXId}) => { + const {avatar} = userXId + ? useSelector((state: RootState) => state.userX[screenType][userXId]) + : useSelector((state: RootState) => state.user); + return ( <Image style={[styles.image, style]} diff --git a/src/components/profile/Content.tsx b/src/components/profile/Content.tsx index 13db60a5..73f6fad3 100644 --- a/src/components/profile/Content.tsx +++ b/src/components/profile/Content.tsx @@ -1,9 +1,13 @@ -import AsyncStorage from '@react-native-community/async-storage'; -import React, {useCallback, useEffect, useState} from 'react'; +import React, {useCallback, useEffect, useState, useContext} from 'react'; import {LayoutChangeEvent, StyleSheet, View} from 'react-native'; import Animated from 'react-native-reanimated'; -import {AuthContext, ProfileContext} from '../../routes/'; -import {MomentType} from 'src/types'; +import { + MomentType, + ProfilePreviewType, + ProfileType, + ScreenType, + UserType, +} from '../../types'; import {defaultMoments} from '../../constants'; import {SCREEN_HEIGHT} from '../../utils'; import TaggsBar from '../taggs/TaggsBar'; @@ -11,26 +15,49 @@ import {Moment} from '../moments'; import ProfileBody from './ProfileBody'; import ProfileCutout from './ProfileCutout'; import ProfileHeader from './ProfileHeader'; -import {followOrUnfollowUser, blockOrUnblockUser} from '../../services'; +import {useDispatch, useSelector, useStore} from 'react-redux'; +import {RootState} from '../../store/rootreducer'; +import { + followUnfollowUser, + blockUnblockUser, + loadFollowData, + updateUserXFollowersAndFollowing, +} from '../../store/actions'; +import { + NO_USER, + NO_PROFILE, + EMPTY_PROFILE_PREVIEW_LIST, + EMPTY_MOMENTS_LIST, +} from '../../store/initialStates'; interface ContentProps { y: Animated.Value<number>; - isProfileView: boolean; + userXId: string; + screenType: ScreenType; } -const Content: React.FC<ContentProps> = ({y, isProfileView}) => { - const [profileBodyHeight, setProfileBodyHeight] = useState(0); - const {user, moments, followers, following, updateFollowers} = isProfileView - ? React.useContext(ProfileContext) - : React.useContext(AuthContext); - - const { - logout, - user: loggedInUser, - updateFollowers: updateLoggedInUserFollowers, - blockedUsers, - updateBlockedUsers, - } = React.useContext(AuthContext); +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 {followers = EMPTY_PROFILE_PREVIEW_LIST} = userXId + ? useSelector((state: RootState) => state.userX[screenType][userXId]) + : useSelector((state: RootState) => state.follow); + + const {moments = EMPTY_MOMENTS_LIST} = userXId + ? useSelector((state: RootState) => state.userX[screenType][userXId]) + : useSelector((state: RootState) => state.moments); + + 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(); /** * States @@ -38,8 +65,9 @@ const Content: React.FC<ContentProps> = ({y, isProfileView}) => { const [imagesMap, setImagesMap] = useState<Map<string, MomentType[]>>( new Map(), ); - const [isFollowed, setIsFollowed] = React.useState<boolean>(false); - const [isBlocked, setIsBlocked] = React.useState<boolean>(false); + const [isFollowed, setIsFollowed] = useState<boolean>(false); + const [isBlocked, setIsBlocked] = useState<boolean>(false); + const [profileBodyHeight, setProfileBodyHeight] = useState(0); /** * If own profile is being viewed then do not show the follow button. @@ -51,8 +79,6 @@ const Content: React.FC<ContentProps> = ({y, isProfileView}) => { setProfileBodyHeight(height); }; - const {userId} = user; - const createImagesMap = useCallback(() => { var map = new Map(); moments.forEach(function (imageObject) { @@ -68,9 +94,6 @@ const Content: React.FC<ContentProps> = ({y, isProfileView}) => { }, [moments]); useEffect(() => { - if (!userId) { - return; - } createImagesMap(); }, [createImagesMap]); @@ -78,9 +101,6 @@ const Content: React.FC<ContentProps> = ({y, isProfileView}) => { * This hook is called on load of profile and when you update the followers list. */ useEffect(() => { - if (!userId) { - return; - } const isActuallyFollowed = followers.some( (follower) => follower.username === loggedInUser.username, ); @@ -90,62 +110,63 @@ const Content: React.FC<ContentProps> = ({y, isProfileView}) => { }, [followers]); useEffect(() => { - if (!userId) { - return; - } - const isActuallyBlocked = blockedUsers.some( (cur_user) => user.username === cur_user.username, ); if (isBlocked != isActuallyBlocked) { setIsBlocked(isActuallyBlocked); } - }, [blockedUsers]); + }, [blockedUsers, user]); + + /** + * The object returned by this method is added to the list of blocked / followed users by the reducer. + * Which helps us prevent an extra api call to the backend just to fetch a user. + */ + const getUserAsProfilePreviewType = ( + passedInUser: UserType, + passedInProfile: ProfileType, + ): ProfilePreviewType => { + const fullName = passedInProfile.name.split(' '); + return { + id: passedInUser.userId, + username: passedInUser.username, + first_name: fullName[0], + last_name: fullName[1], + }; + }; /** * Handles a click on the follow / unfollow button. - * updateFollowers and updateLoggedInUerFollowers to make sure that we update followers list / count for both the users in context. + * followUnfollowUser takes care of updating the following list for loggedInUser + * updateUserXFollowersAndFollowing updates followers and following list for the followed user. */ + const handleFollowUnfollow = async () => { - const token = await AsyncStorage.getItem('token'); - if (!token) { - logout(); - return; - } - const isUpdatedSuccessful = await followOrUnfollowUser( - loggedInUser.userId, - userId, - token, - isFollowed, + await dispatch( + followUnfollowUser( + loggedInUser, + getUserAsProfilePreviewType(user, profile), + isFollowed, + ), ); - if (isUpdatedSuccessful) { - setIsFollowed(!isFollowed); - updateFollowers(true); - updateLoggedInUserFollowers(true); - } + await dispatch(updateUserXFollowersAndFollowing(user.userId, state)); }; /** * Handles a click on the block / unblock button. + * loadFollowData updates followers / following list for the logged in user + * updateUserXFollowersAndFollowing updates followers and following list for the followed user. */ const handleBlockUnblock = async () => { - const token = await AsyncStorage.getItem('token'); - if (!token) { - logout(); - return; - } - const isUpdatedSuccessful = await blockOrUnblockUser( - loggedInUser.userId, - userId, - token, - isBlocked, + await dispatch( + blockUnblockUser( + loggedInUser, + getUserAsProfilePreviewType(user, profile), + isBlocked, + ), ); - if (isUpdatedSuccessful) { - setIsBlocked(!isBlocked); - updateBlockedUsers(true); - updateFollowers(true); - updateLoggedInUserFollowers(true); - } + await dispatch(loadFollowData(loggedInUser.userId)); + await dispatch(updateUserXFollowersAndFollowing(user.userId, state)); }; return ( @@ -155,15 +176,12 @@ const Content: React.FC<ContentProps> = ({y, isProfileView}) => { showsVerticalScrollIndicator={false} scrollEventThrottle={1}> <ProfileCutout /> - <ProfileHeader - isProfileView={isProfileView} - numFollowing={following.length} - numFollowers={followers.length} - /> + <ProfileHeader {...{userXId, screenType}} /> <ProfileBody {...{ onLayout, - isProfileView, + userXId, + screenType, isOwnProfile, isFollowed, handleFollowUnfollow, @@ -171,14 +189,15 @@ const Content: React.FC<ContentProps> = ({y, isProfileView}) => { handleBlockUnblock, }} /> - <TaggsBar {...{y, profileBodyHeight, isProfileView}} /> + <TaggsBar {...{y, profileBodyHeight, userXId, screenType}} /> <View style={styles.momentsContainer}> {defaultMoments.map((title, index) => ( <Moment key={index} title={title} images={imagesMap.get(title)} - isProfileView={isProfileView} + userXId={userXId} + screenType={screenType} /> ))} </View> diff --git a/src/components/profile/Cover.tsx b/src/components/profile/Cover.tsx index 36e41776..3c0f7045 100644 --- a/src/components/profile/Cover.tsx +++ b/src/components/profile/Cover.tsx @@ -1,18 +1,23 @@ -import React from 'react'; +import React, {useContext} from 'react'; import {Image, StyleSheet} from 'react-native'; import Animated from 'react-native-reanimated'; import {IMAGE_WIDTH, COVER_HEIGHT, IMAGE_HEIGHT} from '../../constants'; -import {AuthContext, ProfileContext} from '../../routes/'; +import {useSelector, useStore} from 'react-redux'; +import {RootState} from '../../store/rootreducer'; +import {ScreenType} from '../../types'; +import {DUMMY_USERID, NO_USER_DATA} from '../../store/initialStates'; const {interpolate, Extrapolate} = Animated; interface CoverProps { y: Animated.Value<number>; - isProfileView: boolean; + userXId: string; + screenType: ScreenType; } -const Cover: React.FC<CoverProps> = ({y, isProfileView}) => { - const {cover} = isProfileView - ? React.useContext(ProfileContext) - : React.useContext(AuthContext); +const Cover: React.FC<CoverProps> = ({y, userXId, screenType}) => { + const {cover = ''} = userXId + ? useSelector((state: RootState) => state.userX[screenType][userXId]) + : useSelector((state: RootState) => state.user); + const scale: Animated.Node<number> = interpolate(y, { inputRange: [-COVER_HEIGHT, 0], outputRange: [1.5, 1.25], diff --git a/src/components/profile/FollowCount.tsx b/src/components/profile/FollowCount.tsx index 3e270428..a23a3533 100644 --- a/src/components/profile/FollowCount.tsx +++ b/src/components/profile/FollowCount.tsx @@ -1,24 +1,33 @@ -import React from 'react'; +import React, {useContext} from 'react'; import {View, Text, StyleSheet, ViewProps} from 'react-native'; import {TouchableOpacity} from 'react-native-gesture-handler'; import {useNavigation} from '@react-navigation/native'; -import {AuthContext, ProfileContext} from '../../routes'; +import {RootState} from '../../store/rootReducer'; +import {useSelector} from 'react-redux'; +import {ScreenType} from '../../types'; +import {EMPTY_PROFILE_PREVIEW_LIST} from '../../store/initialStates'; interface FollowCountProps extends ViewProps { mode: 'followers' | 'following'; - count: number; - isProfileView: boolean; + userXId: string; + screenType: ScreenType; } const FollowCount: React.FC<FollowCountProps> = ({ style, mode, - count, - isProfileView, + userXId, + screenType, }) => { - const {followers, following} = isProfileView - ? React.useContext(ProfileContext) - : React.useContext(AuthContext); + const { + followers = EMPTY_PROFILE_PREVIEW_LIST, + following = EMPTY_PROFILE_PREVIEW_LIST, + } = userXId + ? useSelector((state: RootState) => state.userX[screenType][userXId]) + : useSelector((state: RootState) => state.follow); + + const isFollowers = mode === 'followers'; + const count = isFollowers ? followers.length : following.length; const navigation = useNavigation(); const displayed: string = @@ -33,14 +42,15 @@ const FollowCount: React.FC<FollowCountProps> = ({ <TouchableOpacity onPress={() => navigation.push('FollowersListScreen', { - isFollowers: mode === 'followers', - list: mode === 'followers' ? followers : following, + isFollowers, + userXId, + screenType, }) }> <View style={[styles.container, style]}> <Text style={styles.count}>{displayed}</Text> <Text style={styles.label}> - {mode === 'followers' ? 'Followers' : 'Following'} + {isFollowers ? 'Followers' : 'Following'} </Text> </View> </TouchableOpacity> diff --git a/src/components/profile/Followers.tsx b/src/components/profile/Followers.tsx index e11041d0..c665603d 100644 --- a/src/components/profile/Followers.tsx +++ b/src/components/profile/Followers.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {View, StyleSheet, ViewProps, Text} from 'react-native'; -import {ProfilePreviewType} from '../../types'; +import {ProfilePreviewType, ScreenType} from '../../types'; import {ProfilePreview} from '..'; import {useNavigation} from '@react-navigation/native'; import {Button} from 'react-native-elements'; @@ -8,9 +8,14 @@ import {Button} from 'react-native-elements'; interface FollowersListProps { result: Array<ProfilePreviewType>; sectionTitle: string; + screenType: ScreenType; } -const Followers: React.FC<FollowersListProps> = ({result, sectionTitle}) => { +const Followers: React.FC<FollowersListProps> = ({ + result, + sectionTitle, + screenType, +}) => { const navigation = useNavigation(); return ( <> @@ -31,6 +36,7 @@ const Followers: React.FC<FollowersListProps> = ({result, sectionTitle}) => { key={profilePreview.id} {...{profilePreview}} previewType={'Comment'} + screenType={screenType} /> ))} </> diff --git a/src/components/profile/MoreInfoDrawer.tsx b/src/components/profile/MoreInfoDrawer.tsx index 719c1894..a8908b4d 100644 --- a/src/components/profile/MoreInfoDrawer.tsx +++ b/src/components/profile/MoreInfoDrawer.tsx @@ -4,14 +4,14 @@ import {StyleSheet, Text, TouchableOpacity, View} from 'react-native'; import {useSafeAreaInsets} from 'react-native-safe-area-context'; import {TAGG_TEXT_LIGHT_BLUE} from '../../constants'; import {SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils'; -import {AuthContext} from '../../routes'; import {BottomDrawer} from '../common'; import PersonOutline from '../../assets/ionicons/person-outline.svg'; +import {useSelector} from 'react-redux'; +import {RootState} from '../../store/rootreducer'; interface MoreInfoDrawerProps { isOpen: boolean; setIsOpen: (visible: boolean) => void; - isProfileView: boolean; } const MoreInfoDrawer: React.FC<MoreInfoDrawerProps> = (props) => { @@ -20,7 +20,7 @@ const MoreInfoDrawer: React.FC<MoreInfoDrawerProps> = (props) => { const navigation = useNavigation(); const { user: {userId, username}, - } = useContext(AuthContext); + } = useSelector((state: RootState) => state.user); const goToEditProfile = () => { navigation.push('EditProfile', { diff --git a/src/components/profile/ProfileBody.tsx b/src/components/profile/ProfileBody.tsx index c0253533..3c05fc26 100644 --- a/src/components/profile/ProfileBody.tsx +++ b/src/components/profile/ProfileBody.tsx @@ -1,33 +1,38 @@ -import React from 'react'; +import React, {useContext} from 'react'; import {StyleSheet, View, Text, LayoutChangeEvent} from 'react-native'; import {TAGG_DARK_BLUE, TOGGLE_BUTTON_TYPE} from '../../constants'; -import {AuthContext, ProfileContext} from '../../routes/'; import ToggleButton from './ToggleButton'; +import {RootState} from '../../store/rootReducer'; +import {useSelector} from 'react-redux'; +import {ScreenType} from '../../types'; +import {NO_PROFILE} from '../../store/initialStates'; interface ProfileBodyProps { onLayout: (event: LayoutChangeEvent) => void; - isProfileView: boolean; isFollowed: boolean; isBlocked: boolean; isOwnProfile: boolean; handleFollowUnfollow: Function; handleBlockUnblock: Function; + userXId: string; + screenType: ScreenType; } const ProfileBody: React.FC<ProfileBodyProps> = ({ onLayout, - isProfileView, isFollowed, isBlocked, isOwnProfile, handleFollowUnfollow, handleBlockUnblock, + userXId, + screenType, }) => { const { - profile, + profile = NO_PROFILE, user: {username}, - } = isProfileView - ? React.useContext(ProfileContext) - : React.useContext(AuthContext); + } = userXId + ? useSelector((state: RootState) => state.userX[screenType][userXId]) + : useSelector((state: RootState) => state.user); const {biography, website} = profile; @@ -36,7 +41,7 @@ const ProfileBody: React.FC<ProfileBodyProps> = ({ <Text style={styles.username}>{`@${username}`}</Text> <Text style={styles.biography}>{`${biography}`}</Text> <Text style={styles.website}>{`${website}`}</Text> - {isProfileView && !isOwnProfile ? ( + {userXId && !isOwnProfile ? ( <View style={styles.toggleButtonContainer}> {!isBlocked && ( <ToggleButton diff --git a/src/components/profile/ProfileHeader.tsx b/src/components/profile/ProfileHeader.tsx index 62949746..621aae9a 100644 --- a/src/components/profile/ProfileHeader.tsx +++ b/src/components/profile/ProfileHeader.tsx @@ -1,34 +1,30 @@ -import React, {useState} from 'react'; +import React, {useState, useContext} from 'react'; import {StyleSheet, Text, TouchableOpacity, View} from 'react-native'; import MoreIcon from '../../assets/icons/more_horiz-24px.svg'; import {TAGG_DARK_BLUE} from '../../constants'; -import {AuthContext, ProfileContext} from '../../routes/'; import {SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils'; import Avatar from './Avatar'; import MoreInfoDrawer from './MoreInfoDrawer'; import FollowCount from './FollowCount'; +import {useSelector} from 'react-redux'; +import {RootState} from '../../store/rootreducer'; +import {ScreenType} from '../../types'; type ProfileHeaderProps = { - isProfileView: boolean; - numFollowing: number; - numFollowers: number; + userXId: string; + screenType: ScreenType; }; -const ProfileHeader: React.FC<ProfileHeaderProps> = ({ - isProfileView, - numFollowing, - numFollowers, -}) => { - const { - profile: {name}, - } = isProfileView - ? React.useContext(ProfileContext) - : React.useContext(AuthContext); +const ProfileHeader: React.FC<ProfileHeaderProps> = ({userXId, screenType}) => { + const {profile: {name = ''} = {}} = userXId + ? useSelector((state: RootState) => state.userX[screenType][userXId]) + : useSelector((state: RootState) => state.user); + const [drawerVisible, setDrawerVisible] = useState(false); return ( <View style={styles.container}> - {!isProfileView && ( + {!userXId && ( <> <TouchableOpacity style={styles.more} @@ -37,29 +33,29 @@ const ProfileHeader: React.FC<ProfileHeaderProps> = ({ }}> <MoreIcon height={30} width={30} color={TAGG_DARK_BLUE} /> </TouchableOpacity> - <MoreInfoDrawer - isOpen={drawerVisible} - setIsOpen={setDrawerVisible} - isProfileView={isProfileView} - /> + <MoreInfoDrawer isOpen={drawerVisible} setIsOpen={setDrawerVisible} /> </> )} <View style={styles.row}> - <Avatar style={styles.avatar} isProfileView={isProfileView} /> + <Avatar + style={styles.avatar} + userXId={userXId} + screenType={screenType} + /> <View style={styles.header}> <Text style={styles.name}>{name}</Text> <View style={styles.row}> <FollowCount style={styles.follows} mode="followers" - count={numFollowers} - isProfileView={isProfileView} + screenType={screenType} + userXId={userXId} /> <FollowCount style={styles.follows} mode="following" - count={numFollowing} - isProfileView={isProfileView} + screenType={screenType} + userXId={userXId} /> </View> </View> diff --git a/src/components/profile/ProfilePreview.tsx b/src/components/profile/ProfilePreview.tsx index af116dd3..5567fa5a 100644 --- a/src/components/profile/ProfilePreview.tsx +++ b/src/components/profile/ProfilePreview.tsx @@ -1,5 +1,5 @@ import React, {useEffect, useState, useContext} from 'react'; -import {ProfilePreviewType} from '../../types'; +import {ProfilePreviewType, ScreenType} from '../../types'; import { View, Text, @@ -14,8 +14,11 @@ import RNFetchBlob from 'rn-fetch-blob'; import AsyncStorage from '@react-native-community/async-storage'; import {AVATAR_PHOTO_ENDPOINT} from '../../constants'; import {UserType, PreviewType} from '../../types'; -import {AuthContext} from '../../routes/'; import {isUserBlocked} from '../../services'; +import {useSelector, useDispatch, useStore} from 'react-redux'; +import {RootState} from '../../store/rootreducer'; +import {loadUserX, logout} from '../../store/actions'; +import {userXInStore} from '../../utils'; const NO_USER: UserType = { userId: '', username: '', @@ -27,22 +30,24 @@ const NO_USER: UserType = { * If isComment is true then it means that we are not displaying this tile as a part of search results. * And hence we do not cache the search results. * On the other hand, if isComment is false, then we should update the search cache. (This cache needs to be revamped to clear outdated results.) - * In either case, we update the userBeingVisited in our AuthContext (Which can be used to make api calls later on to fetch user specific data). * Finally, We navigate to Profile. */ interface ProfilePreviewProps extends ViewProps { profilePreview: ProfilePreviewType; previewType: PreviewType; + screenType: ScreenType; } const ProfilePreview: React.FC<ProfilePreviewProps> = ({ profilePreview: {username, first_name, last_name, id}, previewType, + screenType, }) => { const navigation = useNavigation(); - const {user: loggedInUser, logout} = React.useContext(AuthContext); + const {user: loggedInUser} = useSelector((state: RootState) => state.user); const [avatarURI, setAvatarURI] = useState<string | null>(null); const [user, setUser] = useState<UserType>(NO_USER); + const dispatch = useDispatch(); useEffect(() => { let mounted = true; const loadAvatar = async () => { @@ -87,12 +92,14 @@ const ProfilePreview: React.FC<ProfilePreviewProps> = ({ const checkIfUserIsBlocked = async (userId: string) => { const token = await AsyncStorage.getItem('token'); if (!token) { - logout(); + dispatch(logout()); return false; } return await isUserBlocked(userId, loggedInUser.userId, token); }; + const state: RootState = useStore().getState(); + const addToRecentlyStoredAndNavigateToProfile = async () => { let user: ProfilePreviewType = { id, @@ -145,12 +152,19 @@ const ProfilePreview: React.FC<ProfilePreviewProps> = ({ } /** - * Navigate to profile of the user selected + * Dispatch an event to Fetch the user details + * If the user is already present in store, do not fetch again + * Finally, Navigate to profile of the user selected */ + if (!userXInStore(state, screenType, user.id)) { + dispatch( + loadUserX({userId: user.id, username: user.username}, screenType), + ); + } navigation.push('Profile', { - isProfileView: true, username: user.username, - userId: user.id, + userXId: user.id, + screenType: screenType, }); } catch (e) { console.log(e); diff --git a/src/components/search/DiscoverUsers.tsx b/src/components/search/DiscoverUsers.tsx index 885c712b..ec0a8daa 100644 --- a/src/components/search/DiscoverUsers.tsx +++ b/src/components/search/DiscoverUsers.tsx @@ -6,23 +6,32 @@ import { StyleSheet, TouchableOpacityProps, } from 'react-native'; -import {ProfilePreviewType} from '../../types'; +import {PreviewType, ProfilePreviewType, ScreenType} from '../../types'; import SearchResults from './SearchResults'; interface DiscoverUsersProps extends TouchableOpacityProps { - sectionTitle: string; + sectionTitle: PreviewType; users: Array<ProfilePreviewType>; + screenType: ScreenType; } /** * An image component that returns the <Image> of the icon for a specific social media platform. */ -const DiscoverUsers: React.FC<DiscoverUsersProps> = (props) => { +const DiscoverUsers: React.FC<DiscoverUsersProps> = ({ + sectionTitle, + screenType, + users, +}) => { return ( <View style={styles.container}> <View style={styles.headerContainer}> - <Text style={styles.title}>{props.sectionTitle}</Text> + <Text style={styles.title}>{sectionTitle}</Text> </View> - <SearchResults results={props.users} previewType={props.sectionTitle} /> + <SearchResults + results={users} + previewType={sectionTitle} + screenType={screenType} + /> </View> ); }; diff --git a/src/components/search/RecentSearches.tsx b/src/components/search/RecentSearches.tsx index 6a98e49a..22a36a6b 100644 --- a/src/components/search/RecentSearches.tsx +++ b/src/components/search/RecentSearches.tsx @@ -6,14 +6,15 @@ import { StyleSheet, TouchableOpacityProps, } from 'react-native'; -import {ProfilePreviewType} from 'src/types'; -import { TAGG_TEXT_LIGHT_BLUE } from '../../constants'; +import {PreviewType, ProfilePreviewType, ScreenType} from 'src/types'; +import {TAGG_TEXT_LIGHT_BLUE} from '../../constants'; import SearchResults from './SearchResults'; interface RecentSearchesProps extends TouchableOpacityProps { - sectionTitle: string; + sectionTitle: PreviewType; sectionButtonTitle: string; recents: Array<ProfilePreviewType>; + screenType: ScreenType; } /** * An image component that returns the <Image> of the icon for a specific social media platform. @@ -29,7 +30,11 @@ const RecentSearches: React.FC<RecentSearchesProps> = (props) => { </TouchableOpacity> )} </View> - <SearchResults results={props.recents} previewType={props.sectionTitle} /> + <SearchResults + results={props.recents} + previewType={props.sectionTitle} + screenType={props.screenType} + /> </> ); }; diff --git a/src/components/search/SearchResults.tsx b/src/components/search/SearchResults.tsx index 2d5c9db8..001c7968 100644 --- a/src/components/search/SearchResults.tsx +++ b/src/components/search/SearchResults.tsx @@ -1,20 +1,26 @@ import React from 'react'; -import {ProfilePreviewType, PreviewType} from '../../types'; +import {ProfilePreviewType, PreviewType, ScreenType} from '../../types'; import ProfilePreview from '../profile/ProfilePreview'; import {StyleSheet, View} from 'react-native'; interface SearchResultsProps { results: Array<ProfilePreviewType>; previewType: PreviewType; + screenType: ScreenType; } -const SearchResults: React.FC<SearchResultsProps> = (props) => { +const SearchResults: React.FC<SearchResultsProps> = ({ + results, + previewType, + screenType, +}) => { return ( <View style={styles.container}> - {props.results.map((profilePreview) => ( + {results.map((profilePreview) => ( <ProfilePreview style={styles.result} key={profilePreview.id} {...{profilePreview}} - previewType={props.previewType} + previewType={previewType} + screenType={screenType} /> ))} </View> diff --git a/src/components/taggs/Tagg.tsx b/src/components/taggs/Tagg.tsx index 9f8fafd1..086b3c87 100644 --- a/src/components/taggs/Tagg.tsx +++ b/src/components/taggs/Tagg.tsx @@ -1,5 +1,5 @@ import {useNavigation} from '@react-navigation/native'; -import React, {Fragment, useContext, useState} from 'react'; +import React, {Fragment, useContext, useEffect, useState} from 'react'; import {Alert, Linking, StyleSheet, TouchableOpacity, View} from 'react-native'; import PurpleRingPlus from '../../assets/icons/purple_ring+.svg'; import PurpleRing from '../../assets/icons/purple_ring.svg'; @@ -17,36 +17,35 @@ import { registerNonIntegratedSocialLink, } from '../../services'; import {SmallSocialIcon, SocialIcon, SocialLinkModal} from '../common'; -import {AuthContext, ProfileContext} from '../../routes'; +import {useSelector} from 'react-redux'; +import {RootState} from '../../store/rootreducer'; +import {ScreenType} from '../../types'; interface TaggProps { social: string; - isProfileView: boolean; isLinked: boolean; isIntegrated: boolean; setTaggsNeedUpdate: (_: boolean) => void; - setSocialDataNeedUpdate: (_: string[]) => void; - userId: string; + setSocialDataNeedUpdate: (_: string) => void; + userXId: string; + screenType: ScreenType; } const Tagg: React.FC<TaggProps> = ({ social, - isProfileView, isLinked, isIntegrated, setTaggsNeedUpdate, setSocialDataNeedUpdate, - userId, + userXId, + screenType, }) => { const navigation = useNavigation(); const [modalVisible, setModalVisible] = useState(false); - const youMayPass = isLinked || isProfileView; - const { - profile: {name}, - socialAccounts, - avatar, - } = isProfileView ? useContext(ProfileContext) : useContext(AuthContext); - + const youMayPass = isLinked || userXId; + const {user} = userXId + ? useSelector((state: RootState) => state.userX[screenType][userXId]) + : useSelector((state: RootState) => state.user); /* case isProfileView: case linked: @@ -62,7 +61,7 @@ const Tagg: React.FC<TaggProps> = ({ show auth browser case !integrated_social: show modal - Tagg's "Tagg" will use the Ring instead of PurpleRing + Tagg's "Tagg" will use the Ring instead of PurpleRing */ const modalOrAuthBrowserOrPass = async () => { @@ -70,14 +69,10 @@ const Tagg: React.FC<TaggProps> = ({ if (INTEGRATED_SOCIAL_LIST.indexOf(social) !== -1) { navigation.push('SocialMediaTaggs', { socialMediaType: social, - isProfileView: isProfileView, - userId: userId, - name: name, - accountData: socialAccounts[social], - avatar: avatar, + userXId, }); } else { - getNonIntegratedURL(social, userId).then((socialURL) => { + getNonIntegratedURL(social, user.userId).then((socialURL) => { if (socialURL) { Linking.openURL(socialURL); } else { @@ -89,7 +84,7 @@ const Tagg: React.FC<TaggProps> = ({ if (isIntegrated) { handlePressForAuthBrowser(social).then((success) => { setTaggsNeedUpdate(success); - setSocialDataNeedUpdate(success ? [social] : []); + if (success) setSocialDataNeedUpdate(social); }); } else { setModalVisible(true); @@ -127,7 +122,7 @@ const Tagg: React.FC<TaggProps> = ({ return ( <> - {isProfileView && !isLinked ? ( + {userXId && !isLinked ? ( <Fragment /> ) : ( <> diff --git a/src/components/taggs/TaggsBar.tsx b/src/components/taggs/TaggsBar.tsx index aac68e99..12e4b93a 100644 --- a/src/components/taggs/TaggsBar.tsx +++ b/src/components/taggs/TaggsBar.tsx @@ -1,35 +1,53 @@ -// @refresh react -import React, {useEffect, useState} from 'react'; +import React, {useEffect, useState, useContext} from 'react'; import {StyleSheet} from 'react-native'; import Animated from 'react-native-reanimated'; +import {useDispatch, useSelector} from 'react-redux'; import { INTEGRATED_SOCIAL_LIST, PROFILE_CUTOUT_BOTTOM_Y, SOCIAL_LIST, } from '../../constants'; -import {AuthContext, ProfileContext} from '../../routes'; import {getLinkedSocials} from '../../services'; import {StatusBarHeight} from '../../utils'; import Tagg from './Tagg'; +import {RootState} from '../../store/rootReducer'; +import {ScreenType} from '../../types'; +import {loadIndividualSocial} from '../../store/actions'; const {View, ScrollView, interpolate, Extrapolate} = Animated; interface TaggsBarProps { y: Animated.Value<number>; profileBodyHeight: number; - isProfileView: boolean; + userXId: string; + screenType: ScreenType; } const TaggsBar: React.FC<TaggsBarProps> = ({ y, profileBodyHeight, - isProfileView, + userXId, + screenType, }) => { let [taggs, setTaggs] = useState<Object[]>([]); let [taggsNeedUpdate, setTaggsNeedUpdate] = useState(true); - const context = isProfileView - ? React.useContext(ProfileContext) - : React.useContext(AuthContext); - const {user, socialsNeedUpdate} = context; + const {user} = userXId + ? useSelector((state: RootState) => state.userX[screenType][userXId]) + : useSelector((state: RootState) => state.user); + + const dispatch = useDispatch(); + + /** + * Updates the individual social that needs update + * @param socialType Type of the social that needs update + */ + const handleSocialUpdate = (socialType: string) => { + dispatch(loadIndividualSocial(user.userId, socialType)); + }; + + /** + * This useEffect should be called evey time the user being viewed is changed OR + * And update is triggered manually + */ useEffect(() => { const loadData = async () => { getLinkedSocials(user.userId).then((linkedSocials) => { @@ -43,12 +61,12 @@ const TaggsBar: React.FC<TaggsBarProps> = ({ <Tagg key={i} social={social} - isProfileView={isProfileView} + userXId={userXId} + screenType={screenType} isLinked={true} isIntegrated={INTEGRATED_SOCIAL_LIST.indexOf(social) !== -1} setTaggsNeedUpdate={setTaggsNeedUpdate} - setSocialDataNeedUpdate={socialsNeedUpdate} - userId={user.userId} + setSocialDataNeedUpdate={handleSocialUpdate} />, ); i++; @@ -58,12 +76,12 @@ const TaggsBar: React.FC<TaggsBarProps> = ({ <Tagg key={i} social={social} - isProfileView={isProfileView} + userXId={userXId} + screenType={screenType} isLinked={false} isIntegrated={INTEGRATED_SOCIAL_LIST.indexOf(social) !== -1} setTaggsNeedUpdate={setTaggsNeedUpdate} - setSocialDataNeedUpdate={socialsNeedUpdate} - userId={user.userId} + setSocialDataNeedUpdate={handleSocialUpdate} />, ); i++; @@ -73,17 +91,8 @@ const TaggsBar: React.FC<TaggsBarProps> = ({ }); }; - if (taggsNeedUpdate) { - /** - * Triggering redundant call to the backend for now to make the app work. - * TODO : Figure out a better way to get the updates social posts for the profile being visited. - * Have an event triggered from ProfileProvider based on which we could make a call to backedn to get updated posts. - */ - //We may need the line below in future ? - // socialsNeedUpdate(INTEGRATED_SOCIAL_LIST); - loadData(); - } - }, [isProfileView, taggsNeedUpdate, user.userId]); + loadData(); + }, [taggsNeedUpdate, user]); const shadowOpacity: Animated.Node<number> = interpolate(y, { inputRange: [ diff --git a/src/routes/Routes.tsx b/src/routes/Routes.tsx index 92cd3dd2..e54f038d 100644 --- a/src/routes/Routes.tsx +++ b/src/routes/Routes.tsx @@ -1,13 +1,29 @@ -import React from 'react'; - -import {AuthContext} from './authentication'; +import React, {useEffect} from 'react'; import NavigationBar from './tabs'; import Onboarding from './onboarding'; +import {useSelector, useDispatch} from 'react-redux'; +import {RootState} from '../store/rootReducer'; +import {userLogin} from '../utils'; const Routes: React.FC = () => { const { user: {userId}, - } = React.useContext(AuthContext); + } = useSelector((state: RootState) => state.user); + const dispatch = useDispatch(); + + /** + * Load the user from AsyncStorage if any + * Note that this makes logout triggered by invalid Token have no effect. + * We should figure out a way to handle that. + * Suggestions? + * NOTE : Not something introduced by this commit but something we already have. + */ + useEffect(() => { + if (!userId) { + userLogin(dispatch, {userId: '', username: ''}); + } + }, [userId, userLogin]); + return userId ? <NavigationBar /> : <Onboarding />; }; diff --git a/src/routes/authentication/AuthProvider.tsx b/src/routes/authentication/AuthProvider.tsx deleted file mode 100644 index 46f761e1..00000000 --- a/src/routes/authentication/AuthProvider.tsx +++ /dev/null @@ -1,315 +0,0 @@ -import AsyncStorage from '@react-native-community/async-storage'; -import React, {createContext, useEffect, useState} from 'react'; -import {INTEGRATED_SOCIAL_LIST} from '../../constants'; -import { - loadAvatar, - loadCover, - loadFollowers, - loadFollowing, - loadMoments, - loadProfileInfo, - loadRecentlySearchedUsers, - loadSocialPosts, - getAllTaggUsers, - loadBlockedUsers, -} from '../../services'; -import { - MomentType, - ProfilePreviewType, - ProfileType, - SocialAccountType, - UserType, -} from '../../types'; - -interface AuthContextProps { - user: UserType; - profile: ProfileType; - login: (userId: string, username: string) => void; - logout: () => void; - avatar: string | null; - cover: string | null; - socialAccounts: Record<string, SocialAccountType>; - recentSearches: Array<ProfilePreviewType>; - taggUsers: Array<ProfilePreviewType>; - newMomentsAvailable: boolean; - updateMoments: (value: boolean) => void; - socialsNeedUpdate: (_: string[]) => void; - moments: MomentType[]; - followers: ProfilePreviewType[]; - following: ProfilePreviewType[]; - followersNeedUpdate: boolean; - updateFollowers: (value: boolean) => void; - blockedUsers: ProfilePreviewType[]; - blockedUsersNeedUpdate: boolean; - updateBlockedUsers: (value: boolean) => void; - isEditedProfile: boolean; - updateIsEditedProfile: (value: boolean) => void; -} - -const NO_USER: UserType = { - userId: '', - username: '', -}; - -const NO_PROFILE: ProfileType = { - biography: '', - website: '', - name: '', - gender: '', - birthday: undefined, -}; - -const NO_SOCIAL_ACCOUNTS: Record<string, SocialAccountType> = { - Instagram: {posts: []}, - Facebook: {posts: []}, - Twitter: {posts: []}, -}; - -export const AuthContext = createContext<AuthContextProps>({ - user: NO_USER, - profile: NO_PROFILE, - login: () => {}, - logout: () => {}, - avatar: null, - cover: null, - recentSearches: [], - taggUsers: [], - newMomentsAvailable: true, - updateMoments: () => {}, - socialAccounts: NO_SOCIAL_ACCOUNTS, - socialsNeedUpdate: () => {}, - moments: [], - followers: [], - following: [], - followersNeedUpdate: true, - updateFollowers: () => {}, - blockedUsers: [], - blockedUsersNeedUpdate: true, - updateBlockedUsers: () => {}, - isEditedProfile: false, - updateIsEditedProfile: () => {}, -}); - -/** - * Authentication provider for the application. - */ -const AuthProvider: React.FC = ({children}) => { - const [user, setUser] = useState<UserType>(NO_USER); - const [profile, setProfile] = useState<ProfileType>(NO_PROFILE); - const [avatar, setAvatar] = useState<string | null>(null); - const [cover, setCover] = useState<string | null>(null); - const [socialAccounts, setSocialAccounts] = useState< - Record<string, SocialAccountType> - >(NO_SOCIAL_ACCOUNTS); - const [recentSearches, setRecentSearches] = useState< - Array<ProfilePreviewType> - >([]); - const [taggUsers, setTaggUsers] = useState<Array<ProfilePreviewType>>([]); - const [newMomentsAvailable, setNewMomentsAvailable] = useState<boolean>(true); - // Default update all integrated social lists on start - const [socialsNeedUpdate, setSocialsNeedUpdate] = useState<string[]>([ - ...INTEGRATED_SOCIAL_LIST, - ]); - const [moments, setMoments] = useState<Array<MomentType>>([]); - const [followers, setFollowers] = useState<Array<ProfilePreviewType>>([]); - const [following, setFollowing] = useState<Array<ProfilePreviewType>>([]); - const [followersNeedUpdate, setFollowersNeedUpdate] = useState<boolean>(true); - const [blockedUsers, setBlockedUsers] = useState<Array<ProfilePreviewType>>( - [], - ); - const [blockedUsersNeedUpdate, setBlockedUsersNeedUpdate] = useState<boolean>( - true, - ); - const [isEditedProfile, setIsEditedProfile] = useState<boolean>(false); - const {userId} = user; - - useEffect(() => { - const loadUserInfoFromStorage = async () => { - const [id, username, token] = await Promise.all([ - AsyncStorage.getItem('userId'), - AsyncStorage.getItem('username'), - AsyncStorage.getItem('token'), - ]); - if (id && username && token) { - setUser({...user, userId: id, username}); - } - }; - if (user === NO_USER) { - loadUserInfoFromStorage(); - } - }, [user]); - - useEffect(() => { - if (!userId) { - return; - } - - const loadData = async () => { - try { - const token = await AsyncStorage.getItem('token'); - if (!token) { - setUser(NO_USER); - return; - } - loadProfileInfo(token, userId, setProfile); - loadAvatar(token, userId, setAvatar); - loadCover(token, userId, setCover); - loadRecentlySearchedUsers(setRecentSearches); - } catch (err) { - console.log(err); - } - }; - loadData(); - }, [userId, isEditedProfile]); - - useEffect(() => { - const loadNewMoments = async () => { - try { - const token = await AsyncStorage.getItem('token'); - if (!token) { - setUser(NO_USER); - return; - } - const newMoments = await loadMoments(userId, token); - if (newMoments) { - setMoments(newMoments); - } - setNewMomentsAvailable(false); - } catch (error) { - console.log(error); - } - }; - if (newMomentsAvailable && userId) { - loadNewMoments(); - } - }, [newMomentsAvailable, userId, moments]); - - useEffect(() => { - const loadNewFollowers = async () => { - try { - const token = await AsyncStorage.getItem('token'); - if (!token) { - setUser(NO_USER); - return; - } - loadFollowers(userId, token, setFollowers); - loadFollowing(userId, token, setFollowing); - setFollowersNeedUpdate(false); - } catch (error) { - console.log(error); - } - }; - if (followersNeedUpdate && userId) { - loadNewFollowers(); - } - }, [followersNeedUpdate, userId, followers, following]); - - useEffect(() => { - if (socialsNeedUpdate.length > 0 && userId) { - for (let social of socialsNeedUpdate) { - loadSocialPosts(userId, social).then((accountData) => { - socialAccounts[social] = accountData; - setSocialAccounts(socialAccounts); - console.log('Refreshed posts data:', social); - }); - } - setSocialsNeedUpdate([]); - } - }, [socialAccounts, socialsNeedUpdate, userId]); - - useEffect(() => { - const loadNewBlockedUsers = async () => { - try { - const token = await AsyncStorage.getItem('token'); - if (!token) { - setUser(NO_USER); - return; - } - loadBlockedUsers(userId, token, setBlockedUsers); - setBlockedUsersNeedUpdate(false); - } catch (error) { - console.log(error); - } - }; - if (blockedUsersNeedUpdate && userId) { - loadNewBlockedUsers(); - } - }, [ - setBlockedUsersNeedUpdate, - blockedUsersNeedUpdate, - userId, - setBlockedUsers, - ]); - - useEffect(() => { - const loadTaggUsers = async () => { - try { - const token = await AsyncStorage.getItem('token'); - if (!token) { - setUser(NO_USER); - return; - } - await getAllTaggUsers(token, setTaggUsers); - } catch (error) { - console.log(error); - } - }; - loadTaggUsers(); - }, [userId]); - - return ( - <AuthContext.Provider - value={{ - user, - profile, - avatar, - cover, - newMomentsAvailable, - socialAccounts, - moments, - followers, - following, - followersNeedUpdate, - blockedUsers, - blockedUsersNeedUpdate, - isEditedProfile, - login: (id, username) => { - setUser({...user, userId: id, username}); - }, - logout: () => { - try { - new Promise(() => { - AsyncStorage.removeItem('token'); - AsyncStorage.removeItem('userId'); - AsyncStorage.removeItem('username'); - }).then(() => { - setUser(NO_USER); - }); - } catch (err) { - console.log(err); - } - }, - recentSearches, - taggUsers, - updateMoments: (value) => { - setNewMomentsAvailable(value); - }, - socialsNeedUpdate: (socials: string[]) => { - setSocialsNeedUpdate(socials); - }, - updateFollowers: (value) => { - setFollowersNeedUpdate(value); - }, - updateBlockedUsers: (value) => { - setBlockedUsersNeedUpdate(value); - }, - updateIsEditedProfile: (value: boolean) => { - setIsEditedProfile(value); - }, - }}> - {children} - </AuthContext.Provider> - ); -}; - -export default AuthProvider; diff --git a/src/routes/authentication/index.ts b/src/routes/authentication/index.ts deleted file mode 100644 index 9968ae93..00000000 --- a/src/routes/authentication/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './AuthProvider'; -export {default} from './AuthProvider'; diff --git a/src/routes/index.ts b/src/routes/index.ts index 7e8a84ce..3b74e130 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,7 +1,3 @@ -export {default as AuthProvider} from './authentication'; -export {default as ProfileProvider} from './viewProfile'; -export * from './authentication'; -export * from './viewProfile'; export * from './onboarding'; export * from './profile'; export {default} from './Routes'; diff --git a/src/routes/profile/Profile.tsx b/src/routes/profile/Profile.tsx index b6672c85..f47d25c4 100644 --- a/src/routes/profile/Profile.tsx +++ b/src/routes/profile/Profile.tsx @@ -11,17 +11,17 @@ import { } from '../../screens'; import {ProfileStack, ProfileStackParams} from './ProfileStack'; import {RouteProp} from '@react-navigation/native'; +import {ScreenType} from '../../types'; /** - * What will be the First Screen of the stack depends on value of isProfileView (Search if its true else Profile) * Trying to explain the purpose of each route on the stack (ACTUALLY A STACK) - * Profile : To display the logged in user's profile when isProfileView is false, else displays profile of any user the logged in user wants to view. - * When you click on the profile icon after looking at a user's profile, the stack is reset and you come back to the top of the stack (First screen : Profile in this case) - * Search : To display the search screen. Search for a user on this screen, click on a result tile and navigate to the same (isProfileView = true). + * 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<ProfileStackParams, 'Profile'>; @@ -31,7 +31,12 @@ interface ProfileStackProps { } const Profile: React.FC<ProfileStackProps> = ({route}) => { - const {isProfileView} = route.params; + const {screenType} = route.params; + + /** + * This parameter isProfileStack acts as a switch between Search and Profile Stacks + */ + const isProfileStack = screenType === ScreenType.Profile; return ( <ProfileStack.Navigator screenOptions={{ @@ -56,15 +61,21 @@ const Profile: React.FC<ProfileStackProps> = ({route}) => { }), }} mode="modal" - initialRouteName={!isProfileView ? 'Profile' : 'Search'}> + initialRouteName={isProfileStack ? 'Profile' : 'Search'}> <ProfileStack.Screen name="Profile" component={ProfileScreen} options={{headerShown: false}} - initialParams={{isProfileView: isProfileView}} + initialParams={{ + screenType, + }} /> - {isProfileView ? ( - <ProfileStack.Screen name="Search" component={SearchScreen} /> + {!isProfileStack ? ( + <ProfileStack.Screen + name="Search" + component={SearchScreen} + initialParams={{screenType}} + /> ) : ( <React.Fragment /> )} @@ -77,8 +88,9 @@ const Profile: React.FC<ProfileStackProps> = ({route}) => { headerBackTitleVisible: false, headerTintColor: 'white', }} + initialParams={{screenType}} /> - {!isProfileView ? ( + {isProfileStack ? ( <ProfileStack.Screen name="CaptionScreen" component={CaptionScreen} /> ) : ( <React.Fragment /> @@ -87,18 +99,18 @@ const Profile: React.FC<ProfileStackProps> = ({route}) => { name="IndividualMoment" component={IndividualMoment} options={{headerShown: false}} - initialParams={{isProfileView: isProfileView}} + initialParams={{screenType}} /> <ProfileStack.Screen name="MomentCommentsScreen" component={MomentCommentsScreen} options={{headerShown: false}} - initialParams={{isProfileView: isProfileView}} + initialParams={{screenType}} /> <ProfileStack.Screen name="FollowersListScreen" component={FollowersListScreen} - initialParams={{isProfileView: isProfileView}} + initialParams={{screenType}} /> <ProfileStack.Screen name="EditProfile" diff --git a/src/routes/profile/ProfileStack.tsx b/src/routes/profile/ProfileStack.tsx index 5590f78a..e7db9f37 100644 --- a/src/routes/profile/ProfileStack.tsx +++ b/src/routes/profile/ProfileStack.tsx @@ -1,41 +1,45 @@ +/** + * Note the name userXId here, it refers to the id of the user being visited + */ import {createStackNavigator} from '@react-navigation/stack'; -import {MomentType, ProfilePreviewType, SocialAccountType} from '../../types'; +import {MomentType, ScreenType} from '../../types'; export type ProfileStackParams = { - Search: undefined; + Search: { + screenType: ScreenType; + }; Profile: { - isProfileView: boolean; - username: string; - userId: string; + userXId: string | undefined; + screenType: ScreenType; }; SocialMediaTaggs: { socialMediaType: string; - socialMediaHandle: string; - isProfileView: boolean; - name: string; - accountData: SocialAccountType; - avatar: string; + userXId: string | undefined; + screenType: ScreenType; }; CaptionScreen: { title: string; image: object; + screenType: ScreenType; }; IndividualMoment: { moment: MomentType; - isProfileView: boolean; - username: string; + userXId: string | undefined; + screenType: ScreenType; }; MomentCommentsScreen: { - isProfileView: boolean; moment_id: string; + userXId: string | undefined; + screenType: ScreenType; }; FollowersListScreen: { isFollowers: boolean; - list: ProfilePreviewType[]; + userXId: string | undefined; + screenType: ScreenType; }; EditProfile: { - userId: boolean; - username: ProfilePreviewType[]; + userId: string; + username: string; }; }; diff --git a/src/routes/tabs/NavigationBar.tsx b/src/routes/tabs/NavigationBar.tsx index 67f842e0..8fcbb869 100644 --- a/src/routes/tabs/NavigationBar.tsx +++ b/src/routes/tabs/NavigationBar.tsx @@ -1,6 +1,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/Profile'; const Tabs = createBottomTabNavigator(); @@ -45,12 +46,12 @@ const NavigationBar: React.FC = () => { <Tabs.Screen name="Search" component={Profile} - initialParams={{isProfileView: true}} + initialParams={{screenType: ScreenType.Search}} /> <Tabs.Screen name="Profile" component={Profile} - initialParams={{isProfileView: false}} + initialParams={{screenType: ScreenType.Profile}} /> </Tabs.Navigator> ); diff --git a/src/routes/viewProfile/ProfileProvider.tsx b/src/routes/viewProfile/ProfileProvider.tsx deleted file mode 100644 index f2d27a84..00000000 --- a/src/routes/viewProfile/ProfileProvider.tsx +++ /dev/null @@ -1,207 +0,0 @@ -import AsyncStorage from '@react-native-community/async-storage'; -import React, {createContext, useEffect, useState} from 'react'; -import {Value} from 'react-native-reanimated'; -import {INTEGRATED_SOCIAL_LIST} from '../../constants'; -import { - loadAvatar, - loadCover, - loadProfileInfo, - loadSocialPosts, - loadMoments, - loadFollowers, - loadFollowing, -} from '../../services'; -import { - ProfileType, - SocialAccountType, - ProfilePreviewType, - UserType, - MomentType, -} from '../../types'; - -interface ProfileContextProps { - user: UserType; - profile: ProfileType; - avatar: string | null; - cover: string | null; - socialAccounts: Record<string, SocialAccountType>; - socialsNeedUpdate: (_: string[]) => void; - moments: MomentType[]; - followers: ProfilePreviewType[]; - following: ProfilePreviewType[]; - followersNeedUpdate: boolean; - updateFollowers: (value: boolean) => void; -} -const NO_USER: UserType = { - userId: '', - username: '', -}; -const NO_PROFILE: ProfileType = { - biography: '', - website: '', - name: '', -}; - -const NO_SOCIAL_ACCOUNTS: Record<string, SocialAccountType> = { - Instagram: {posts: []}, - Facebook: {posts: []}, - Twitter: {posts: []}, -}; - -export const ProfileContext = createContext<ProfileContextProps>({ - user: NO_USER, - profile: NO_PROFILE, - avatar: null, - cover: null, - socialAccounts: NO_SOCIAL_ACCOUNTS, - socialsNeedUpdate: () => {}, - moments: [], - followers: [], - following: [], - followersNeedUpdate: true, - updateFollowers: () => {}, -}); - -/** - * This is the context provider for user profiles that the logged in user wants to see - * The ProfileProviderProps is used to initialise data as soon as the component is initialised. - */ - -type ProfileProviderProps = { - uname: string; - uId: string; -}; - -const ProfileProvider: React.FC<ProfileProviderProps> = ({ - children, - uId, - uname, -}) => { - const [user, setUser] = useState<UserType>({userId: uId, username: uname}); - const [profile, setProfile] = useState<ProfileType>(NO_PROFILE); - const [avatar, setAvatar] = useState<string | null>(null); - const [cover, setCover] = useState<string | null>(null); - const [newMomentsAvailable, setNewMomentsAvailable] = useState<boolean>(true); - const [socialAccounts, setSocialAccounts] = useState< - Record<string, SocialAccountType> - >(NO_SOCIAL_ACCOUNTS); - const [moments, setMoments] = useState<Array<MomentType>>([]); - const [followers, setFollowers] = useState<Array<ProfilePreviewType>>([]); - const [following, setFollowing] = useState<Array<ProfilePreviewType>>([]); - const [followersNeedUpdate, setFollowersNeedUpdate] = useState<boolean>(true); - // Default update all integrated social lists on start - const [socialsNeedUpdate, setSocialsNeedUpdate] = useState<string[]>([ - ...INTEGRATED_SOCIAL_LIST, - ]); - - const {userId} = user; - useEffect(() => { - if (!userId) { - return; - } - const loadData = async () => { - try { - const token = await AsyncStorage.getItem('token'); - if (!token) { - setUser(NO_USER); - return; - } - loadProfileInfo(token, userId, setProfile); - loadAvatar(token, userId, setAvatar); - loadCover(token, userId, setCover); - } catch (err) { - console.log(err); - } - }; - loadData(); - }, [userId]); - - useEffect(() => { - const loadNewMoments = async () => { - try { - const token = await AsyncStorage.getItem('token'); - if (!token) { - setUser(NO_USER); - return; - } - const newMoments = await loadMoments(userId, token); - if (newMoments) { - setMoments(newMoments); - } - setNewMomentsAvailable(false); - } catch (error) { - console.log(error); - } - }; - if (newMomentsAvailable && userId) { - loadNewMoments(); - } - }, [newMomentsAvailable, userId]); - - useEffect(() => { - const loadNewFollowers = async () => { - try { - const token = await AsyncStorage.getItem('token'); - if (!token) { - setUser(NO_USER); - return; - } - loadFollowers(userId, token, setFollowers); - loadFollowing(userId, token, setFollowing); - setFollowersNeedUpdate(false); - } catch (error) { - console.log(error); - } - }; - if (followersNeedUpdate && userId) { - loadNewFollowers(); - } - }, [followersNeedUpdate, userId, followers, following]); - - useEffect(() => { - if (socialsNeedUpdate.length > 0 && userId) { - for (let social of socialsNeedUpdate) { - loadSocialPosts(userId, social).then((accountData) => { - /** - * Please use the following syntax when updating an object, fixing this problem broke our head. LOLs - * ref1 : https://stackoverflow.com/questions/56423256/set-dynamic-key-in-state-via-usestate-react-hooks - * ref2: https://stackoverflow.com/questions/43638938/updating-an-object-with-setstate-in-react/43639228 - * The spread operator {...} helps us make a simple copy of the object - * And form there on we can use the [] to specify the dynamically constructed key and set its value. - */ - setSocialAccounts((prevSocialAccounts) => ({ - ...prevSocialAccounts, - [social]: accountData, - })); - console.log('Updated posts data', social); - }); - } - setSocialsNeedUpdate([]); - } - }, [socialAccounts, socialsNeedUpdate, userId]); - - return ( - <ProfileContext.Provider - value={{ - user, - profile, - avatar, - cover, - socialAccounts, - moments, - followers, - following, - followersNeedUpdate, - socialsNeedUpdate: (socials: string[]) => { - setSocialsNeedUpdate(socials); - }, - updateFollowers: (value) => { - setFollowersNeedUpdate(value); - }, - }}> - {children} - </ProfileContext.Provider> - ); -}; - -export default ProfileProvider; diff --git a/src/routes/viewProfile/index.ts b/src/routes/viewProfile/index.ts deleted file mode 100644 index 7035ce4a..00000000 --- a/src/routes/viewProfile/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './ProfileProvider'; -export {default} from './ProfileProvider'; diff --git a/src/screens/onboarding/Checkpoint.tsx b/src/screens/onboarding/Checkpoint.tsx index 0be1e831..83a8a2bc 100644 --- a/src/screens/onboarding/Checkpoint.tsx +++ b/src/screens/onboarding/Checkpoint.tsx @@ -11,7 +11,6 @@ import { } from 'react-native'; import {OnboardingStackParams} from '../../routes'; -import {AuthContext} from '../../routes/authentication'; import {RegistrationWizard, Background} from '../../components'; type CheckpointRouteProp = RouteProp<OnboardingStackParams, 'Checkpoint'>; diff --git a/src/screens/onboarding/Login.tsx b/src/screens/onboarding/Login.tsx index 8f19ec3d..2ddae403 100644 --- a/src/screens/onboarding/Login.tsx +++ b/src/screens/onboarding/Login.tsx @@ -14,11 +14,12 @@ import { } from 'react-native'; import {OnboardingStackParams} from '../../routes/onboarding'; -import {AuthContext} from '../../routes/authentication'; import {Background, TaggInput, SubmitButton} from '../../components'; import {usernameRegex, LOGIN_ENDPOINT} from '../../constants'; import AsyncStorage from '@react-native-community/async-storage'; import {UserType} from '../../types'; +import {useDispatch} from 'react-redux'; +import {userLogin} from '../../utils'; type VerificationScreenRouteProp = RouteProp<OnboardingStackParams, 'Login'>; type VerificationScreenNavigationProp = StackNavigationProp< @@ -51,9 +52,15 @@ const Login: React.FC<LoginProps> = ({navigation}: LoginProps) => { attemptedSubmit: false, token: '', }); - // determines if user is logged in - const {login} = React.useContext(AuthContext); const [user, setUser] = useState<UserType>(NO_USER); + + /** + * Redux Store stuff + * Get the dispatch reference + */ + + const dispatch = useDispatch(); + /** * Updates the state of username. Also verifies the input of the username field by ensuring proper length and appropriate characters. */ @@ -140,7 +147,7 @@ const Login: React.FC<LoginProps> = ({navigation}: LoginProps) => { await AsyncStorage.setItem('token', data.token); await AsyncStorage.setItem('userId', data.UserID); await AsyncStorage.setItem('username', username); - login(data.UserID, username); + userLogin(dispatch, {userId: data.UserID, username}); } catch (err) { setUser(NO_USER); console.log(data); diff --git a/src/screens/onboarding/SocialMedia.tsx b/src/screens/onboarding/SocialMedia.tsx index 57270e4e..ee2bed10 100644 --- a/src/screens/onboarding/SocialMedia.tsx +++ b/src/screens/onboarding/SocialMedia.tsx @@ -10,6 +10,7 @@ import { TouchableOpacity, View, } from 'react-native'; +import {useDispatch} from 'react-redux'; import {LinkerType} from 'src/types'; import { Background, @@ -17,7 +18,8 @@ import { RegistrationWizard, } from '../../components'; import {SOCIAL_LIST} from '../../constants/'; -import {AuthContext, OnboardingStackParams} from '../../routes'; +import {OnboardingStackParams} from '../../routes'; +import {userLogin} from '../../utils'; /** * Social Media Screen for displaying social media linkers @@ -33,12 +35,6 @@ const SocialMedia: React.FC<SocialMediaProps> = ({route}) => { const {userId, username} = route.params; const linkers: Array<LinkerType> = []; - /** - * login: determines if user successully created an account to - * navigate to home and display main tab navigation bar - */ - const {login} = React.useContext(AuthContext); - // let numSocials: Number = state.showMore ? 9 : 3; for (let i = 0; i < SOCIAL_LIST.length; i++) { @@ -48,6 +44,8 @@ const SocialMedia: React.FC<SocialMediaProps> = ({route}) => { linkers.push(linker); } + const dispatch = useDispatch(); + /** * Just commenting this out, in case we need it in the future */ @@ -60,7 +58,7 @@ const SocialMedia: React.FC<SocialMediaProps> = ({route}) => { const handleLogin = () => { try { - login(userId, username); + userLogin(dispatch, {userId: userId, username: username}); } catch (error) { console.log(error); Alert.alert('There was a problem logging you in'); diff --git a/src/screens/profile/CaptionScreen.tsx b/src/screens/profile/CaptionScreen.tsx index 08dd8e5b..e9eed668 100644 --- a/src/screens/profile/CaptionScreen.tsx +++ b/src/screens/profile/CaptionScreen.tsx @@ -18,7 +18,9 @@ import {ProfileStackParams} from 'src/routes'; import {StackNavigationProp} from '@react-navigation/stack'; import {CaptionScreenHeader} from '../../components/'; import {MOMENTS_ENDPOINT} from '../../constants'; -import {AuthContext} from '../../routes/authentication'; +import {useDispatch, useSelector} from 'react-redux'; +import {loadUserMoments} from '../../store/actions'; +import {RootState} from '../../store/rootReducer'; /** * Upload Screen to allow users to upload posts to Tagg @@ -34,11 +36,11 @@ interface CaptionScreenProps { } const CaptionScreen: React.FC<CaptionScreenProps> = ({route, navigation}) => { - const {title, image} = route.params; + const {title, image, screenType} = route.params; const { user: {userId}, - updateMoments, - } = React.useContext(AuthContext); + } = useSelector((state: RootState) => state.user); + const dispatch = useDispatch(); const [caption, setCaption] = React.useState(''); const handleCaptionUpdate = (caption: string) => { @@ -54,6 +56,14 @@ const CaptionScreen: React.FC<CaptionScreenProps> = ({route, navigation}) => { return true; }; + const navigateToProfile = () => { + //Since the logged In User is navigating to own profile, useXId is not required + navigation.navigate('Profile', { + screenType, + userXId: undefined, + }); + }; + const handleShare = async () => { try { const request = new FormData(); @@ -86,8 +96,8 @@ const CaptionScreen: React.FC<CaptionScreenProps> = ({route, navigation}) => { let data = await response.json(); if (statusCode === 200 && checkImageUploadStatus(data)) { Alert.alert('The picture was uploaded successfully!'); - updateMoments(true); - navigation.navigate('Profile', {isProfileView: false}); + dispatch(loadUserMoments(userId)); + navigateToProfile(); } else { Alert.alert('An error occured while uploading. Please try again!'); } @@ -107,9 +117,7 @@ const CaptionScreen: React.FC<CaptionScreenProps> = ({route, navigation}) => { <Button title="Cancel" buttonStyle={styles.button} - onPress={() => { - navigation.navigate('Profile', {isProfileView: false}); - }} + onPress={() => navigateToProfile()} /> <Button title="Share" diff --git a/src/screens/profile/EditProfile.tsx b/src/screens/profile/EditProfile.tsx index 01b67155..44d99b82 100644 --- a/src/screens/profile/EditProfile.tsx +++ b/src/screens/profile/EditProfile.tsx @@ -30,9 +30,11 @@ import { genderRegex, } from '../../constants'; import AsyncStorage from '@react-native-community/async-storage'; -import {AuthContext} from '../../routes'; import Animated from 'react-native-reanimated'; import {SCREEN_HEIGHT} from '../../utils'; +import {RootState} from '../../store/rootReducer'; +import {useDispatch, useSelector} from 'react-redux'; +import {loadUserData} from '../../store/actions'; type ProfileOnboardingScreenRouteProp = RouteProp< OnboardingStackParams, @@ -62,13 +64,14 @@ const ProfileOnboarding: React.FC<ProfileOnboardingProps> = ({ profile: {website, biography, birthday, gender}, avatar, cover, - updateIsEditedProfile, - } = React.useContext(AuthContext); + } = useSelector((state: RootState) => state.user); + const [needsUpdate, setNeedsUpdate] = useState(false); + const dispatch = useDispatch(); useEffect(() => { - updateIsEditedProfile(needsUpdate); - }, [needsUpdate, updateIsEditedProfile]); + if (needsUpdate) dispatch(loadUserData({userId, username})); + }, [loadUserData, needsUpdate]); const [isCustomGender, setIsCustomGender] = React.useState<boolean>( gender !== '' && gender !== 'female' && gender !== 'male', diff --git a/src/screens/profile/FollowersListScreen.tsx b/src/screens/profile/FollowersListScreen.tsx index bad264a1..874dd01b 100644 --- a/src/screens/profile/FollowersListScreen.tsx +++ b/src/screens/profile/FollowersListScreen.tsx @@ -1,10 +1,14 @@ -import React from 'react'; +import React, {useEffect, useState} from 'react'; import {RouteProp} from '@react-navigation/native'; import {TabsGradient, Followers, CenteredView} from '../../components'; import {ScrollView} from 'react-native-gesture-handler'; import {SCREEN_HEIGHT} from '../../utils'; import {StyleSheet, View} from 'react-native'; import {ProfileStackParams} from '../../routes'; +import {ProfilePreviewType} from '../../types'; +import {EMPTY_PROFILE_PREVIEW_LIST} from '../../store/initialStates'; +import {useSelector} from 'react-redux'; +import {RootState} from '../../store/rootReducer'; type FollowersListScreenRouteProp = RouteProp< ProfileStackParams, @@ -15,7 +19,19 @@ interface FollowersListScreenProps { } const FollowersListScreen: React.FC<FollowersListScreenProps> = ({route}) => { - const {isFollowers, list} = route.params; + const {isFollowers, userXId, screenType} = route.params; + + const {followers, following} = userXId + ? useSelector((state: RootState) => state.userX[screenType][userXId]) + : useSelector((state: RootState) => state.follow); + + const [list, setList] = useState<ProfilePreviewType[]>( + EMPTY_PROFILE_PREVIEW_LIST, + ); + + useEffect(() => { + setList(isFollowers ? followers : following); + }, [followers, following, setList]); return ( <CenteredView> @@ -28,6 +44,7 @@ const FollowersListScreen: React.FC<FollowersListScreenProps> = ({route}) => { <Followers result={list} sectionTitle={isFollowers ? 'Followers' : 'Following'} + screenType={screenType} /> </ScrollView> <TabsGradient /> diff --git a/src/screens/profile/IndividualMoment.tsx b/src/screens/profile/IndividualMoment.tsx index 29a624cf..2739324e 100644 --- a/src/screens/profile/IndividualMoment.tsx +++ b/src/screens/profile/IndividualMoment.tsx @@ -12,7 +12,6 @@ import {UserType} from '../../types'; import {RouteProp} from '@react-navigation/native'; import {StackNavigationProp} from '@react-navigation/stack'; import {CaptionScreenHeader} from '../../components'; -import {AuthContext, ProfileContext} from '../../routes/'; import {ProfileStackParams} from 'src/routes/profile/ProfileStack'; import Animated from 'react-native-reanimated'; import {CommentsCount} from '../../components'; @@ -21,6 +20,10 @@ import {getMomentCommentsCount} from '../../services'; import {TouchableOpacity} from 'react-native-gesture-handler'; import {Alert} from 'react-native'; import {sendReport} from '../../services/ReportingService'; +import {logout} from '../../store/actions'; +import {useDispatch, useSelector} from 'react-redux'; +import {RootState} from '../../store/rootreducer'; +import {DUMMY_USERNAME} from '../../store/initialStates'; const NO_USER: UserType = { userId: '', @@ -53,17 +56,25 @@ const IndividualMoment: React.FC<IndividualMomentProps> = ({ date_time, moment_id, } = route.params.moment; - const {isProfileView, username} = route.params; + const {userXId, screenType} = route.params; + const { user: {userId: loggedInUserId, username: loggedInusername}, - logout, - } = React.useContext(AuthContext); + } = useSelector((state: RootState) => state.user); + + const { + user: {username}, + } = userXId + ? useSelector((state: RootState) => state.userX[screenType][userXId]) + : {user: {username: DUMMY_USERNAME}}; + const isOwnProfile = username === loggedInusername; const [user, setUser] = useState<UserType>(NO_USER); const [caption, setCaption] = React.useState(route.params.moment.caption); const [elapsedTime, setElapsedTime] = React.useState<string>(); const [comments_count, setCommentsCount] = React.useState(''); const [isReporting, setIsReporting] = React.useState(false); + const dispatch = useDispatch(); const handleCaptionUpdate = (caption: string) => { setCaption(caption); @@ -80,7 +91,7 @@ const IndividualMoment: React.FC<IndividualMomentProps> = ({ const loadComments = async () => { const token = await AsyncStorage.getItem('token'); if (!token) { - logout(); + dispatch(logout()); return; } getMomentCommentsCount(moment_id, setCommentsCount, token); @@ -155,13 +166,13 @@ const IndividualMoment: React.FC<IndividualMomentProps> = ({ <View style={styles.bodyContainer}> <CommentsCount comments_count={comments_count} - isProfileView={isProfileView} moment_id={moment_id} + screenType={screenType} /> <Animated.Text style={styles.text}>{elapsedTime}</Animated.Text> </View> <Animated.Text style={styles.captionText}>{caption}</Animated.Text> - {isProfileView && !isOwnProfile && !isReporting && ( + {userXId && !isOwnProfile && !isReporting && ( <TouchableOpacity onPress={sendReportAlert}> <Animated.Text style={styles.reportIssue}> {'Report an issue'} diff --git a/src/screens/profile/MomentCommentsScreen.tsx b/src/screens/profile/MomentCommentsScreen.tsx index 7a0bfa66..34f85c28 100644 --- a/src/screens/profile/MomentCommentsScreen.tsx +++ b/src/screens/profile/MomentCommentsScreen.tsx @@ -9,8 +9,9 @@ import {Button} from 'react-native-elements'; import {AddComment} from '../../components/'; import {useEffect} from 'react'; import AsyncStorage from '@react-native-community/async-storage'; -import {AuthContext} from '../../routes/authentication'; import {getMomentComments} from '../..//services'; +import {useDispatch} from 'react-redux'; +import {logout} from '../../store/actions'; /** * Comments Screen for an image uploaded @@ -29,16 +30,16 @@ interface MomentCommentsScreenProps { const MomentCommentsScreen: React.FC<MomentCommentsScreenProps> = ({route}) => { const navigation = useNavigation(); - const {isProfileView, moment_id} = route.params; + const {moment_id, screenType} = route.params; const [commentsList, setCommentsList] = React.useState([]); const [newCommentsAvailable, setNewCommentsAvailable] = React.useState(true); - const {logout} = React.useContext(AuthContext); + const dispatch = useDispatch(); useEffect(() => { const loadComments = async () => { const token = await AsyncStorage.getItem('token'); if (!token) { - logout(); + dispatch(logout()); return; } getMomentComments(moment_id, setCommentsList, token); @@ -70,7 +71,11 @@ const MomentCommentsScreen: React.FC<MomentCommentsScreenProps> = ({route}) => { contentContainerStyle={styles.modalScrollViewContent}> {commentsList && commentsList.map((comment: CommentType) => ( - <CommentTile key={comment.comment_id} comment_object={comment} /> + <CommentTile + key={comment.comment_id} + comment_object={comment} + screenType={screenType} + /> ))} </ScrollView> <AddComment diff --git a/src/screens/profile/ProfileScreen.tsx b/src/screens/profile/ProfileScreen.tsx index d32eca98..a2a1b5bd 100644 --- a/src/screens/profile/ProfileScreen.tsx +++ b/src/screens/profile/ProfileScreen.tsx @@ -1,11 +1,15 @@ -import React, {useContext, useEffect} from 'react'; +import React, {useContext, useEffect, useState} from 'react'; import {StatusBar} from 'react-native'; import Animated from 'react-native-reanimated'; import {Content, Cover, TabsGradient} from '../../components'; -import {RouteProp} from '@react-navigation/native'; -import {ProfileStackParams, ProfileProvider, AuthContext} from '../../routes/'; - -/** +import {RouteProp, useFocusEffect} from '@react-navigation/native'; +import {ProfileStackParams, ProfileProvider} from '../../routes/'; +import {resetScreenType} from '../../store/actions'; +import {useDispatch, useStore} from 'react-redux'; +import {ScreenType} from '../../types'; +import {DUMMY_USERID} from '../../store/initialStates'; + +/**r * Profile Screen for a user's profile * including posts, messaging, and settings */ @@ -17,45 +21,37 @@ interface ProfileOnboardingProps { } const ProfileScreen: React.FC<ProfileOnboardingProps> = ({route}) => { - const {isProfileView, username, userId} = route.params; + const {screenType} = route.params; + let {userXId} = route.params; const y = Animated.useValue(0); - const {updateIsEditedProfile} = useContext(AuthContext); - - useEffect(() => { - updateIsEditedProfile(false); - }); + const dispatch = useDispatch(); - const profileView = () => { - return ( - <> - <StatusBar /> - <Cover {...{y, isProfileView}} /> - <Content {...{y, isProfileView}} /> - <TabsGradient /> - </> - ); - }; + /** + * This is a double safety check to avoid app crash. + * Checks if the required userXId is present in the store, if not userXId is set to dummy id + */ + if (userXId && !(userXId in useStore().getState().userX[screenType])) { + userXId = DUMMY_USERID; + } /** - * Every profile to have it's own ProfileContext if a profile is being visited by the logged in user. - * Pass userid and username of the user whose profile needs to be viewed and the information gets loaded to the ProfileContext. - * Now this profile context is local to every user being viewed. - * This comes with BENEFITS and CAVEATS - * BENEFITS : - * 1 - The app now remembers which user was visited last. - * 2 - The Search and Profile stacks (and potentially more stacks in the future) now have totally unrelated stacks of profiles being visited. - * 3 - This will help us manage the navigation stack better if we introduce a feature to go back to the last profile visited. - * CAVEATS : - * 1 - Since the ProfileContext is not global, whenever we navigate to some component that is not in the child tree of the ProfileProvider, we would not be able to use anything in the context (It would basically have default values). - * 2 - For example, the followers list (FollowersListScreen) and profile picture on the Taggs screen (AvatarTile) now no longer have access to the ProfileContext. - * 3 - Components like these now have to have data passed in via props, instead of using the context. + * Code under useFocusEffect gets executed every time the screen comes under focus / is being viewed by the user. + * This is done to reset the users stored in our store for the Search screen. + * Read more about useFocusEffect here : https://reactnavigation.org/docs/function-after-focusing-screen/ */ - return isProfileView ? ( - <ProfileProvider uId={userId} uname={username}> - {profileView()} - </ProfileProvider> - ) : ( - profileView() + useFocusEffect(() => { + if (!userXId) { + dispatch(resetScreenType(screenType)); + } + }); + + return ( + <> + <StatusBar /> + <Cover {...{y, userXId, screenType}} /> + <Content {...{y, userXId, screenType}} /> + <TabsGradient /> + </> ); }; diff --git a/src/screens/profile/SocialMediaTaggs.tsx b/src/screens/profile/SocialMediaTaggs.tsx index d67f3a1a..5634c251 100644 --- a/src/screens/profile/SocialMediaTaggs.tsx +++ b/src/screens/profile/SocialMediaTaggs.tsx @@ -1,7 +1,13 @@ import {RouteProp} from '@react-navigation/native'; import {StackNavigationProp} from '@react-navigation/stack'; -import React, {useEffect} from 'react'; -import {ScrollView, StatusBar, StyleSheet, View} from 'react-native'; +import React, {useEffect, useState} from 'react'; +import { + ActivityIndicator, + ScrollView, + StatusBar, + StyleSheet, + View, +} from 'react-native'; import LinearGradient from 'react-native-linear-gradient'; import { AvatarTitle, @@ -10,10 +16,13 @@ import { TaggPost, TwitterTaggPost, } from '../../components'; -import {AVATAR_GRADIENT} from '../../constants'; +import {AVATAR_GRADIENT, TAGG_DARK_BLUE} from '../../constants'; import {ProfileStackParams} from '../../routes'; -import {SimplePostType, TwitterPostType} from '../../types'; +import {SimplePostType, TwitterPostType, SocialAccountType} from '../../types'; import {AvatarHeaderHeight, SCREEN_HEIGHT} from '../../utils'; +import {useSelector} from 'react-redux'; +import {RootState} from '../../store/rootReducer'; +import TaggLoadingIndicator from '../../components/common/TaggLoadingIndicator'; type SocialMediaTaggsRouteProp = RouteProp< ProfileStackParams, @@ -34,12 +43,27 @@ const SocialMediaTaggs: React.FC<SocialMediaTaggsProps> = ({ route, navigation, }) => { + const [accountData, setAccountData] = useState<SocialAccountType>({ + posts: [], + }); + const {socialMediaType, userXId, screenType} = route.params; + const { + avatar, + profile: {name}, + } = userXId + ? useSelector((state: RootState) => state.userX[screenType][userXId]) + : useSelector((state: RootState) => state.user); + + const {socialAccounts} = userXId + ? useSelector((state: RootState) => state.userX[screenType][userXId]) + : useSelector((state: RootState) => state.socialAccounts); - /** - * Since access to the loaded context is lost, we need the previous screen to pass in a whole lot of data to this screen. - * This calls for a better state management system such as Redux which allows to have a central repo for the app state. - */ - const {socialMediaType, name, accountData, avatar} = route.params; + useEffect(() => { + const currentSocialData = {...socialAccounts[socialMediaType]}; + if (currentSocialData) { + setAccountData(currentSocialData); + } + }, [socialAccounts, setAccountData]); useEffect(() => { navigation.setOptions({ @@ -58,33 +82,37 @@ const SocialMediaTaggs: React.FC<SocialMediaTaggsProps> = ({ colors={[AVATAR_GRADIENT.start, AVATAR_GRADIENT.end]}> <StatusBar barStyle={'light-content'} /> {/* Cropping the scroll view to mimic the presence of a header while preserving the gradient background */} - <View - // we want a slightly bigger header here for the avatar image - style={[styles.flex, {marginTop: AvatarHeaderHeight}]}> - <ScrollView - showsVerticalScrollIndicator={false} - contentContainerStyle={styles.contentContainer}> - <SocialMediaInfo - fullname={name} - type={socialMediaType} - handle={accountData?.handle} - /> - {(accountData?.posts as Array< - SimplePostType | TwitterPostType - >).map((post, index) => - socialMediaType === 'Twitter' ? ( - <TwitterTaggPost - key={index} - ownerHandle={accountData?.handle || '_'} - post={post as TwitterPostType} - /> - ) : ( - <TaggPost key={index} post={post as SimplePostType} /> - ), - )} - </ScrollView> - <TabsGradient /> - </View> + {accountData?.posts && accountData.posts.length > 0 ? ( + <View + // we want a slightly bigger header here for the avatar image + style={[styles.flex, {marginTop: AvatarHeaderHeight}]}> + <ScrollView + showsVerticalScrollIndicator={false} + contentContainerStyle={styles.contentContainer}> + <SocialMediaInfo + fullname={name} + type={socialMediaType} + handle={accountData?.handle} + /> + {(accountData?.posts as Array< + SimplePostType | TwitterPostType + >).map((post, index) => + socialMediaType === 'Twitter' ? ( + <TwitterTaggPost + key={index} + ownerHandle={accountData?.handle || '_'} + post={post as TwitterPostType} + /> + ) : ( + <TaggPost key={index} post={post as SimplePostType} /> + ), + )} + </ScrollView> + <TabsGradient /> + </View> + ) : ( + <TaggLoadingIndicator color="white" /> + )} </LinearGradient> ); }; diff --git a/src/screens/search/SearchScreen.tsx b/src/screens/search/SearchScreen.tsx index 535b964c..b11f6a1a 100644 --- a/src/screens/search/SearchScreen.tsx +++ b/src/screens/search/SearchScreen.tsx @@ -13,21 +13,27 @@ import { TabsGradient, } from '../../components'; import {SEARCH_ENDPOINT, TAGG_TEXT_LIGHT_BLUE} from '../../constants'; -import {AuthContext} from '../../routes/authentication'; -import {ProfilePreviewType, UserType} from '../../types'; +import {ProfilePreviewType, ScreenType, UserType} from '../../types'; import {SCREEN_HEIGHT, SCREEN_WIDTH, StatusBarHeight} from '../../utils'; const NO_USER: UserType = { userId: '', username: '', }; +import {RootState} from '../../store/rootReducer'; +import {useSelector, useDispatch} from 'react-redux'; +import {resetScreenType} from '../../store/actions'; +import {useFocusEffect} from '@react-navigation/native'; + /** * Search Screen for user recommendations and a search * tool to allow user to find other users */ const SearchScreen: React.FC = () => { - const {recentSearches, taggUsers} = React.useContext(AuthContext); + const {recentSearches, taggUsers} = useSelector( + (state: RootState) => state.taggUsers, + ); const [query, setQuery] = useState<string>(''); const [results, setResults] = useState<Array<ProfilePreviewType>>([]); const [recents, setRecents] = useState<Array<ProfilePreviewType>>( @@ -36,6 +42,7 @@ const SearchScreen: React.FC = () => { const [searching, setSearching] = useState(false); const top = Animated.useValue(-SCREEN_HEIGHT); const [user, setUser] = useState<UserType>(NO_USER); + useEffect(() => { if (query.length < 3) { setResults([]); @@ -69,6 +76,17 @@ const SearchScreen: React.FC = () => { loadResults(query); }, [query]); + const dispatch = useDispatch(); + + /** + * Code under useFocusEffect gets executed every time the screen comes under focus / is being viewed by the user. + * This is done to reset the users stored in our store for the Search screen. + * Read more here : https://reactnavigation.org/docs/function-after-focusing-screen/ + */ + useFocusEffect(() => { + dispatch(resetScreenType(ScreenType.Search)); + }); + const handleFocus = () => { const topInConfig = { duration: 180, @@ -130,7 +148,11 @@ const SearchScreen: React.FC = () => { /> {/* Removed for Alpha for now */} {/* <Explore /> */} - <DiscoverUsers sectionTitle="Discover Users" users={taggUsers} /> + <DiscoverUsers + sectionTitle="Discover Users" + users={taggUsers} + screenType={ScreenType.Search} + /> <SearchResultsBackground {...{top}}> {results.length === 0 && recents.length !== 0 ? ( <RecentSearches @@ -138,9 +160,14 @@ const SearchScreen: React.FC = () => { sectionButtonTitle="Clear all" onPress={clearRecentlySearched} recents={recents} + screenType={ScreenType.Search} /> ) : ( - <SearchResults {...{results}} previewType={'Search'} /> + <SearchResults + {...{results}} + previewType={'Search'} + screenType={ScreenType.Search} + /> )} </SearchResultsBackground> </ScrollView> diff --git a/src/services/BlockUserService.ts b/src/services/BlockUserService.ts index 56243729..21e259b6 100644 --- a/src/services/BlockUserService.ts +++ b/src/services/BlockUserService.ts @@ -3,11 +3,7 @@ import {Alert} from 'react-native'; import {BLOCK_USER_ENDPOINT} from '../constants'; -export const loadBlockedUsers = async ( - userId: string, - token: string, - callback: Function, -) => { +export const loadBlockedUsers = async (userId: string, token: string) => { try { const response = await fetch(BLOCK_USER_ENDPOINT + `?user_id=${userId}`, { method: 'GET', @@ -17,7 +13,7 @@ export const loadBlockedUsers = async ( }); if (response.status === 200) { const body = await response.json(); - callback(body); + return body; } else { throw new Error(await response.json()); } diff --git a/src/services/ExploreServices.ts b/src/services/ExploreServices.ts index 7c242d57..2181ea7d 100644 --- a/src/services/ExploreServices.ts +++ b/src/services/ExploreServices.ts @@ -1,9 +1,6 @@ import {ALL_USERS_ENDPOINT} from '../constants'; -export const getAllTaggUsers = async ( - token: string, - setTaggUsers: Function, -) => { +export const getAllTaggUsers = async (token: string) => { try { const response = await fetch(ALL_USERS_ENDPOINT, { method: 'GET', @@ -14,7 +11,7 @@ export const getAllTaggUsers = async ( const status = response.status; if (status === 200) { const response_data = await response.json(); - setTaggUsers(response_data); + return response_data; } else { console.log( 'Something went wrong! ðŸ˜', diff --git a/src/services/MomentServices.ts b/src/services/MomentServices.ts index bf846b1c..46ca1351 100644 --- a/src/services/MomentServices.ts +++ b/src/services/MomentServices.ts @@ -1,7 +1,7 @@ //Common moments api abstracted out here - -import {COMMENTS_ENDPOINT} from '../constants'; +import {COMMENTS_ENDPOINT, MOMENTS_ENDPOINT} from '../constants'; import {Alert} from 'react-native'; +import {MomentType} from '../types'; //Get all comments for a moment export const getMomentComments = async ( @@ -96,3 +96,30 @@ export const getMomentCommentsCount = async ( ); } }; + +export const loadMoments: ( + userId: string, + token: string, +) => Promise<MomentType[]> = async (userId, token) => { + let moments: MomentType[] = []; + try { + const response = await fetch(MOMENTS_ENDPOINT + '?user_id=' + userId, { + method: 'GET', + headers: { + Authorization: 'Token ' + token, + }, + }); + const status = response.status; + if (status === 200) { + const data = await response.json(); + moments = data; + } else { + console.log('Could not load moments!'); + return []; + } + } catch (err) { + console.log(err); + return []; + } + return moments; +}; diff --git a/src/services/UserFollowServices.ts b/src/services/UserFollowServices.ts index 105124bc..f0f176fc 100644 --- a/src/services/UserFollowServices.ts +++ b/src/services/UserFollowServices.ts @@ -8,13 +8,7 @@ import { FOLLOWING_ENDPOINT, } from '../constants'; -import {ProfilePreviewType} from 'src/types'; - -export const loadFollowers = async ( - userId: string, - token: string, - callback: Function, -) => { +export const loadFollowers = async (userId: string, token: string) => { try { const response = await fetch(FOLLOWERS_ENDPOINT + `?user_id=${userId}`, { method: 'GET', @@ -24,7 +18,7 @@ export const loadFollowers = async ( }); if (response.status === 200) { const body = await response.json(); - callback(body); + return body; } else { throw new Error(await response.json()); } @@ -33,11 +27,7 @@ export const loadFollowers = async ( } }; -export const loadFollowing = async ( - userId: string, - token: string, - callback: Function, -) => { +export const loadFollowing = async (userId: string, token: string) => { try { const response = await fetch(FOLLOWING_ENDPOINT + `?user_id=${userId}`, { method: 'GET', @@ -47,7 +37,7 @@ export const loadFollowing = async ( }); if (response.status === 200) { const body = await response.json(); - callback(body); + return body; } else { throw new Error(await response.json()); } diff --git a/src/services/UserProfileService.ts b/src/services/UserProfileService.ts index 38e04221..e69e4103 100644 --- a/src/services/UserProfileService.ts +++ b/src/services/UserProfileService.ts @@ -12,14 +12,9 @@ import { GET_IG_POSTS_ENDPOINT, GET_TWITTER_POSTS_ENDPOINT, PROFILE_INFO_ENDPOINT, - MOMENTS_ENDPOINT, } from '../constants'; -export const loadProfileInfo = async ( - token: string, - userId: string, - callback: Function, -) => { +export const loadProfileInfo = async (token: string, userId: string) => { try { const response = await fetch(PROFILE_INFO_ENDPOINT + `${userId}/`, { method: 'GET', @@ -33,7 +28,7 @@ export const loadProfileInfo = async ( let {name, biography, website, birthday, gender} = info; // user should always have a birthday, but a safety check here birthday = birthday && moment(birthday).format('YYYY-MM-DD'); - callback({name, biography, website, birthday, gender}); + return {name, biography, website, birthday, gender}; } } catch (error) { Alert.alert( @@ -43,11 +38,7 @@ export const loadProfileInfo = async ( } }; -export const loadAvatar = async ( - token: string, - userId: string, - callback: Function, -) => { +export const loadAvatar = async (token: string, userId: string) => { try { const response = await RNFetchBlob.config({ fileCache: true, @@ -57,20 +48,16 @@ export const loadAvatar = async ( }); const status = response.info().status; if (status === 200) { - callback(response.path()); + return response.path(); } else { - callback(''); + return ''; } } catch (error) { console.log(error); } }; -export const loadCover = async ( - token: string, - userId: string, - callback: Function, -) => { +export const loadCover = async (token: string, userId: string) => { try { let response = await RNFetchBlob.config({ fileCache: true, @@ -80,9 +67,9 @@ export const loadCover = async ( }); const status = response.info().status; if (status === 200) { - callback(response.path()); + return response.path(); } else { - callback(''); + return ''; } } catch (error) { console.log(error); @@ -124,37 +111,10 @@ export const loadSocialPosts: ( return accountData; }; -export const loadMoments: ( - userId: string, - token: string, -) => Promise<MomentType[]> = async (userId, token) => { - let moments: MomentType[] = []; - try { - const response = await fetch(MOMENTS_ENDPOINT + '?user_id=' + userId, { - method: 'GET', - headers: { - Authorization: 'Token ' + token, - }, - }); - const status = response.status; - if (status === 200) { - const data = await response.json(); - moments = data; - } else { - console.log('Could not load moments!'); - return []; - } - } catch (err) { - console.log(err); - return []; - } - return moments; -}; - -export const loadRecentlySearchedUsers = async (callback: Function) => { +export const loadRecentlySearchedUsers = async () => { try { const asyncCache = await AsyncStorage.getItem('@recently_searched_users'); - asyncCache != null ? callback(JSON.parse(asyncCache)) : null; + return asyncCache != null ? JSON.parse(asyncCache) : null; } catch (e) { console.log(e); } diff --git a/src/store/actions/index.ts b/src/store/actions/index.ts new file mode 100644 index 00000000..04fa9767 --- /dev/null +++ b/src/store/actions/index.ts @@ -0,0 +1,7 @@ +export * from './user'; +export * from './userFollow'; +export * from './userMoments'; +export * from './socials'; +export * from './taggUsers'; +export * from './userBlock'; +export * from './userX'; diff --git a/src/store/actions/socials.ts b/src/store/actions/socials.ts new file mode 100644 index 00000000..f79b4ad1 --- /dev/null +++ b/src/store/actions/socials.ts @@ -0,0 +1,38 @@ +import {RootState} from '../rootReducer'; +import {loadSocialPosts} from '../../services'; +import {Action, ThunkAction} from '@reduxjs/toolkit'; +import {userSocialsFetched, individualSocialfetched} from '../reducers'; +import {loadAllSocialsForUser} from '../../utils'; + +export const loadIndividualSocial = ( + userId: string, + socialType: string, +): ThunkAction<Promise<void>, RootState, unknown, Action<string>> => async ( + dispatch, +) => { + try { + const social = await loadSocialPosts(userId, socialType); + dispatch({ + type: individualSocialfetched.type, + payload: {socialType, social}, + }); + } catch (error) { + console.log(error); + } +}; + +export const loadAllSocials = ( + userId: string, +): ThunkAction<Promise<void>, RootState, unknown, Action<string>> => async ( + dispatch, +) => { + try { + const socials = await loadAllSocialsForUser(userId); + dispatch({ + type: userSocialsFetched.type, + payload: socials, + }); + } catch (error) { + console.log(error); + } +}; diff --git a/src/store/actions/taggUsers.ts b/src/store/actions/taggUsers.ts new file mode 100644 index 00000000..7f841c51 --- /dev/null +++ b/src/store/actions/taggUsers.ts @@ -0,0 +1,24 @@ +import {RootState} from '../rootReducer'; +import {loadRecentlySearchedUsers, getAllTaggUsers} from '../../services'; +import {Action, ThunkAction} from '@reduxjs/toolkit'; +import {taggUsersFetched} from '../reducers'; +import {getTokenOrLogout} from '../../utils'; + +export const loadRecentlySearched = (): ThunkAction< + Promise<void>, + RootState, + unknown, + Action<string> +> => async (dispatch) => { + try { + const token = await getTokenOrLogout(dispatch); + const recentSearches = await loadRecentlySearchedUsers(); + const taggUsers = await getAllTaggUsers(token); + dispatch({ + type: taggUsersFetched.type, + payload: {recentSearches, taggUsers}, + }); + } catch (error) { + console.log(error); + } +}; diff --git a/src/store/actions/user.ts b/src/store/actions/user.ts new file mode 100644 index 00000000..09ec8abf --- /dev/null +++ b/src/store/actions/user.ts @@ -0,0 +1,52 @@ +import {RootState} from '../rootReducer'; +import {UserType} from '../../types/types'; +import {loadProfileInfo, loadAvatar, loadCover} from '../../services'; +import {Action, ThunkAction} from '@reduxjs/toolkit'; +import {userLoggedIn, userDetailsFetched} from '../reducers'; +import {getTokenOrLogout} from '../../utils'; + +/** + * Entry point to our store. + * Thunk allows us to make async API calls and hence is responsible to fetch data from server. + */ + +/** + * Lets understand Thunk. + * https://bloggie.io/@_ChristineOo/understanding-typings-of-redux-thunk-action + * https://github.com/reduxjs/redux-thunk + */ + +export const loadUserData = ( + user: UserType, +): ThunkAction<Promise<void>, RootState, unknown, Action<string>> => async ( + dispatch, +) => { + try { + await dispatch({type: userLoggedIn.type, payload: user}); + const token = await getTokenOrLogout(dispatch); + const [profile, avatar, cover] = await Promise.all([ + loadProfileInfo(token, user.userId), + loadAvatar(token, user.userId), + loadCover(token, user.userId), + ]); + dispatch({ + type: userDetailsFetched.type, + payload: {profile, cover, avatar}, + }); + } catch (error) { + console.log(error); + } +}; + +export const logout = (): ThunkAction< + Promise<void>, + RootState, + unknown, + Action<string> +> => async (dispatch) => { + try { + dispatch({type: userLoggedIn.type, payload: {userId: '', username: ''}}); + } catch (error) { + console.log(error); + } +}; diff --git a/src/store/actions/userBlock.ts b/src/store/actions/userBlock.ts new file mode 100644 index 00000000..f903e99e --- /dev/null +++ b/src/store/actions/userBlock.ts @@ -0,0 +1,48 @@ +import {RootState} from '../rootReducer'; +import {ProfilePreviewType, UserType} from '../../types/types'; +import {blockOrUnblockUser, loadBlockedUsers} from '../../services'; +import {Action, ThunkAction} from '@reduxjs/toolkit'; +import {userBlockFetched, updateBlockedList, userLoggedIn} from '../reducers'; +import {getTokenOrLogout} from '../../utils'; + +export const loadBlockedList = ( + userId: string, +): ThunkAction<Promise<void>, RootState, unknown, Action<string>> => async ( + dispatch, +) => { + try { + const token = await getTokenOrLogout(dispatch); + const blocked = await loadBlockedUsers(userId, token); + dispatch({ + type: userBlockFetched.type, + payload: blocked, + }); + } catch (error) { + console.log(error); + } +}; + +export const blockUnblockUser = ( + blocker: UserType, + blocked: ProfilePreviewType, + isBlocked: boolean, +): ThunkAction<Promise<void>, RootState, unknown, Action<string>> => async ( + dispatch, +) => { + const token = await getTokenOrLogout(dispatch); + const success = blockOrUnblockUser( + blocker.userId, + blocked.id, + token, + isBlocked, + ); + if (success) { + dispatch({ + type: updateBlockedList.type, + payload: { + isBlocked, + data: blocked, + }, + }); + } +}; diff --git a/src/store/actions/userFollow.ts b/src/store/actions/userFollow.ts new file mode 100644 index 00000000..e23bbfc0 --- /dev/null +++ b/src/store/actions/userFollow.ts @@ -0,0 +1,57 @@ +import {getTokenOrLogout} from './../../utils'; +import {RootState} from '../rootReducer'; +import {ProfilePreviewType, UserType} from '../../types/types'; +import { + followOrUnfollowUser, + loadFollowers, + loadFollowing, +} from '../../services'; +import {Action, ThunkAction} from '@reduxjs/toolkit'; +import {userFollowFetched, updateFollowing, userLoggedIn} from '../reducers'; + +export const loadFollowData = ( + userId: string, +): ThunkAction<Promise<void>, RootState, unknown, Action<string>> => async ( + dispatch, +) => { + try { + const token = await getTokenOrLogout(dispatch); + const followers = await loadFollowers(userId, token); + const following = await loadFollowing(userId, token); + dispatch({ + type: userFollowFetched.type, + payload: {followers, following}, + }); + } catch (error) { + console.log(error); + } +}; + +export const followUnfollowUser = ( + follower: UserType, + followed: ProfilePreviewType, + isFollowed: boolean, +): ThunkAction<Promise<void>, RootState, unknown, Action<string>> => async ( + dispatch, +) => { + try { + const token = await getTokenOrLogout(dispatch); + const success = await followOrUnfollowUser( + follower.userId, + followed.id, + token, + isFollowed, + ); + if (success) { + dispatch({ + type: updateFollowing.type, + payload: { + isFollowed, + data: followed, + }, + }); + } + } catch (error) { + console.log(error); + } +}; diff --git a/src/store/actions/userMoments.ts b/src/store/actions/userMoments.ts new file mode 100644 index 00000000..dce8da8a --- /dev/null +++ b/src/store/actions/userMoments.ts @@ -0,0 +1,22 @@ +import {RootState} from '../rootReducer'; +import {loadMoments} from '../../services'; +import {Action, ThunkAction} from '@reduxjs/toolkit'; +import {userMomentsFetched} from '../reducers'; +import {getTokenOrLogout} from '../../utils'; + +export const loadUserMoments = ( + userId: string, +): ThunkAction<Promise<void>, RootState, unknown, Action<string>> => async ( + dispatch, +) => { + try { + const token = await getTokenOrLogout(dispatch); + const moments = await loadMoments(userId, token); + dispatch({ + type: userMomentsFetched.type, + payload: moments, + }); + } catch (error) { + console.log(error); + } +}; diff --git a/src/store/actions/userX.ts b/src/store/actions/userX.ts new file mode 100644 index 00000000..5468f762 --- /dev/null +++ b/src/store/actions/userX.ts @@ -0,0 +1,131 @@ +import {userXInStore} from './../../utils/'; +import {getTokenOrLogout, loadAllSocialsForUser} from './../../utils'; +import {UserType, ScreenType, ProfilePreviewType} from '../../types/types'; +import {RootState} from '../rootReducer'; +import {Action, ThunkAction} from '@reduxjs/toolkit'; +import { + userXRequested, + userXAvatarFetched, + userXFollowersFetched, + userXFollowingFetched, + userXCoverFetched, + userXMomentsFetched, + userXProfileFetched, + userXSocialsFetched, + userXUserFetched, + resetScreen, +} from '../reducers'; +import { + loadProfileInfo, + loadAvatar, + loadCover, + loadFollowers, + loadFollowing, + loadMoments, +} from '../../services'; + +export const loadUserX = ( + user: UserType, + screenType: ScreenType, +): ThunkAction<Promise<void>, RootState, unknown, Action<string>> => async ( + dispatch, +) => { + try { + const {userId} = user; + await dispatch({type: userXRequested.type, payload: {screenType, userId}}); + await dispatch({ + type: userXUserFetched.type, + payload: {screenType, userId, user}, + }); + const token = await getTokenOrLogout(dispatch); + loadProfileInfo(token, userId).then((data) => + dispatch({ + type: userXProfileFetched.type, + payload: {screenType, userId, data}, + }), + ); + loadAllSocialsForUser(userId).then((data) => + dispatch({ + type: userXSocialsFetched.type, + payload: {screenType, userId, data}, + }), + ); + loadAvatar(token, userId).then((data) => + dispatch({ + type: userXAvatarFetched.type, + payload: {screenType, userId, data}, + }), + ); + loadCover(token, userId).then((data) => + dispatch({ + type: userXCoverFetched.type, + payload: {screenType, userId, data}, + }), + ); + loadFollowers(userId, token).then((data) => + dispatch({ + type: userXFollowersFetched.type, + payload: {screenType, userId, data}, + }), + ); + loadFollowing(userId, token).then((data) => + dispatch({ + type: userXFollowingFetched.type, + payload: {screenType, userId, data}, + }), + ); + loadMoments(userId, token).then((data) => + dispatch({ + type: userXMomentsFetched.type, + payload: {screenType, userId, data}, + }), + ); + } catch (error) { + console.log(error); + } +}; + +export const updateUserXFollowersAndFollowing = ( + userId: string, + state: RootState, +): ThunkAction<Promise<void>, RootState, unknown, Action<string>> => async ( + dispatch, +) => { + try { + const screens = <ScreenType[]>[ScreenType.Profile, ScreenType.Search]; + const token = await getTokenOrLogout(dispatch); + screens.forEach((screenType) => { + if (userXInStore(state, screenType, userId)) { + loadFollowers(userId, token).then((data) => + dispatch({ + type: userXFollowersFetched.type, + payload: {screenType, userId, data}, + }), + ); + loadFollowing(userId, token).then((data) => + dispatch({ + type: userXFollowingFetched.type, + payload: {screenType, userId, data}, + }), + ); + } + }); + } catch (error) { + console.log(error); + } +}; + +export const resetScreenType = ( + screenType: ScreenType, +): ThunkAction<Promise<void>, RootState, unknown, Action<string>> => async ( + dispatch, +) => { + try { + dispatch({ + type: resetScreen.type, + payload: {screenType}, + }); + } catch (error) { + console.log(error); + } +}; diff --git a/src/store/configureStore.ts b/src/store/configureStore.ts new file mode 100644 index 00000000..7689af5c --- /dev/null +++ b/src/store/configureStore.ts @@ -0,0 +1,20 @@ +import {configureStore, getDefaultMiddleware} from '@reduxjs/toolkit'; +import reducer from './rootReducer'; + +/** + * The entry point to our store + * getDefaultMiddleware : This returns an array of default middlewares like Thunk (used for async calls) + */ + +const store = configureStore({ + reducer, + middleware: [...getDefaultMiddleware()], +}); + +/** + * The exports below come in handy when dispatching from a file outside of any of the Child component's + */ +export type AppDispatch = typeof store.dispatch; +export type GetState = typeof store.getState; + +export default store; diff --git a/src/store/initialStates.ts b/src/store/initialStates.ts new file mode 100644 index 00000000..4087b97c --- /dev/null +++ b/src/store/initialStates.ts @@ -0,0 +1,95 @@ +import {MomentType} from 'src/types'; +import { + ProfileType, + SocialAccountType, + ProfilePreviewType, + ScreenType, + UserXType, + UserType, +} from '../types'; + +export const NO_PROFILE: ProfileType = { + biography: '', + website: '', + name: '', + gender: '', + birthday: undefined, +}; + +export const EMPTY_MOMENTS_LIST = <MomentType[]>[]; + +export const NO_USER: UserType = { + userId: '', + username: '', +}; + +export const EMPTY_PROFILE_PREVIEW_LIST = <ProfilePreviewType[]>[]; + +export const NO_USER_DATA = { + user: <UserType>NO_USER, + profile: <ProfileType>NO_PROFILE, + avatar: <string | null>'', + cover: <string | null>'', +}; + +export const NO_FOLLOW_DATA = { + followers: EMPTY_PROFILE_PREVIEW_LIST, + following: EMPTY_PROFILE_PREVIEW_LIST, +}; + +export const NO_MOMENTS = { + moments: EMPTY_MOMENTS_LIST, +}; + +export const NO_SOCIAL_ACCOUNTS: Record<string, SocialAccountType> = { + Instagram: {posts: []}, + Facebook: {posts: []}, + Twitter: {posts: []}, +}; + +export const NO_TAGG_USERS = { + recentSearches: EMPTY_PROFILE_PREVIEW_LIST, + taggUsers: EMPTY_PROFILE_PREVIEW_LIST, +}; + +export const NO_SOCIALS = { + socialAccounts: NO_SOCIAL_ACCOUNTS, +}; + +export const NO_BLOCKED_USERS = { + blockedUsers: EMPTY_PROFILE_PREVIEW_LIST, +}; + +/** + * The dummy userId and username serve the purpose of preventing app crash + * For instance, if it may happen that data in our store is not loaded yet for the userXId being visited. + * Then we will set the userXId / username to this dummy username / userid + */ +export const DUMMY_USERID = 'ID-1234-567'; +export const DUMMY_USERNAME = 'tagg_userX'; + +export const EMPTY_USER_X = <UserXType>{ + followers: EMPTY_PROFILE_PREVIEW_LIST, + following: EMPTY_PROFILE_PREVIEW_LIST, + moments: EMPTY_MOMENTS_LIST, + socialAccounts: NO_SOCIAL_ACCOUNTS, + user: NO_USER, + profile: NO_PROFILE, + avatar: '', + cover: '', +}; + +/** + * A dummy userX to always be there in out initial app state + */ +export const EMPTY_USERX_LIST = <Record<string, UserXType>>{ + [DUMMY_USERID]: EMPTY_USER_X, +}; + +export const EMPTY_SCREEN_TO_USERS_LIST: Record< + ScreenType, + Record<string, UserXType> +> = { + [ScreenType.Profile]: EMPTY_USERX_LIST, + [ScreenType.Search]: EMPTY_USERX_LIST, +}; diff --git a/src/store/reducers/index.ts b/src/store/reducers/index.ts new file mode 100644 index 00000000..0e378bc5 --- /dev/null +++ b/src/store/reducers/index.ts @@ -0,0 +1,7 @@ +export * from './userFollowReducer'; +export * from './userReducer'; +export * from './userMomentsReducer'; +export * from './userSocialsReducer'; +export * from './taggUsersReducer'; +export * from './userBlockReducer'; +export * from './userXReducer'; diff --git a/src/store/reducers/taggUsersReducer.ts b/src/store/reducers/taggUsersReducer.ts new file mode 100644 index 00000000..ff30f7a0 --- /dev/null +++ b/src/store/reducers/taggUsersReducer.ts @@ -0,0 +1,16 @@ +import {NO_TAGG_USERS} from '../initialStates'; +import {createSlice} from '@reduxjs/toolkit'; + +const taggUsersSlice = createSlice({ + name: 'taggUsers', + initialState: NO_TAGG_USERS, + reducers: { + taggUsersFetched: (state, action) => { + state.recentSearches = action.payload.taggUsers; + state.taggUsers = action.payload.taggUsers; + }, + }, +}); + +export const {taggUsersFetched} = taggUsersSlice.actions; +export const taggUsersReducer = taggUsersSlice.reducer; diff --git a/src/store/reducers/userBlockReducer.ts b/src/store/reducers/userBlockReducer.ts new file mode 100644 index 00000000..90e4a04a --- /dev/null +++ b/src/store/reducers/userBlockReducer.ts @@ -0,0 +1,25 @@ +import {createSlice} from '@reduxjs/toolkit'; +import {NO_BLOCKED_USERS} from '../initialStates'; + +const userBlockSlice = createSlice({ + name: 'userBlock', + initialState: NO_BLOCKED_USERS, + reducers: { + userBlockFetched: (state, action) => { + state.blockedUsers = action.payload; + }, + + updateBlockedList: (state, action) => { + const {isBlocked, data} = action.payload; + if (!isBlocked) state.blockedUsers.push(data); + else { + state.blockedUsers = state.blockedUsers.filter( + (user) => user.username != data.username, + ); + } + }, + }, +}); + +export const {userBlockFetched, updateBlockedList} = userBlockSlice.actions; +export const userBlockReducer = userBlockSlice.reducer; diff --git a/src/store/reducers/userFollowReducer.ts b/src/store/reducers/userFollowReducer.ts new file mode 100644 index 00000000..55e16532 --- /dev/null +++ b/src/store/reducers/userFollowReducer.ts @@ -0,0 +1,27 @@ +import {createSlice} from '@reduxjs/toolkit'; +import {act} from 'react-test-renderer'; +import {NO_FOLLOW_DATA} from '../initialStates'; + +const userFollowSlice = createSlice({ + name: 'userFollow', + initialState: NO_FOLLOW_DATA, + reducers: { + userFollowFetched: (state, action) => { + state.followers = action.payload.followers; + state.following = action.payload.following; + }, + + updateFollowing: (state, action) => { + const {isFollowed, data} = action.payload; + if (!isFollowed) state.following.push(data); + else { + state.following = state.following.filter( + (follow) => follow.username !== data.username, + ); + } + }, + }, +}); + +export const {userFollowFetched, updateFollowing} = userFollowSlice.actions; +export const userFollowReducer = userFollowSlice.reducer; diff --git a/src/store/reducers/userMomentsReducer.ts b/src/store/reducers/userMomentsReducer.ts new file mode 100644 index 00000000..456ca2fa --- /dev/null +++ b/src/store/reducers/userMomentsReducer.ts @@ -0,0 +1,15 @@ +import {createSlice} from '@reduxjs/toolkit'; +import {NO_MOMENTS} from '../initialStates'; + +const userMomentsSlice = createSlice({ + name: 'userMoments', + initialState: NO_MOMENTS, + reducers: { + userMomentsFetched: (state, action) => { + state.moments = action.payload; + }, + }, +}); + +export const {userMomentsFetched} = userMomentsSlice.actions; +export const userMomentsReducer = userMomentsSlice.reducer; diff --git a/src/store/reducers/userReducer.ts b/src/store/reducers/userReducer.ts new file mode 100644 index 00000000..f43bd0bc --- /dev/null +++ b/src/store/reducers/userReducer.ts @@ -0,0 +1,36 @@ +import {createSlice, Action} from '@reduxjs/toolkit'; +import {NO_USER_DATA} from '../initialStates'; + +/** + * A reducer is a pure function with the sole responsibility of updating the state and nothing else. + * No side effects are allowed. + */ + +/** + * Actions are a way to indicate what just happened / what is going to happen and update the state accordingly. + */ + +/** + * Create slice allows us + * To initialise State , create Actions and Reducers in one go + * Read more here https://redux.js.org/introduction/installation + */ + +const userDataSlice = createSlice({ + name: 'userData', + initialState: NO_USER_DATA, + reducers: { + userLoggedIn: (state, action) => { + state.user = action.payload; + }, + + userDetailsFetched: (state, action) => { + state.profile = action.payload.profile; + state.avatar = action.payload.avatar; + state.cover = action.payload.cover; + }, + }, +}); + +export const {userLoggedIn, userDetailsFetched} = userDataSlice.actions; +export const userDataReducer = userDataSlice.reducer; diff --git a/src/store/reducers/userSocialsReducer.ts b/src/store/reducers/userSocialsReducer.ts new file mode 100644 index 00000000..de79568c --- /dev/null +++ b/src/store/reducers/userSocialsReducer.ts @@ -0,0 +1,21 @@ +import {createSlice} from '@reduxjs/toolkit'; +import {NO_SOCIALS} from '../initialStates'; + +const userSocialsSlice = createSlice({ + name: 'userSocials', + initialState: NO_SOCIALS, + reducers: { + individualSocialfetched: (state, actions) => { + state.socialAccounts[actions.payload.socialType] = actions.payload.social; + }, + userSocialsFetched: (state, action) => { + state.socialAccounts = action.payload; + }, + }, +}); + +export const { + userSocialsFetched, + individualSocialfetched, +} = userSocialsSlice.actions; +export const userSocialsReducer = userSocialsSlice.reducer; diff --git a/src/store/reducers/userXReducer.ts b/src/store/reducers/userXReducer.ts new file mode 100644 index 00000000..154dd7dc --- /dev/null +++ b/src/store/reducers/userXReducer.ts @@ -0,0 +1,77 @@ +import {ScreenType} from '../../types/types'; +import {EMPTY_SCREEN_TO_USERS_LIST, EMPTY_USER_X} from '../initialStates'; +import {createSlice} from '@reduxjs/toolkit'; + +const userXSlice = createSlice({ + name: 'userX', + initialState: EMPTY_SCREEN_TO_USERS_LIST, + reducers: { + userXRequested: (state, action) => { + state[<ScreenType>action.payload.screenType][ + action.payload.userId + ] = EMPTY_USER_X; + }, + + userXProfileFetched: (state, action) => { + state[<ScreenType>action.payload.screenType][ + action.payload.userId + ].profile = action.payload.data; + }, + + userXUserFetched: (state, action) => { + state[<ScreenType>action.payload.screenType][action.payload.userId].user = + action.payload.user; + }, + + userXMomentsFetched: (state, action) => { + state[<ScreenType>action.payload.screenType][ + action.payload.userId + ].moments = action.payload.data; + }, + userXFollowersFetched: (state, action) => { + state[<ScreenType>action.payload.screenType][ + action.payload.userId + ].followers = action.payload.data; + }, + userXFollowingFetched: (state, action) => { + state[<ScreenType>action.payload.screenType][ + action.payload.userId + ].following = action.payload.data; + }, + userXAvatarFetched: (state, action) => { + state[<ScreenType>action.payload.screenType][ + action.payload.userId + ].avatar = action.payload.data; + }, + userXCoverFetched: (state, action) => { + state[<ScreenType>action.payload.screenType][ + action.payload.userId + ].cover = action.payload.data; + }, + userXSocialsFetched: (state, action) => { + state[<ScreenType>action.payload.screenType][ + action.payload.userId + ].socialAccounts = action.payload.data; + }, + + resetScreen: (state, action) => { + for (let userId in state[<ScreenType>action.payload.screenType]) { + state[<ScreenType>action.payload.screenType][userId] = EMPTY_USER_X; + } + }, + }, +}); + +export const { + userXUserFetched, + userXRequested, + userXAvatarFetched, + userXFollowersFetched, + userXFollowingFetched, + userXCoverFetched, + userXMomentsFetched, + userXProfileFetched, + userXSocialsFetched, + resetScreen, +} = userXSlice.actions; +export const userXReducer = userXSlice.reducer; diff --git a/src/store/rootReducer.ts b/src/store/rootReducer.ts new file mode 100644 index 00000000..695ed8c7 --- /dev/null +++ b/src/store/rootReducer.ts @@ -0,0 +1,30 @@ +import {combineReducers} from 'redux'; +import { + userDataReducer, + userSocialsReducer, + userFollowReducer, + userMomentsReducer, + taggUsersReducer, + userBlockReducer, + userXReducer, +} from './reducers'; + +/** + * Combine all the reducers in our application. + */ + +const rootReducer = combineReducers({ + user: userDataReducer, + follow: userFollowReducer, + moments: userMomentsReducer, + socialAccounts: userSocialsReducer, + taggUsers: taggUsersReducer, + blocked: userBlockReducer, + userX: userXReducer, +}); + +/** + * This RootState export is needed when a component subscribes to a slice of the state. + */ +export type RootState = ReturnType<typeof rootReducer>; +export default rootReducer; diff --git a/src/types/types.ts b/src/types/types.ts index 7d0cf318..56ec0fd0 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -87,3 +87,26 @@ export interface CommentType { } export type PreviewType = 'Comment' | 'Search' | 'Recent' | 'Discover Users'; + +export enum ScreenType { + Profile, + Search, +} + +/** + * Redux store to have a Record of ScreenType (Search, Profile, Home etc) mapped to + * A Record of userIXd mapped to UserXType + * This combined information will go in to the redux store like : Record<ScreenType, Array<Record<string, UserXType>> + * We will call this slice of store userX + * We reset information on this record as soon as the stack corresponding to the screen is reset. + */ +export interface UserXType { + followers: ProfilePreviewType[]; + following: ProfilePreviewType[]; + moments: MomentType[]; + socialAccounts: Record<string, SocialAccountType>; + user: UserType; + profile: ProfileType; + avatar: string; + cover: string; +} diff --git a/src/utils/hooks.ts b/src/utils/hooks.ts new file mode 100644 index 00000000..3914ef48 --- /dev/null +++ b/src/utils/hooks.ts @@ -0,0 +1,31 @@ +import AsyncStorage from '@react-native-community/async-storage'; +import {useEffect, useState} from 'react'; + +export const useAsyncStorage = (key: string, defaultValue: string) => { + const [storedValue, setStoredValue] = useState<string>(defaultValue); + + const getStoredItem = async (key: string, defaultValue: string) => { + try { + const item = await AsyncStorage.getItem(key); + const value = item ? item : defaultValue; + setStoredValue(value); + } catch (error) { + console.log(error); + } + }; + + useEffect(() => { + getStoredItem(key, defaultValue); + }, [key, defaultValue]); + + const setValue = async (value: string) => { + try { + setStoredValue(value); + await AsyncStorage.setItem(key, value); + } catch (error) { + console.log(error); + } + }; + + return [storedValue, setValue]; +}; diff --git a/src/utils/index.ts b/src/utils/index.ts index 95449c54..f5352af1 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -2,3 +2,4 @@ export * from './screenDimensions'; export * from './statusBarHeight'; export * from './moments'; export * from './common'; +export * from './users'; diff --git a/src/utils/users.ts b/src/utils/users.ts new file mode 100644 index 00000000..30b9d77b --- /dev/null +++ b/src/utils/users.ts @@ -0,0 +1,97 @@ +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 { + loadAllSocials, + loadBlockedList, + loadFollowData, + loadRecentlySearched, + loadUserData, + loadUserMoments, +} from '../store/actions'; +import {NO_SOCIAL_ACCOUNTS} from '../store/initialStates'; +import {loadSocialPosts} from '../services'; +import {userLoggedIn} from '../store/reducers'; + +const loadData = async (dispatch: AppDispatch, user: UserType) => { + await Promise.all([ + dispatch(loadUserData(user)), + dispatch(loadFollowData(user.userId)), + dispatch(loadUserMoments(user.userId)), + dispatch(loadAllSocials(user.userId)), + dispatch(loadBlockedList(user.userId)), + dispatch(loadRecentlySearched()), + ]); +}; + +/** + * This tries to log the user in present with the AsyncStorage if user.userId is empty + * Else it tries to login the user passed in + * @param dispatch This is the dispatch object from the redux store + * @param user The user if at all any + */ +export const userLogin = async (dispatch: AppDispatch, user: UserType) => { + try { + let localUser = {...user}; + if (!user.userId) { + const [id, username, token] = await Promise.all([ + AsyncStorage.getItem('userId'), + AsyncStorage.getItem('username'), + AsyncStorage.getItem('token'), + ]); + if (id && username && token) { + localUser = {...localUser, userId: id, username: username}; + } else { + return; + } + } + await loadData(dispatch, localUser); + } catch (error) { + console.log(error); + } +}; + +/** + * This function checks if the userX slice of our store contains the given user for the provided Screen + */ +export const userXInStore = ( + state: RootState, + screen: ScreenType, + userId: string, +) => { + const userX = state.userX[screen]; + return userId in userX && userX[userId].user.userId; +}; + +/** + * Abstracted the code to laod all socials out. + * @param userId userId for whom socials should be fetched + */ +export const loadAllSocialsForUser = async (userId: string) => { + let socials = NO_SOCIAL_ACCOUNTS; + try { + let socialNeedsUpdate = INTEGRATED_SOCIAL_LIST; + for (let socialType of socialNeedsUpdate) { + const social = await loadSocialPosts(userId, socialType); + socials = {...socials, [socialType]: social}; + } + return socials; + } catch (error) { + console.log(error); + } +}; + +/** + * Push the user out of system if token is not present in async storage + * @param dispatch + */ +export const getTokenOrLogout = async (dispatch: Function): Promise<string> => { + const token = await AsyncStorage.getItem('token'); + if (!token) { + dispatch({type: userLoggedIn.type, payload: {userId: '', username: ''}}); + return ''; + } + return token; +}; |