diff options
-rw-r--r-- | src/components/common/DateLabel.tsx | 58 | ||||
-rw-r--r-- | src/components/common/index.ts | 1 | ||||
-rw-r--r-- | src/components/onboarding/SocialMediaLinker.tsx | 17 | ||||
-rw-r--r-- | src/components/profile/Feed.tsx | 30 | ||||
-rw-r--r-- | src/components/profile/index.ts | 1 | ||||
-rw-r--r-- | src/components/taggs/SocialMediaInfo.tsx | 8 | ||||
-rw-r--r-- | src/components/taggs/Tagg.tsx | 5 | ||||
-rw-r--r-- | src/components/taggs/TaggPost.tsx | 63 | ||||
-rw-r--r-- | src/components/taggs/TaggPostFooter.tsx | 29 | ||||
-rw-r--r-- | src/components/taggs/TaggsBar.tsx | 35 | ||||
-rw-r--r-- | src/components/taggs/TaggsFeed.tsx | 29 | ||||
-rw-r--r-- | src/components/taggs/TwitterTaggPost.tsx | 213 | ||||
-rw-r--r-- | src/components/taggs/index.ts | 2 | ||||
-rw-r--r-- | src/constants/api.ts | 2 | ||||
-rw-r--r-- | src/routes/authentication/AuthProvider.tsx | 68 | ||||
-rw-r--r-- | src/routes/profile/Profile.tsx | 2 | ||||
-rw-r--r-- | src/routes/viewProfile/ProfileProvider.tsx | 49 | ||||
-rw-r--r-- | src/screens/profile/SocialMediaTaggs.tsx | 43 | ||||
-rw-r--r-- | src/services/UserProfileService.ts | 26 | ||||
-rw-r--r-- | src/types/types.ts | 35 |
20 files changed, 539 insertions, 177 deletions
diff --git a/src/components/common/DateLabel.tsx b/src/components/common/DateLabel.tsx new file mode 100644 index 00000000..145c614c --- /dev/null +++ b/src/components/common/DateLabel.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import {StyleSheet, Text} from 'react-native'; +import moment from 'moment'; + +interface DateLabelProps { + timestamp: string; + type: 'default' | 'short' | 'small'; + decorate?: (date: string) => string; +} + +const DateLabel: React.FC<DateLabelProps> = ({ + timestamp, + type, + decorate = (date) => `${date}`, +}) => { + let parsedDate = moment(timestamp); + + if (!parsedDate) { + return <React.Fragment />; + } + + switch (type) { + case 'default': + return ( + <Text style={styles.default}> + {decorate(parsedDate.format('h:mm a • MMM D, YYYY'))} + </Text> + ); + + case 'short': + return ( + <Text style={styles.default}> + {decorate(parsedDate.format('MMM D'))} + </Text> + ); + + case 'small': + return ( + <Text style={styles.smallAndBlue}> + {decorate(parsedDate.format('MMM D'))} + </Text> + ); + } +}; + +const styles = StyleSheet.create({ + default: { + fontSize: 15, + color: '#c4c4c4', + }, + smallAndBlue: { + fontSize: 14, + fontWeight: 'bold', + color: '#8FA9C2', + }, +}); + +export default DateLabel; diff --git a/src/components/common/index.ts b/src/components/common/index.ts index cb9d641b..cd72a70b 100644 --- a/src/components/common/index.ts +++ b/src/components/common/index.ts @@ -8,4 +8,5 @@ export {default as SocialIcon} from './SocialIcon'; export {default as TabsGradient} from './TabsGradient'; export {default as RecentSearches} from '../search/RecentSearches'; export {default as LoadingIndicator} from './LoadingIndicator'; +export {default as DateLabel} from './DateLabel'; export * from './post'; diff --git a/src/components/onboarding/SocialMediaLinker.tsx b/src/components/onboarding/SocialMediaLinker.tsx index e7f78834..15afb731 100644 --- a/src/components/onboarding/SocialMediaLinker.tsx +++ b/src/components/onboarding/SocialMediaLinker.tsx @@ -28,7 +28,6 @@ interface SocialMediaLinkerProps extends TouchableOpacityProps { const SocialMediaLinker: React.FC<SocialMediaLinkerProps> = ({ social: {label}, }) => { - const [state, setState] = React.useState({ authenticated: false, }); @@ -36,7 +35,7 @@ const SocialMediaLinker: React.FC<SocialMediaLinkerProps> = ({ const integrated_endpoints: {[label: string]: [string, string]} = { Instagram: [LINK_IG_OAUTH, LINK_IG_ENDPOINT], Facebook: [LINK_FB_OAUTH, LINK_FB_ENDPOINT], - Twitter: [LINK_TWITTER_OAUTH, LINK_TWITTER_ENDPOINT] + Twitter: [LINK_TWITTER_OAUTH, LINK_TWITTER_ENDPOINT], }; const registerSocialLink: (token: string) => Promise<boolean> = async ( @@ -71,7 +70,7 @@ const SocialMediaLinker: React.FC<SocialMediaLinkerProps> = ({ Alert.alert('Coming soon!'); return; } - let url = integrated_endpoints[label][0] + let url = integrated_endpoints[label][0]; // We will need to do an extra step for twitter sign-in if (label === 'Twitter') { @@ -79,10 +78,10 @@ const SocialMediaLinker: React.FC<SocialMediaLinkerProps> = ({ const response = await fetch(url, { method: 'GET', headers: { - Authorization: `Token ${user_token}` - } + Authorization: `Token ${user_token}`, + }, }); - url = response.url + url = response.url; } if (isAvailable) { @@ -90,7 +89,7 @@ const SocialMediaLinker: React.FC<SocialMediaLinkerProps> = ({ ephemeralWebSession: true, }) .then(async (response) => { - console.log(response) + console.log(response); if (response.type === 'success' && response.url) { const success = await registerSocialLink(response.url); if (!success) { @@ -107,9 +106,7 @@ const SocialMediaLinker: React.FC<SocialMediaLinkerProps> = ({ }) .catch((error) => { console.log(error); - Alert.alert( - `Something went wrong, we can't link with ${label} 😔`, - ); + Alert.alert(`Something went wrong, we can't link with ${label} 😔`); }); } else { // Okay... to open an external browser and have it link back to diff --git a/src/components/profile/Feed.tsx b/src/components/profile/Feed.tsx deleted file mode 100644 index 3353d25b..00000000 --- a/src/components/profile/Feed.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import {PostType, UserType} from '../../types'; -import {Post} from '../common'; -import {AuthContext} from '../../routes/authentication'; - -interface FeedProps { - user: UserType; -} -const Feed: React.FC<FeedProps> = ({user}) => { - const {instaPosts} = React.useContext(AuthContext); - const posts: Array<PostType> = []; - for (let i = 0; i < 10; i++) { - const testPost: PostType = { - owner: user, - social: 'Instagram', - socialHandle: 'igHandle', - data: instaPosts[i], - }; - posts.push(testPost); - } - return ( - <> - {posts.map((post, index) => ( - <Post key={index} post={post} /> - ))} - </> - ); -}; - -export default Feed; diff --git a/src/components/profile/index.ts b/src/components/profile/index.ts index 4eb435df..e2063e26 100644 --- a/src/components/profile/index.ts +++ b/src/components/profile/index.ts @@ -4,5 +4,4 @@ export {default as Moment} from './Moment'; export {default as ProfileCutout} from './ProfileCutout'; export {default as ProfileBody} from './ProfileBody'; export {default as ProfileHeader} from './ProfileHeader'; -export {default as Feed} from './Feed'; export {default as CaptionScreenHeader} from './CaptionScreenHeader'; diff --git a/src/components/taggs/SocialMediaInfo.tsx b/src/components/taggs/SocialMediaInfo.tsx index 0e93660d..a7ed6fe6 100644 --- a/src/components/taggs/SocialMediaInfo.tsx +++ b/src/components/taggs/SocialMediaInfo.tsx @@ -5,7 +5,7 @@ import {SocialIcon} from '..'; interface SocialMediaInfoProps { fullname: string; type: string; - handle: string; + handle?: string; } const SocialMediaInfo: React.FC<SocialMediaInfoProps> = ({ @@ -15,7 +15,11 @@ const SocialMediaInfo: React.FC<SocialMediaInfoProps> = ({ }) => { return ( <View style={styles.container}> - <Text style={styles.handle}> @{handle} </Text> + {handle && type !== 'Facebook' ? ( + <Text style={styles.handle}> @{handle} </Text> + ) : ( + <></> + )} <View style={styles.row}> <View /> <SocialIcon style={styles.icon} social={type} /> diff --git a/src/components/taggs/Tagg.tsx b/src/components/taggs/Tagg.tsx index d6cffee5..9274e0eb 100644 --- a/src/components/taggs/Tagg.tsx +++ b/src/components/taggs/Tagg.tsx @@ -6,17 +6,18 @@ import {TAGGS_GRADIENT} from '../../constants'; interface TaggProps { style: object; + social: string; isProfileView: boolean; } -const Tagg: React.FC<TaggProps> = ({style, isProfileView}) => { +const Tagg: React.FC<TaggProps> = ({style, social, isProfileView}) => { const navigation = useNavigation(); return ( <TouchableOpacity onPress={() => navigation.navigate('SocialMediaTaggs', { - socialMediaType: 'Instagram', + socialMediaType: social, isProfileView: isProfileView, }) }> diff --git a/src/components/taggs/TaggPost.tsx b/src/components/taggs/TaggPost.tsx index 73f15268..0d3aee50 100644 --- a/src/components/taggs/TaggPost.tsx +++ b/src/components/taggs/TaggPost.tsx @@ -1,38 +1,59 @@ -import moment from 'moment'; import React from 'react'; -import {Image, StyleSheet, View} from 'react-native'; -import {PostType} from '../../types'; +import {Image, StyleSheet, Text, View} from 'react-native'; +import {SimplePostType} from '../../types'; import {SCREEN_WIDTH} from '../../utils'; +import {DateLabel} from '../common'; import TaggPostFooter from './TaggPostFooter'; interface TaggPostProps { - post: PostType; + post: SimplePostType; } -const TaggPost: React.FC<TaggPostProps> = ({post: {socialHandle, data}}) => { - const parsedDate = moment(data?.timestamp || ''); - const date = parsedDate.isValid() ? parsedDate.format('MMM d') : ''; - - return ( - <> - <View style={styles.image}> - {data && <Image style={styles.image} source={{uri: data.media_url}} />} +const TaggPost: React.FC<TaggPostProps> = ({post}) => { + if (post.media_type === 'photo') { + // Post with image and footer that shows caption + return ( + <View style={styles.photoContainer}> + <View style={styles.image}> + {post && ( + <Image style={styles.image} source={{uri: post.media_url}} /> + )} + </View> + <TaggPostFooter + // we currently don't have a way to retreive num of likes information + likes={undefined} + handle={post.username} + caption={post.caption || ''} + timestamp={post.timestamp} + /> + </View> + ); + } else { + // Post with large text + return ( + <View style={styles.textContianer}> + <Text style={styles.text}>{post.caption}</Text> + <DateLabel timestamp={post.timestamp} type={'default'} /> </View> - <TaggPostFooter - // we currently don't have a way to retreive num of likes information - likes={109} - handle={socialHandle} - caption={data?.caption || ''} - date={date} - /> - </> - ); + ); + } }; const styles = StyleSheet.create({ + photoContainer: { + marginBottom: 50, + }, image: { width: SCREEN_WIDTH, height: SCREEN_WIDTH, backgroundColor: '#eee', + marginBottom: 30, + }, + textContianer: {marginBottom: 50, paddingHorizontal: 10}, + text: { + marginBottom: 30, + fontSize: 18, + color: 'white', + flexWrap: 'wrap', }, }); diff --git a/src/components/taggs/TaggPostFooter.tsx b/src/components/taggs/TaggPostFooter.tsx index 024670a8..8371a847 100644 --- a/src/components/taggs/TaggPostFooter.tsx +++ b/src/components/taggs/TaggPostFooter.tsx @@ -1,30 +1,32 @@ import React from 'react'; import {StyleSheet, View} from 'react-native'; import {Text} from 'react-native-animatable'; +import {DateLabel} from '../common'; interface TaggPostFooterProps { - likes: number; - handle: string; + likes?: number; + handle?: string; caption: string; - date: string; + timestamp: string; } const TaggPostFooter: React.FC<TaggPostFooterProps> = ({ likes, handle, caption, - date, + timestamp, }) => { + const handleText = handle ? handle : ''; return ( <View> <View style={styles.container}> - <Text style={styles.likeText}>{likes} likes</Text> + {likes ? <Text style={styles.likeText}>{likes} likes</Text> : <></>} <View style={styles.captionContainer}> <Text style={styles.handleText}> - {handle} + {handleText} <Text style={styles.captionText}> {caption}</Text> </Text> </View> - <Text style={styles.dateText}>{date}</Text> + <DateLabel timestamp={timestamp} type={'small'} /> </View> </View> ); @@ -33,11 +35,11 @@ const TaggPostFooter: React.FC<TaggPostFooterProps> = ({ const styles = StyleSheet.create({ container: { flexDirection: 'column', - padding: 10, - paddingBottom: '10%', + paddingHorizontal: 10, + marginBottom: 50, }, captionContainer: { - paddingVertical: 10, + paddingBottom: 30, }, likeText: { fontSize: 14, @@ -51,15 +53,10 @@ const styles = StyleSheet.create({ }, captionText: { fontSize: 14, - fontWeight: 'bold', + fontWeight: 'normal', color: 'white', flexWrap: 'wrap', }, - dateText: { - fontSize: 14, - fontWeight: 'bold', - color: '#8FA9C2', - }, }); export default TaggPostFooter; diff --git a/src/components/taggs/TaggsBar.tsx b/src/components/taggs/TaggsBar.tsx index 933f355d..88f670b5 100644 --- a/src/components/taggs/TaggsBar.tsx +++ b/src/components/taggs/TaggsBar.tsx @@ -18,9 +18,40 @@ const TaggsBar: React.FC<TaggsBarProps> = ({ isProfileView, }) => { const taggs: Array<JSX.Element> = []; - for (let i = 0; i < 10; i++) { + + taggs.push( + <Tagg + key={0} + style={styles.tagg} + social={'Instagram'} + isProfileView={isProfileView} + />, + ); + taggs.push( + <Tagg + key={1} + style={styles.tagg} + social={'Facebook'} + isProfileView={isProfileView} + />, + ); + taggs.push( + <Tagg + key={2} + style={styles.tagg} + social={'Twitter'} + isProfileView={isProfileView} + />, + ); + + for (let i = 3; i < 10; i++) { taggs.push( - <Tagg key={i} style={styles.tagg} isProfileView={isProfileView} />, + <Tagg + key={i} + style={styles.tagg} + social={'Instagram'} + isProfileView={isProfileView} + />, ); } const shadowOpacity: Animated.Node<number> = interpolate(y, { diff --git a/src/components/taggs/TaggsFeed.tsx b/src/components/taggs/TaggsFeed.tsx deleted file mode 100644 index 3f27e248..00000000 --- a/src/components/taggs/TaggsFeed.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; -import {InstagramPostType, UserType} from '../../types'; -import {TaggPost} from './'; - -interface TaggsFeedProps { - user: UserType; - socialHandle: string; - posts: Array<InstagramPostType>; -} - -const TaggsFeed: React.FC<TaggsFeedProps> = ({user, socialHandle, posts}) => { - return ( - <> - {posts?.map((post, index) => ( - <TaggPost - key={index} - post={{ - owner: user, - social: 'Instagram', - socialHandle: socialHandle, - data: post, - }} - /> - ))} - </> - ); -}; - -export default TaggsFeed; diff --git a/src/components/taggs/TwitterTaggPost.tsx b/src/components/taggs/TwitterTaggPost.tsx new file mode 100644 index 00000000..2cc23bcf --- /dev/null +++ b/src/components/taggs/TwitterTaggPost.tsx @@ -0,0 +1,213 @@ +import React from 'react'; +import {Image, Linking, StyleSheet, View} from 'react-native'; +import {Text} from 'react-native-animatable'; +import LinearGradient from 'react-native-linear-gradient'; +import {AVATAR_DIM, TAGGS_GRADIENT} from '../../constants'; +import {TwitterPostType} from '../../types'; +import {SCREEN_WIDTH} from '../../utils'; +import {DateLabel} from '../common'; + +interface TwitterTaggPostProps { + ownerHandle: string; + post: TwitterPostType; +} +const TwitterTaggPost: React.FC<TwitterTaggPostProps> = ({ + ownerHandle, + post, +}) => { + return ( + <View style={styles.mainContainer}> + {/* Retweeted? */} + {post.type === 'retweet' ? ( + <Text style={styles.retweetedText}>@{ownerHandle} retweeted</Text> + ) : ( + <React.Fragment /> + )} + {/* Post header (avatar and handle) */} + <View style={styles.header}> + <Image + style={styles.avatar} + source={ + post.profile_pic + ? {uri: post.profile_pic} + : require('../../assets/images/avatar-placeholder.png') + } + /> + <Text style={styles.headerText}>@{post.handle}</Text> + </View> + {/* Tweet/Reply/Retweet Content */} + <View style={styles.contentContainer}> + {/* First part of content is text or empty */} + {post.text ? ( + <Text style={styles.contentText}>{post.text}</Text> + ) : ( + <React.Fragment /> + )} + {/* Second part of content is an image or empty */} + {post.media_url ? ( + <View style={styles.imageContainer}> + <Image style={styles.image} source={{uri: post.media_url}} /> + </View> + ) : ( + <React.Fragment /> + )} + {/* Third part of content is the reply/retweet container or empty */} + {(post.type === 'reply' || post.type === 'retweet') && + post.in_reply_to ? ( + <LinearGradient + colors={[TAGGS_GRADIENT.start, TAGGS_GRADIENT.end]} + useAngle={true} + angle={300} + angleCenter={{x: 0.5, y: 0.5}} + style={[styles.replyGradient]}> + <View style={styles.replyPostContainer}> + <View style={styles.replyHeader}> + <Image + style={styles.replyAvatar} + source={ + post.in_reply_to.profile_pic + ? {uri: post.in_reply_to.profile_pic} + : require('../../assets/images/avatar-placeholder.png') + } + /> + <Text style={styles.replyHandleText}> + @{post.in_reply_to.handle} + </Text> + <DateLabel + timestamp={post.in_reply_to.timestamp} + type={'short'} + decorate={(date) => ` • ${date}`} + /> + </View> + <Text style={styles.replyText} numberOfLines={2}> + {post.in_reply_to.text} + </Text> + <Text + style={styles.replyShowThisThread} + onPress={() => { + if (post.in_reply_to?.permalink) { + Linking.openURL(post.in_reply_to.permalink); + } + }}> + Show this thread + </Text> + </View> + </LinearGradient> + ) : ( + <React.Fragment /> + )} + </View> + {/* Footer */} + <View style={styles.footer}> + <DateLabel timestamp={post.timestamp} type={'default'} /> + <Text + style={styles.viewOnTwitterText} + onPress={() => { + if (post.permalink) { + Linking.openURL(post.permalink); + } + }}> + View on Twitter + </Text> + </View> + </View> + ); +}; + +const styles = StyleSheet.create({ + mainContainer: { + marginHorizontal: 10, + marginBottom: 50, + }, + retweetedText: { + fontSize: 12, + color: 'grey', + marginBottom: 20, + }, + header: { + alignItems: 'center', + flexDirection: 'row', + marginBottom: 30, + }, + avatar: { + width: AVATAR_DIM, + height: AVATAR_DIM, + borderRadius: AVATAR_DIM / 2, + }, + headerText: { + fontSize: 15, + fontWeight: 'bold', + color: 'white', + paddingHorizontal: 12, + }, + contentContainer: {}, + contentText: { + fontSize: 18, + color: 'white', + marginBottom: 30, + }, + // image media + imageContainer: { + marginBottom: 30, + }, + image: { + width: SCREEN_WIDTH - 20, + height: SCREEN_WIDTH - 20, + backgroundColor: '#eee', + borderRadius: 15, + }, + // footer + footer: { + height: 50, + flexDirection: 'column', + justifyContent: 'space-between', + marginBottom: 50, + }, + viewOnTwitterText: { + fontSize: 12, + color: '#6ee7e7', + }, + // reply post + replyPostContainer: { + flex: 1, + marginVertical: 1, + paddingHorizontal: 10, + width: SCREEN_WIDTH - 22, + justifyContent: 'space-between', + paddingTop: 10, + paddingBottom: 20, + borderRadius: 15, + backgroundColor: '#1d0034', + }, + replyGradient: { + height: 150, + borderRadius: 15, + justifyContent: 'center', + alignItems: 'center', + marginBottom: 30, + }, + replyHeader: { + alignItems: 'center', + flexDirection: 'row', + }, + replyAvatar: { + height: AVATAR_DIM / 2, + width: AVATAR_DIM / 2, + borderRadius: AVATAR_DIM / 2 / 2, + }, + replyHandleText: { + fontSize: 15, + color: '#c4c4c4', + paddingLeft: 7, + }, + replyText: { + fontSize: 15, + color: 'white', + }, + replyShowThisThread: { + fontSize: 15, + color: '#698dd3', + }, +}); + +export default TwitterTaggPost; diff --git a/src/components/taggs/index.ts b/src/components/taggs/index.ts index 1cb0c412..5d0a4d48 100644 --- a/src/components/taggs/index.ts +++ b/src/components/taggs/index.ts @@ -1,4 +1,4 @@ export {default as TaggsBar} from './TaggsBar'; export {default as SocialMediaInfo} from './SocialMediaInfo'; -export {default as TaggsFeed} from './TaggsFeed'; export {default as TaggPost} from './TaggPost'; +export {default as TwitterTaggPost} from './TwitterTaggPost'; diff --git a/src/constants/api.ts b/src/constants/api.ts index 93a68d65..3d7bd017 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -11,6 +11,8 @@ export const PROFILE_INFO_ENDPOINT: string = API_URL + 'user-profile-info/'; export const COVER_PHOTO_ENDPOINT: string = API_URL + 'large-profile-pic/'; export const AVATAR_PHOTO_ENDPOINT: string = API_URL + 'small-profile-pic/'; export const GET_IG_POSTS_ENDPOINT: string = API_URL + 'posts-ig/'; +export const GET_FB_POSTS_ENDPOINT: string = API_URL + 'posts-fb/'; +export const GET_TWITTER_POSTS_ENDPOINT: string = API_URL + 'posts-twitter/'; export const SEARCH_ENDPOINT: string = API_URL + 'search/'; export const MOMENTS_ENDPOINT: string = API_URL + 'moments/'; export const VERIFY_INVITATION_CODE_ENDPOUNT: string = API_URL + 'verify-code/'; diff --git a/src/routes/authentication/AuthProvider.tsx b/src/routes/authentication/AuthProvider.tsx index 8dd9fd73..a705f074 100644 --- a/src/routes/authentication/AuthProvider.tsx +++ b/src/routes/authentication/AuthProvider.tsx @@ -1,19 +1,23 @@ -import React, {useEffect} from 'react'; -import {createContext, useState} from 'react'; -import { - UserType, - ProfileType, - InstagramPostType, - ProfilePreviewType, -} from '../../types'; import AsyncStorage from '@react-native-community/async-storage'; +import React, {createContext, useEffect, useState} from 'react'; +import { + GET_FB_POSTS_ENDPOINT, + GET_IG_POSTS_ENDPOINT, + GET_TWITTER_POSTS_ENDPOINT, +} from '../../constants'; import { - loadProfileInfo, loadAvatar, loadCover, - loadInstaPosts, + loadProfileInfo, loadRecentlySearchedUsers, + loadSocialPosts, } from '../../services'; +import { + ProfilePreviewType, + ProfileType, + SocialAccountType, + UserType, +} from '../../types'; interface AuthContextProps { user: UserType; @@ -22,20 +26,30 @@ interface AuthContextProps { logout: () => void; avatar: string | null; cover: string | null; - instaPosts: Array<InstagramPostType>; + socialAccounts: Record<string, SocialAccountType>; recentSearches: Array<ProfilePreviewType>; newMomentsAvailable: boolean; updateMoments: (value: boolean) => void; } + const NO_USER: UserType = { userId: '', username: '', }; + const NO_PROFILE: ProfileType = { biography: '', website: '', name: '', }; + +// Not necessary, but safer, in case SocialAccountType object is undefined +const NO_SOCIAL_ACCOUNTS: Record<string, SocialAccountType> = { + Instagram: {}, + Facebook: {}, + Twitter: {}, +}; + export const AuthContext = createContext<AuthContextProps>({ user: NO_USER, profile: NO_PROFILE, @@ -43,7 +57,7 @@ export const AuthContext = createContext<AuthContextProps>({ logout: () => {}, avatar: null, cover: null, - instaPosts: [], + socialAccounts: NO_SOCIAL_ACCOUNTS, recentSearches: [], newMomentsAvailable: true, updateMoments: () => {}, @@ -57,7 +71,9 @@ const AuthProvider: React.FC = ({children}) => { const [profile, setProfile] = useState<ProfileType>(NO_PROFILE); const [avatar, setAvatar] = useState<string | null>(null); const [cover, setCover] = useState<string | null>(null); - const [instaPosts, setInstaPosts] = useState<Array<InstagramPostType>>([]); + const [socialAccounts, setSocialAccounts] = useState< + Record<string, SocialAccountType> + >(NO_SOCIAL_ACCOUNTS); const [recentSearches, setRecentSearches] = useState< Array<ProfilePreviewType> >([]); @@ -78,14 +94,34 @@ const AuthProvider: React.FC = ({children}) => { loadProfileInfo(token, userId, setProfile); loadAvatar(token, userId, setAvatar); loadCover(token, userId, setCover); - loadInstaPosts(token, userId, setInstaPosts); loadRecentlySearchedUsers(setRecentSearches); + loadSocialPosts( + token, + userId, + 'Instagram', + GET_IG_POSTS_ENDPOINT, + socialAccounts, + ).then((newSocialAccounts) => setSocialAccounts(newSocialAccounts)); + loadSocialPosts( + token, + userId, + 'Facebook', + GET_FB_POSTS_ENDPOINT, + socialAccounts, + ).then((newSocialAccounts) => setSocialAccounts(newSocialAccounts)); + loadSocialPosts( + token, + userId, + 'Twitter', + GET_TWITTER_POSTS_ENDPOINT, + socialAccounts, + ).then((newSocialAccounts) => setSocialAccounts(newSocialAccounts)); } catch (err) { console.log(err); } }; loadData(); - }, [userId]); + }, [socialAccounts, userId]); return ( <AuthContext.Provider @@ -94,8 +130,8 @@ const AuthProvider: React.FC = ({children}) => { profile, avatar, cover, - instaPosts, newMomentsAvailable, + socialAccounts, login: (id, username) => { setUser({...user, userId: id, username}); }, diff --git a/src/routes/profile/Profile.tsx b/src/routes/profile/Profile.tsx index 363e9e21..736127bf 100644 --- a/src/routes/profile/Profile.tsx +++ b/src/routes/profile/Profile.tsx @@ -42,7 +42,7 @@ const Profile: React.FC<ProfileStackProps> = ({route}) => { }), }} mode="modal" - initialRouteName={!isProfileView ? `Profile` : `Search`}> + initialRouteName={!isProfileView ? 'Profile' : 'Search'}> <ProfileStack.Screen name="Profile" component={ProfileScreen} diff --git a/src/routes/viewProfile/ProfileProvider.tsx b/src/routes/viewProfile/ProfileProvider.tsx index 8fb9a011..a4b6cb14 100644 --- a/src/routes/viewProfile/ProfileProvider.tsx +++ b/src/routes/viewProfile/ProfileProvider.tsx @@ -1,14 +1,16 @@ -import React, {useEffect} from 'react'; -import {createContext, useState} from 'react'; import AsyncStorage from '@react-native-community/async-storage'; -import {UserType, ProfileType, InstagramPostType} from '../../types'; - +import React, {createContext, useEffect, useState} from 'react'; +import { + GET_IG_POSTS_ENDPOINT, + GET_TWITTER_POSTS_ENDPOINT, +} from '../../constants'; import { - loadProfileInfo, loadAvatar, loadCover, - loadInstaPosts, + loadProfileInfo, + loadSocialPosts, } from '../../services'; +import {ProfileType, SocialAccountType, UserType} from '../../types'; interface ProfileContextProps { user: UserType; @@ -16,9 +18,9 @@ interface ProfileContextProps { loadProfile: (userId: string, username: string) => void; avatar: string | null; cover: string | null; - instaPosts: Array<InstagramPostType>; newMomentsAvailable: boolean; updateMoments: (value: boolean) => void; + socialAccounts: Record<string, SocialAccountType>; } const NO_USER: UserType = { userId: '', @@ -29,15 +31,23 @@ const NO_PROFILE: ProfileType = { website: '', name: '', }; + +// Not necessary, but safer, in case SocialAccountType object is undefined +const NO_SOCIAL_ACCOUNTS: Record<string, SocialAccountType> = { + Instagram: {}, + Facebook: {}, + Twitter: {}, +}; + export const ProfileContext = createContext<ProfileContextProps>({ user: NO_USER, profile: NO_PROFILE, loadProfile: () => {}, avatar: null, cover: null, - instaPosts: [], newMomentsAvailable: true, updateMoments: () => {}, + socialAccounts: NO_SOCIAL_ACCOUNTS, }); /** @@ -48,9 +58,11 @@ const ProfileProvider: React.FC = ({children}) => { const [profile, setProfile] = useState<ProfileType>(NO_PROFILE); const [avatar, setAvatar] = useState<string | null>(null); const [cover, setCover] = useState<string | null>(null); - const [instaPosts, setInstaPosts] = useState<Array<InstagramPostType>>([]); const [newMomentsAvailable, setNewMomentsAvailable] = useState<boolean>(true); + const [socialAccounts, setSocialAccounts] = useState< + Record<string, SocialAccountType> + >(NO_SOCIAL_ACCOUNTS); const {userId} = user; useEffect(() => { if (!userId) { @@ -67,13 +79,26 @@ const ProfileProvider: React.FC = ({children}) => { loadProfileInfo(token, userId, setProfile); loadAvatar(token, userId, setAvatar); loadCover(token, userId, setCover); - loadInstaPosts(token, userId, setInstaPosts); + loadSocialPosts( + token, + userId, + 'Instagram', + GET_IG_POSTS_ENDPOINT, + socialAccounts, + ).then((newSocialAccounts) => setSocialAccounts(newSocialAccounts)); + loadSocialPosts( + token, + userId, + 'Twitter', + GET_TWITTER_POSTS_ENDPOINT, + socialAccounts, + ).then((newSocialAccounts) => setSocialAccounts(newSocialAccounts)); } catch (err) { console.log(err); } }; loadData(); - }, [userId]); + }, [socialAccounts, userId]); return ( <ProfileContext.Provider @@ -82,8 +107,8 @@ const ProfileProvider: React.FC = ({children}) => { profile, avatar, cover, - instaPosts, newMomentsAvailable, + socialAccounts, loadProfile: (id, username) => { setUser({...user, userId: id, username}); }, diff --git a/src/screens/profile/SocialMediaTaggs.tsx b/src/screens/profile/SocialMediaTaggs.tsx index c82a310e..5a2e638e 100644 --- a/src/screens/profile/SocialMediaTaggs.tsx +++ b/src/screens/profile/SocialMediaTaggs.tsx @@ -3,9 +3,15 @@ import React from 'react'; import {ScrollView, StatusBar, StyleSheet, View} from 'react-native'; import LinearGradient from 'react-native-linear-gradient'; import {AVATAR_GRADIENT} from '../../constants'; -import {SocialMediaInfo, TabsGradient, TaggsFeed} from '../../components'; +import { + SocialMediaInfo, + TabsGradient, + TaggPost, + TwitterTaggPost, +} from '../../components'; import {AuthContext, ProfileStackParams, ProfileContext} from '../../routes'; import {headerBarHeightWithImage, SCREEN_HEIGHT} from '../../utils'; +import {SimplePostType, TwitterPostType} from '../../types'; type SocialMediaTaggsRouteProp = RouteProp< ProfileStackParams, @@ -28,24 +34,17 @@ interface SocialMediaTaggsProps { * + dark background */ const SocialMediaTaggs: React.FC<SocialMediaTaggsProps> = ({route}) => { - const {isProfileView} = route.params; + const {socialMediaType, isProfileView} = route.params; const context = isProfileView ? React.useContext(ProfileContext) : React.useContext(AuthContext); const { - user, profile: {name}, + socialAccounts, } = context; - // TODO: We should use the passed-in socialmedia type/handle instead. - // Currently don't have an intuitive way of doing so, for now, - // just grabbing from user's AuthContext. - // const {socialMediaType, socialMediaHandle} = route.params; - const {instaPosts} = context; - const socialMediaType = 'Instagram'; - const socialMediaHandle = - instaPosts ? instaPosts[0].username : '_'; - + const handle = socialAccounts[socialMediaType].handle; + const posts = socialAccounts[socialMediaType].posts || []; const headerHeight = headerBarHeightWithImage(); return ( @@ -65,13 +64,21 @@ const SocialMediaTaggs: React.FC<SocialMediaTaggsProps> = ({route}) => { <SocialMediaInfo fullname={name} type={socialMediaType} - handle={socialMediaHandle} - /> - <TaggsFeed - user={user} - socialHandle={socialMediaHandle} - posts={instaPosts} + handle={handle} /> + {(posts as Array< + SimplePostType | TwitterPostType + >).map((post, index) => + socialMediaType === 'Twitter' ? ( + <TwitterTaggPost + key={index} + ownerHandle={handle || '_'} + post={post as TwitterPostType} + /> + ) : ( + <TaggPost key={index} post={post as SimplePostType} /> + ), + )} </ScrollView> <TabsGradient /> </View> diff --git a/src/services/UserProfileService.ts b/src/services/UserProfileService.ts index 4c3af06a..59f9649a 100644 --- a/src/services/UserProfileService.ts +++ b/src/services/UserProfileService.ts @@ -6,10 +6,12 @@ import { AVATAR_PHOTO_ENDPOINT, COVER_PHOTO_ENDPOINT, GET_IG_POSTS_ENDPOINT, + GET_TWITTER_POSTS_ENDPOINT, } from '../constants'; import AsyncStorage from '@react-native-community/async-storage'; import RNFetchBlob from 'rn-fetch-blob'; +import {SocialAccountType} from 'src/types'; export const loadProfileInfo = async ( token: string, @@ -83,32 +85,32 @@ export const loadCover = async ( } }; -export const loadInstaPosts = async ( +export const loadSocialPosts = async ( token: string, userId: string, - callback: Function, + socialType: string, + endpoint: string, + socialAccounts: Record<string, SocialAccountType>, ) => { try { - const response = await fetch(GET_IG_POSTS_ENDPOINT + `${userId}/`, { + const response = await fetch(endpoint + `${userId}/`, { method: 'GET', headers: { Authorization: 'Token ' + token, }, }); - const status = response.status; - if (status === 200) { - let ig_posts = await response.json(); - callback(ig_posts); + if (response.status === 200) { + const body = await response.json(); + socialAccounts[socialType].handle = body.handle; + socialAccounts[socialType].posts = body.posts; + socialAccounts[socialType].profile_pic = body.profile_pic; } else { - callback([]); + throw new Error(await response.json()); } } catch (error) { console.log(error); - Alert.alert( - 'Something went wrong! 😭', - "Would you believe me if I told you that I don't know what happened?", - ); } + return socialAccounts; }; export const loadRecentlySearchedUsers = async (callback: Function) => { diff --git a/src/types/types.ts b/src/types/types.ts index 4473878f..aa46fd36 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -19,11 +19,38 @@ export interface ProfileType { website: string; } -export interface InstagramPostType { +export interface SocialAccountType { + handle?: string; + profile_pic?: string; + posts?: Array<SimplePostType> | Array<TwitterPostType>; +} + +interface TwitterReplyType { + type: 'tweet' | 'reply' | 'retweet'; + handle: string; + profile_pic: string; + text: string; + timestamp: string; + permalink: string; +} + +export interface TwitterPostType { + type: 'tweet' | 'reply' | 'retweet'; + handle: string; + profile_pic: string; + text: string; + timestamp: string; + media_url: string; + permalink: string; + in_reply_to?: TwitterReplyType; +} + +export interface SimplePostType { post_id: string; username: string; + profile_pic: string; media_url: string; - media_type: string; + media_type: 'text' | 'photo'; caption: string; timestamp: string; permalink: string; @@ -32,8 +59,8 @@ export interface InstagramPostType { export interface PostType { owner: UserType; social: string; - socialHandle: string; - data: InstagramPostType | undefined; + socialHandle?: string; + data: SimplePostType | undefined; } export interface LinkerType { |