From d0237cbb61e5c4d77c7b0cefc50891639646ee91 Mon Sep 17 00:00:00 2001 From: Ashm Walia <40498934+ashmgarv@users.noreply.github.com> Date: Thu, 22 Oct 2020 15:34:21 -0700 Subject: [TMA 236] Comments PR (#64) * Added comments count and retrieve comments * Working draft * The one before cleanup * Finally * Added time icon and major refactoring * Small fix for social media taggs * Addressed review comments --- src/assets/icons/clock-icon-01.svg | 1 + src/assets/icons/moment-comment-icon.svg | 1 + src/components/comments/AddComment.tsx | 103 ++++++++++++ src/components/comments/CommentTile.tsx | 71 ++++++++ src/components/comments/CommentsCount.tsx | 57 +++++++ src/components/comments/index.ts | 3 + src/components/index.ts | 2 + src/components/moments/CaptionScreenHeader.tsx | 37 +++++ src/components/moments/Moment.tsx | 126 +++++++++++++++ src/components/moments/MomentTile.tsx | 33 ++++ src/components/moments/index.ts | 2 + src/components/profile/CaptionScreenHeader.tsx | 37 ----- src/components/profile/Content.tsx | 5 +- src/components/profile/Moment.tsx | 126 --------------- src/components/profile/MomentTile.tsx | 33 ---- src/components/profile/ProfilePreview.tsx | 216 +++++++++++++++++++++++++ src/components/profile/index.ts | 3 +- src/components/search/SearchResult.tsx | 172 -------------------- src/components/search/SearchResults.tsx | 5 +- src/constants/api.ts | 1 + src/routes/profile/Profile.tsx | 26 +++ src/routes/profile/ProfileStack.tsx | 7 + src/screens/profile/IndividualMoment.tsx | 63 ++++---- src/screens/profile/MomentCommentsScreen.tsx | 133 +++++++++++++++ src/screens/profile/SocialMediaTaggs.tsx | 3 +- src/screens/profile/index.ts | 1 + src/services/MomentServices.ts | 98 +++++++++++ src/services/index.ts | 1 + src/types/types.ts | 8 + src/utils/index.ts | 1 + src/utils/moments.ts | 33 ++++ 31 files changed, 1002 insertions(+), 406 deletions(-) create mode 100644 src/assets/icons/clock-icon-01.svg create mode 100644 src/assets/icons/moment-comment-icon.svg create mode 100644 src/components/comments/AddComment.tsx create mode 100644 src/components/comments/CommentTile.tsx create mode 100644 src/components/comments/CommentsCount.tsx create mode 100644 src/components/comments/index.ts create mode 100644 src/components/moments/CaptionScreenHeader.tsx create mode 100644 src/components/moments/Moment.tsx create mode 100644 src/components/moments/MomentTile.tsx create mode 100644 src/components/moments/index.ts delete mode 100644 src/components/profile/CaptionScreenHeader.tsx delete mode 100644 src/components/profile/Moment.tsx delete mode 100644 src/components/profile/MomentTile.tsx create mode 100644 src/components/profile/ProfilePreview.tsx delete mode 100644 src/components/search/SearchResult.tsx create mode 100644 src/screens/profile/MomentCommentsScreen.tsx create mode 100644 src/services/MomentServices.ts create mode 100644 src/utils/moments.ts (limited to 'src') diff --git a/src/assets/icons/clock-icon-01.svg b/src/assets/icons/clock-icon-01.svg new file mode 100644 index 00000000..8e90a983 --- /dev/null +++ b/src/assets/icons/clock-icon-01.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/moment-comment-icon.svg b/src/assets/icons/moment-comment-icon.svg new file mode 100644 index 00000000..6b105b72 --- /dev/null +++ b/src/assets/icons/moment-comment-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/comments/AddComment.tsx b/src/components/comments/AddComment.tsx new file mode 100644 index 00000000..65c0b066 --- /dev/null +++ b/src/components/comments/AddComment.tsx @@ -0,0 +1,103 @@ +import * as React from 'react'; +import {Image, StyleSheet, TextInput, View} from 'react-native'; +import AsyncStorage from '@react-native-community/async-storage'; +import {AuthContext} from '../../routes'; +import {TaggBigInput} from '../onboarding'; +import {postMomentComment} from '../../services'; + +/** + * This file provides the add comment view for a user. + * Displays the logged in user's profile picture to the left and then provides space to add a comment. + * Comment is posted when enter is pressed as requested by product team. + */ + +export interface AddCommentProps { + setNewCommentsAvailable: Function; + moment_id: string; +} + +const AddComment: React.FC = ({ + setNewCommentsAvailable, + moment_id, +}) => { + const [comment, setComment] = React.useState(''); + const { + avatar, + user: {userId, username}, + logout, + } = React.useContext(AuthContext); + + const handleCommentUpdate = (comment: string) => { + setComment(comment); + }; + + const postComment = async () => { + try { + const token = await AsyncStorage.getItem('token'); + if (!token) { + logout(); + return; + } + const postedComment = await postMomentComment( + userId, + comment, + moment_id, + token, + ); + + if (postedComment) { + //Set the current comment to en empty string if the comment was posted successfully. + handleCommentUpdate(''); + + //Indicate the MomentCommentsScreen that it needs to download the new comments again + setNewCommentsAvailable(true); + } + } catch (err) { + console.log('Error while posting comment!'); + } + }; + + return ( + + + + + ); +}; +const styles = StyleSheet.create({ + container: {flexDirection: 'row'}, + text: { + position: 'relative', + right: '18%', + backgroundColor: 'white', + width: '70%', + paddingLeft: '2%', + paddingRight: '2%', + paddingBottom: '1%', + paddingTop: '1%', + height: 60, + }, + avatar: { + height: 40, + width: 40, + borderRadius: 30, + marginRight: 15, + }, +}); + +export default AddComment; diff --git a/src/components/comments/CommentTile.tsx b/src/components/comments/CommentTile.tsx new file mode 100644 index 00000000..02840d47 --- /dev/null +++ b/src/components/comments/CommentTile.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import {Text, View} from 'react-native-animatable'; +import {ProfilePreview} from '../profile'; +import {CommentType} from '../../types'; +import {StyleSheet} from 'react-native'; +import {getTimePosted} from '../../utils'; +import ClockIcon from '../../assets/icons/clock-icon-01.svg'; + +/** + * Displays users's profile picture, comment posted by them and the time difference between now and when a comment was posted. + */ + +interface CommentTileProps { + comment_object: CommentType; +} + +const CommentTile: React.FC = ({comment_object}) => { + const timePosted = getTimePosted(comment_object.date_time); + return ( + + + + {comment_object.comment} + + + {' ' + timePosted} + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + marginLeft: '3%', + marginRight: '3%', + borderBottomWidth: 1, + borderColor: 'lightgray', + marginBottom: '3%', + }, + body: { + marginLeft: 56, + }, + comment: { + position: 'relative', + top: -5, + marginBottom: '2%', + }, + date_time: { + color: 'gray', + }, + clockIcon: { + width: 12, + height: 12, + alignSelf: 'center', + }, + clockIconAndTime: { + flexDirection: 'row', + marginBottom: '3%', + }, +}); + +export default CommentTile; diff --git a/src/components/comments/CommentsCount.tsx b/src/components/comments/CommentsCount.tsx new file mode 100644 index 00000000..74b4194c --- /dev/null +++ b/src/components/comments/CommentsCount.tsx @@ -0,0 +1,57 @@ +import * as React from 'react'; +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'; + +/** + * Provides a view for the comment icon and the comment count. + * When the user clicks on this view, a new screen opens to display all the comments. + */ + +type CommentsCountProps = { + comments_count: string; + isProfileView: boolean; + moment_id: string; +}; + +const CommentsCount: React.FC = ({ + comments_count, + isProfileView, + moment_id, +}) => { + const navigation = useNavigation(); + const navigateToCommentsScreen = async () => { + navigation.navigate('MomentCommentsScreen', { + isProfileView: isProfileView, + moment_id: moment_id, + }); + }; + return ( + <> + navigateToCommentsScreen()}> + + + {comments_count !== '0' ? comments_count : ''} + + + + ); +}; + +const styles = StyleSheet.create({ + image: { + position: 'relative', + width: 21, + height: 21, + }, + + count: { + position: 'relative', + fontWeight: 'bold', + color: 'white', + paddingTop: '2%', + }, +}); + +export default CommentsCount; diff --git a/src/components/comments/index.ts b/src/components/comments/index.ts new file mode 100644 index 00000000..6293f799 --- /dev/null +++ b/src/components/comments/index.ts @@ -0,0 +1,3 @@ +export {default as CommentsCount} from '../comments/CommentsCount'; +export {default as CommentTile} from './CommentTile'; +export {default as AddComment} from './AddComment'; diff --git a/src/components/index.ts b/src/components/index.ts index 1b726051..46a7773f 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -3,3 +3,5 @@ export * from './onboarding'; export * from './profile'; export * from './search'; export * from './taggs'; +export * from './comments'; +export * from './moments'; diff --git a/src/components/moments/CaptionScreenHeader.tsx b/src/components/moments/CaptionScreenHeader.tsx new file mode 100644 index 00000000..4715b4ef --- /dev/null +++ b/src/components/moments/CaptionScreenHeader.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import {Text, View, StyleSheet, ViewProps} from 'react-native'; +interface CaptionScreenHeaderProps extends ViewProps { + title: string; +} +const CaptionScreenHeader: React.FC = ({ + title, + style, +}) => { + return ( + + + {title} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + justifyContent: 'center', + height: 30, + }, + headerContainer: { + position: 'absolute', + left: '50%', + }, + header: { + position: 'relative', + right: '50%', + fontSize: 20, + fontWeight: 'bold', + color: 'white', + }, +}); +export default CaptionScreenHeader; diff --git a/src/components/moments/Moment.tsx b/src/components/moments/Moment.tsx new file mode 100644 index 00000000..1ec5511e --- /dev/null +++ b/src/components/moments/Moment.tsx @@ -0,0 +1,126 @@ +import {useNavigation} from '@react-navigation/native'; +import React from 'react'; +import {Alert, StyleSheet, View} from 'react-native'; +import {Text} from 'react-native-animatable'; +import {ScrollView, TouchableOpacity} from 'react-native-gesture-handler'; +import LinearGradient from 'react-native-linear-gradient'; +import PlusIcon from '../../assets/icons/plus_icon-01.svg'; +import BigPlusIcon from '../../assets/icons/plus_icon-02.svg'; +import {MOMENTS_TITLE_COLOR} from '../../constants'; +import {SCREEN_WIDTH} from '../../utils'; +import ImagePicker from 'react-native-image-crop-picker'; +import MomentTile from './MomentTile'; +import {MomentType} from 'src/types'; + +interface MomentProps { + title: string; + images: MomentType[] | undefined; + isProfileView: boolean; +} + +const Moment: React.FC = ({title, images, isProfileView}) => { + const navigation = useNavigation(); + + const navigateToImagePicker = () => { + ImagePicker.openPicker({ + width: 580, + height: 580, + cropping: true, + cropperToolbarTitle: 'Upload a moment', + mediaType: 'photo', + }) + .then((picture) => { + if ('path' in picture) { + navigation.navigate('CaptionScreen', { + title: title, + image: picture, + }); + } + }) + .catch((err) => { + Alert.alert('Unable to upload moment!'); + }); + }; + return ( + + + {title} + {!isProfileView ? ( + navigateToImagePicker()} + /> + ) : ( + + )} + + + {images && + images.map((imageObj: MomentType) => ( + + ))} + {(images === undefined || images.length === 0) && !isProfileView && ( + navigateToImagePicker()}> + + + + + Add a moment of your {title.toLowerCase()}! + + + + + )} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + flexDirection: 'column', + backgroundColor: '#eee', + }, + header: { + flex: 1, + paddingHorizontal: 10, + padding: 5, + paddingTop: 20, + backgroundColor: 'white', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + titleText: { + fontSize: 16, + fontWeight: 'bold', + color: MOMENTS_TITLE_COLOR, + }, + scrollContainer: { + height: SCREEN_WIDTH / 2, + backgroundColor: '#eee', + }, + defaultImage: { + aspectRatio: 1, + height: '100%', + alignItems: 'center', + justifyContent: 'center', + flexDirection: 'column', + }, + defaultImageText: { + fontSize: 20, + paddingTop: 20, + color: 'white', + fontWeight: 'bold', + width: '75%', + textAlign: 'center', + }, +}); + +export default Moment; diff --git a/src/components/moments/MomentTile.tsx b/src/components/moments/MomentTile.tsx new file mode 100644 index 00000000..70b20d40 --- /dev/null +++ b/src/components/moments/MomentTile.tsx @@ -0,0 +1,33 @@ +import {useNavigation} from '@react-navigation/native'; +import React from 'react'; +import {StyleSheet, View, Image, TouchableOpacity} from 'react-native'; +import {MomentType} from 'src/types'; + +interface MomentTileProps { + moment: MomentType; +} +const MomentTile: React.FC = ({moment}) => { + const navigation = useNavigation(); + const {path_hash} = moment; + return ( + { + navigation.navigate('IndividualMoment', {moment}); + }}> + + + + + ); +}; + +const styles = StyleSheet.create({ + image: { + aspectRatio: 1, + height: '100%', + alignItems: 'center', + justifyContent: 'center', + flexDirection: 'column', + }, +}); +export default MomentTile; diff --git a/src/components/moments/index.ts b/src/components/moments/index.ts new file mode 100644 index 00000000..339e0e19 --- /dev/null +++ b/src/components/moments/index.ts @@ -0,0 +1,2 @@ +export {default as CaptionScreenHeader} from '../moments/CaptionScreenHeader'; +export {default as Moment} from './Moment'; diff --git a/src/components/profile/CaptionScreenHeader.tsx b/src/components/profile/CaptionScreenHeader.tsx deleted file mode 100644 index 4715b4ef..00000000 --- a/src/components/profile/CaptionScreenHeader.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import {Text, View, StyleSheet, ViewProps} from 'react-native'; -interface CaptionScreenHeaderProps extends ViewProps { - title: string; -} -const CaptionScreenHeader: React.FC = ({ - title, - style, -}) => { - return ( - - - {title} - - - ); -}; - -const styles = StyleSheet.create({ - container: { - flexDirection: 'row', - justifyContent: 'center', - height: 30, - }, - headerContainer: { - position: 'absolute', - left: '50%', - }, - header: { - position: 'relative', - right: '50%', - fontSize: 20, - fontWeight: 'bold', - color: 'white', - }, -}); -export default CaptionScreenHeader; diff --git a/src/components/profile/Content.tsx b/src/components/profile/Content.tsx index 0bf66dc7..8f20cd8d 100644 --- a/src/components/profile/Content.tsx +++ b/src/components/profile/Content.tsx @@ -7,7 +7,7 @@ import {MomentType} from 'src/types'; import {defaultMoments, MOMENTS_ENDPOINT} from '../../constants'; import {SCREEN_HEIGHT} from '../../utils'; import TaggsBar from '../taggs/TaggsBar'; -import Moment from './Moment'; +import {Moment} from '../moments'; import ProfileBody from './ProfileBody'; import ProfileCutout from './ProfileCutout'; import ProfileHeader from './ProfileHeader'; @@ -45,7 +45,6 @@ const Content: React.FC = ({y, isProfileView}) => { }); setImagesMap(map); - console.log(map); }, [imagesList]); useEffect(() => { @@ -56,7 +55,7 @@ const Content: React.FC = ({y, isProfileView}) => { const retrieveMoments = async () => { try { const token = await AsyncStorage.getItem('token'); - const response = await fetch(MOMENTS_ENDPOINT + `${userId}/`, { + const response = await fetch(MOMENTS_ENDPOINT + '?user_id=' + userId, { method: 'GET', headers: { Authorization: 'Token ' + token, diff --git a/src/components/profile/Moment.tsx b/src/components/profile/Moment.tsx deleted file mode 100644 index 1ec5511e..00000000 --- a/src/components/profile/Moment.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import {useNavigation} from '@react-navigation/native'; -import React from 'react'; -import {Alert, StyleSheet, View} from 'react-native'; -import {Text} from 'react-native-animatable'; -import {ScrollView, TouchableOpacity} from 'react-native-gesture-handler'; -import LinearGradient from 'react-native-linear-gradient'; -import PlusIcon from '../../assets/icons/plus_icon-01.svg'; -import BigPlusIcon from '../../assets/icons/plus_icon-02.svg'; -import {MOMENTS_TITLE_COLOR} from '../../constants'; -import {SCREEN_WIDTH} from '../../utils'; -import ImagePicker from 'react-native-image-crop-picker'; -import MomentTile from './MomentTile'; -import {MomentType} from 'src/types'; - -interface MomentProps { - title: string; - images: MomentType[] | undefined; - isProfileView: boolean; -} - -const Moment: React.FC = ({title, images, isProfileView}) => { - const navigation = useNavigation(); - - const navigateToImagePicker = () => { - ImagePicker.openPicker({ - width: 580, - height: 580, - cropping: true, - cropperToolbarTitle: 'Upload a moment', - mediaType: 'photo', - }) - .then((picture) => { - if ('path' in picture) { - navigation.navigate('CaptionScreen', { - title: title, - image: picture, - }); - } - }) - .catch((err) => { - Alert.alert('Unable to upload moment!'); - }); - }; - return ( - - - {title} - {!isProfileView ? ( - navigateToImagePicker()} - /> - ) : ( - - )} - - - {images && - images.map((imageObj: MomentType) => ( - - ))} - {(images === undefined || images.length === 0) && !isProfileView && ( - navigateToImagePicker()}> - - - - - Add a moment of your {title.toLowerCase()}! - - - - - )} - - - ); -}; - -const styles = StyleSheet.create({ - container: { - flex: 1, - flexDirection: 'column', - backgroundColor: '#eee', - }, - header: { - flex: 1, - paddingHorizontal: 10, - padding: 5, - paddingTop: 20, - backgroundColor: 'white', - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - }, - titleText: { - fontSize: 16, - fontWeight: 'bold', - color: MOMENTS_TITLE_COLOR, - }, - scrollContainer: { - height: SCREEN_WIDTH / 2, - backgroundColor: '#eee', - }, - defaultImage: { - aspectRatio: 1, - height: '100%', - alignItems: 'center', - justifyContent: 'center', - flexDirection: 'column', - }, - defaultImageText: { - fontSize: 20, - paddingTop: 20, - color: 'white', - fontWeight: 'bold', - width: '75%', - textAlign: 'center', - }, -}); - -export default Moment; diff --git a/src/components/profile/MomentTile.tsx b/src/components/profile/MomentTile.tsx deleted file mode 100644 index 70b20d40..00000000 --- a/src/components/profile/MomentTile.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import {useNavigation} from '@react-navigation/native'; -import React from 'react'; -import {StyleSheet, View, Image, TouchableOpacity} from 'react-native'; -import {MomentType} from 'src/types'; - -interface MomentTileProps { - moment: MomentType; -} -const MomentTile: React.FC = ({moment}) => { - const navigation = useNavigation(); - const {path_hash} = moment; - return ( - { - navigation.navigate('IndividualMoment', {moment}); - }}> - - - - - ); -}; - -const styles = StyleSheet.create({ - image: { - aspectRatio: 1, - height: '100%', - alignItems: 'center', - justifyContent: 'center', - flexDirection: 'column', - }, -}); -export default MomentTile; diff --git a/src/components/profile/ProfilePreview.tsx b/src/components/profile/ProfilePreview.tsx new file mode 100644 index 00000000..c527746a --- /dev/null +++ b/src/components/profile/ProfilePreview.tsx @@ -0,0 +1,216 @@ +import React, {useEffect, useState, useContext} from 'react'; +import {ProfilePreviewType} from '../../types'; +import { + View, + Text, + Image, + StyleSheet, + ViewProps, + TouchableOpacity, +} from 'react-native'; +import {useNavigation} from '@react-navigation/native'; +import RNFetchBlob from 'rn-fetch-blob'; +import AsyncStorage from '@react-native-community/async-storage'; +import {AVATAR_PHOTO_ENDPOINT} from '../../constants'; +import {UserType} from '../../types'; +import {ProfileContext} from '../../routes/viewProfile'; +const NO_USER: UserType = { + userId: '', + username: '', +}; + +/** + * This component returns user's profile picture followed by username as a touchable component. + * What happens when someone clicks on this component is partly decided by the prop isComment. + * 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 load the ProfileContext with data and set the getNewMoments flag to true (Which ensures that everything that needs to be displayed on a user's profile is set). + * Finally, We navigate to Profile if we are on the Search Stack. Else we navigate to ProfileView. + */ + +interface ProfilePreviewProps extends ViewProps { + profilePreview: ProfilePreviewType; + isComment: boolean; +} +const ProfilePreview: React.FC = ({ + profilePreview: {username, first_name, last_name, id}, + isComment, + style, +}) => { + const navigation = useNavigation(); + const {loadProfile, updateMoments} = useContext(ProfileContext); + const [avatarURI, setAvatarURI] = useState(null); + const [user, setUser] = useState(NO_USER); + useEffect(() => { + let mounted = true; + const loadAvatar = async () => { + try { + const token = await AsyncStorage.getItem('token'); + if (!token) { + setUser(NO_USER); + return; + } + const response = await RNFetchBlob.config({ + fileCache: true, + appendExt: 'jpg', + }).fetch('GET', AVATAR_PHOTO_ENDPOINT + `${id}/`, { + Authorization: 'Token ' + token, + }); + const status = response.info().status; + if (status === 200) { + if (mounted) { + setAvatarURI(response.path()); + } + return; + } + if (mounted) { + setAvatarURI(''); + } + } catch (error) { + console.log(error); + } + }; + loadAvatar(); + return () => { + mounted = false; + }; + }, [id]); + + /** + * Adds a searched user to the recently searched cache if they're tapped on. + * Cache maintains 10 recently searched users, popping off the oldest one if + * needed to make space. + */ + const addToRecentlyStoredAndNavigateToProfile = async () => { + let user: ProfilePreviewType = { + id, + username, + first_name, + last_name, + }; + try { + if (!isComment) { + const jsonValue = await AsyncStorage.getItem( + '@recently_searched_users', + ); + let recentlySearchedList = + jsonValue != null ? JSON.parse(jsonValue) : null; + if (recentlySearchedList) { + if (recentlySearchedList.length > 0) { + if ( + recentlySearchedList.some( + (saved_user: ProfilePreviewType) => saved_user.id === id, + ) + ) { + console.log('User already in recently searched.'); + } else { + if (recentlySearchedList.length >= 10) { + recentlySearchedList.pop(); + } + recentlySearchedList.unshift(user); + } + } + } else { + recentlySearchedList = [user]; + } + + try { + let recentlySearchedListString = JSON.stringify(recentlySearchedList); + await AsyncStorage.setItem( + '@recently_searched_users', + recentlySearchedListString, + ); + } catch (e) { + console.log(e); + } + } + + //Load user profile and set new moments to true, navigate to Profile + //Load user profile makes sure that we actually load profile of the user the logged in user want to view + //Set new moments to true makes sure that we download the moment for the user being viewed again. + loadProfile(user.id, user.username); + updateMoments(true); + if (!isComment) { + navigation.navigate('Profile', { + isProfileView: true, + }); + } else { + navigation.navigate('ProfileView', { + isProfileView: true, + }); + } + } catch (e) { + console.log(e); + } + }; + + //With @ sign if on search screen. + const usernameToDisplay = !isComment ? `@` + username : username; + const usernameStyle = isComment + ? styles.commentUsername + : styles.searchUsername; + + const avatarStyle = !isComment ? styles.searchAvatar : styles.commentAvatar; + + return ( + + + + {usernameToDisplay} + {first_name ? ( + {first_name.concat(' ', last_name)} + ) : ( + React.Fragment + )} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'center', + }, + searchAvatar: { + height: 60, + width: 60, + borderRadius: 30, + marginRight: 15, + }, + commentAvatar: { + height: 40, + width: 40, + borderRadius: 20, + marginRight: 15, + marginTop: '2%', + }, + nameContainer: { + justifyContent: 'space-evenly', + alignSelf: 'stretch', + }, + searchUsername: { + fontSize: 18, + fontWeight: '500', + }, + commentUsername: { + fontSize: 16, + fontWeight: '500', + }, + name: { + fontSize: 16, + color: '#333', + }, +}); + +export default ProfilePreview; diff --git a/src/components/profile/index.ts b/src/components/profile/index.ts index e2063e26..eb65d509 100644 --- a/src/components/profile/index.ts +++ b/src/components/profile/index.ts @@ -1,7 +1,6 @@ export {default as Cover} from './Cover'; export {default as Content} from './Content'; -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 CaptionScreenHeader} from './CaptionScreenHeader'; +export {default as ProfilePreview} from '../profile/ProfilePreview'; diff --git a/src/components/search/SearchResult.tsx b/src/components/search/SearchResult.tsx deleted file mode 100644 index 04624004..00000000 --- a/src/components/search/SearchResult.tsx +++ /dev/null @@ -1,172 +0,0 @@ -import React, {useEffect, useState, useContext} from 'react'; -import {ProfilePreviewType} from '../../types'; -import { - View, - Text, - Image, - StyleSheet, - ViewProps, - TouchableOpacity, -} from 'react-native'; -import {useNavigation} from '@react-navigation/native'; -import RNFetchBlob from 'rn-fetch-blob'; -import AsyncStorage from '@react-native-community/async-storage'; -import {AVATAR_PHOTO_ENDPOINT} from '../../constants'; -import {UserType} from '../../types'; -import {ProfileContext} from '../../routes/viewProfile'; -const NO_USER: UserType = { - userId: '', - username: '', -}; - -interface SearchResultProps extends ViewProps { - profilePreview: ProfilePreviewType; -} -const SearchResult: React.FC = ({ - profilePreview: {username, first_name, last_name, id}, - style, -}) => { - const navigation = useNavigation(); - const {loadProfile, updateMoments} = useContext(ProfileContext); - const [avatarURI, setAvatarURI] = useState(null); - const [user, setUser] = useState(NO_USER); - useEffect(() => { - let mounted = true; - const loadAvatar = async () => { - try { - const token = await AsyncStorage.getItem('token'); - if (!token) { - setUser(NO_USER); - return; - } - const response = await RNFetchBlob.config({ - fileCache: true, - appendExt: 'jpg', - }).fetch('GET', AVATAR_PHOTO_ENDPOINT + `${id}/`, { - Authorization: 'Token ' + token, - }); - const status = response.info().status; - if (status === 200) { - if (mounted) { - setAvatarURI(response.path()); - } - return; - } - if (mounted) { - setAvatarURI(''); - } - } catch (error) { - console.log(error); - } - }; - loadAvatar(); - return () => { - mounted = false; - }; - }, [id]); - - /** - * Adds a searched user to the recently searched cache if they're tapped on. - * Cache maintains 10 recently searched users, popping off the oldest one if - * needed to make space. - */ - const addToRecentlyStoredAndNavigateToProfile = async () => { - let user: ProfilePreviewType = { - id, - username, - first_name, - last_name, - }; - try { - const jsonValue = await AsyncStorage.getItem('@recently_searched_users'); - let recentlySearchedList = - jsonValue != null ? JSON.parse(jsonValue) : null; - if (recentlySearchedList) { - if (recentlySearchedList.length > 0) { - if ( - recentlySearchedList.some( - (saved_user: ProfilePreviewType) => saved_user.id === id, - ) - ) { - console.log('User already in recently searched.'); - } else { - if (recentlySearchedList.length >= 10) { - recentlySearchedList.pop(); - } - recentlySearchedList.unshift(user); - } - } - } else { - recentlySearchedList = [user]; - } - - //Load user profile and set new moments to true, navigate to Profile - //Load user profile makes sure that we actually load profile of the user the logged in user want to view - //Set new moments to true makes sure that we download the moment for the user being viewed again. - //Not sure if we should make this call before caching the search results ?? - loadProfile(user.id, user.username); - updateMoments(true); - navigation.navigate('Profile', { - isProfileView: true, - }); - - try { - let recentlySearchedListString = JSON.stringify(recentlySearchedList); - await AsyncStorage.setItem( - '@recently_searched_users', - recentlySearchedListString, - ); - } catch (e) { - console.log(e); - } - } catch (e) { - console.log(e); - } - }; - - return ( - - - - @{username} - {first_name.concat(' ', last_name)} - - - ); -}; - -const styles = StyleSheet.create({ - container: { - flexDirection: 'row', - alignItems: 'center', - }, - avatar: { - height: 60, - width: 60, - borderRadius: 30, - marginRight: 15, - }, - nameContainer: { - justifyContent: 'space-evenly', - alignSelf: 'stretch', - }, - username: { - fontSize: 18, - fontWeight: '500', - }, - name: { - fontSize: 16, - color: '#333', - }, -}); - -export default SearchResult; diff --git a/src/components/search/SearchResults.tsx b/src/components/search/SearchResults.tsx index 16bff818..57db4167 100644 --- a/src/components/search/SearchResults.tsx +++ b/src/components/search/SearchResults.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {ProfilePreviewType} from '../../types'; -import SearchResult from './SearchResult'; +import ProfilePreview from '../profile/ProfilePreview'; import {StyleSheet, View} from 'react-native'; interface SearchResultsProps { results: Array; @@ -9,10 +9,11 @@ const SearchResults: React.FC = ({results}) => { return ( {results.map((profilePreview) => ( - ))} diff --git a/src/constants/api.ts b/src/constants/api.ts index 3d7bd017..181247dd 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -16,6 +16,7 @@ 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/'; +export const COMMENTS_ENDPOINT: string = API_URL + 'comments/'; // Social Link export const LINK_IG_ENDPOINT: string = API_URL + 'link-ig/'; diff --git a/src/routes/profile/Profile.tsx b/src/routes/profile/Profile.tsx index 736127bf..8ab8ecde 100644 --- a/src/routes/profile/Profile.tsx +++ b/src/routes/profile/Profile.tsx @@ -5,11 +5,25 @@ import { SocialMediaTaggs, SearchScreen, ProfileScreen, + MomentCommentsScreen, } from '../../screens'; import {ProfileStack, ProfileStackParams} from './ProfileStack'; import {RouteProp} from '@react-navigation/native'; import {AvatarTitle} from '../../components'; +/** + * 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. + * ProfileView : To display profile of a commenter / any user who has commented on a photo. + * 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). + * 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. + */ + type ProfileStackRouteProps = RouteProp; interface ProfileStackProps { @@ -76,6 +90,18 @@ const Profile: React.FC = ({route}) => { options={{headerShown: false}} initialParams={{isProfileView: isProfileView}} /> + + ); }; diff --git a/src/routes/profile/ProfileStack.tsx b/src/routes/profile/ProfileStack.tsx index 1d7b907e..6d875e81 100644 --- a/src/routes/profile/ProfileStack.tsx +++ b/src/routes/profile/ProfileStack.tsx @@ -19,6 +19,13 @@ export type ProfileStackParams = { moment: MomentType; isProfileView: boolean; }; + MomentCommentsScreen: { + isProfileView: boolean; + moment_id: string; + }; + ProfileView: { + isProfileView: boolean; + }; }; export const ProfileStack = createStackNavigator(); diff --git a/src/screens/profile/IndividualMoment.tsx b/src/screens/profile/IndividualMoment.tsx index 639c0965..91f76f9b 100644 --- a/src/screens/profile/IndividualMoment.tsx +++ b/src/screens/profile/IndividualMoment.tsx @@ -1,15 +1,22 @@ import React, {useEffect, useState} from 'react'; import {StyleSheet, View, Image} from 'react-native'; import {Button} from 'react-native-elements'; -import {SCREEN_HEIGHT, SCREEN_WIDTH, StatusBarHeight} from '../../utils'; -import {UserType} from '../../types'; +import { + SCREEN_HEIGHT, + SCREEN_WIDTH, + StatusBarHeight, + getTimePosted, +} from '../../utils'; +import {UserType, CommentType} from '../../types'; import {RouteProp} from '@react-navigation/native'; import {StackNavigationProp} from '@react-navigation/stack'; -import {CaptionScreenHeader} from '../../components/profile'; +import {CaptionScreenHeader} from '../../components'; import {AuthContext} from '../../routes/authentication'; import {ProfileStackParams} from 'src/routes/profile/ProfileStack'; -import moment from 'moment'; import Animated from 'react-native-reanimated'; +import {CommentsCount} from '../../components'; +import AsyncStorage from '@react-native-community/async-storage'; +import {getMomentCommentsCount} from '../../services'; const NO_USER: UserType = { userId: '', @@ -45,10 +52,13 @@ const IndividualMoment: React.FC = ({ const {isProfileView} = route.params; const { user: {userId}, + logout, } = React.useContext(AuthContext); const [user, setUser] = useState(NO_USER); const [caption, setCaption] = React.useState(route.params.moment.caption); const [elapsedTime, setElapsedTime] = React.useState(); + const [comments_count, setCommentsCount] = React.useState(''); + const handleCaptionUpdate = (caption: string) => { setCaption(caption); }; @@ -58,35 +68,20 @@ const IndividualMoment: React.FC = ({ setUser(NO_USER); } const timePeriod = async () => { - const datePosted = moment(date_time); - const now = moment(); - var time = date_time; - var difference = now.diff(datePosted, 'seconds'); + setElapsedTime(getTimePosted(date_time)); + }; - //Creating elapsedTime string to display to user - // 0 to less than 1 minute - if (difference < 60) { - time = difference + 'seconds'; - } - // 1 minute to less than 1 hour - else if (difference >= 60 && difference < 60 * 60) { - difference = now.diff(datePosted, 'minutes'); - time = difference + (difference === 1 ? 'minute' : 'minutes'); + const loadComments = async () => { + const token = await AsyncStorage.getItem('token'); + if (!token) { + logout(); + return; } - //1 hour to less than 1 day - else if (difference >= 60 * 60 && difference < 24 * 60 * 60) { - difference = now.diff(datePosted, 'hours'); - time = difference + (difference === 1 ? 'hour' : 'hours'); - } - //1 day to less than 7 days - else if (difference >= 24 * 60 * 60 && difference < 7 * 24 * 60 * 60) { - difference = now.diff(datePosted, 'days'); - time = difference + (difference === 1 ? 'day' : 'days'); - } - - setElapsedTime(time); + getMomentCommentsCount(moment_id, setCommentsCount, token); }; + timePeriod(); + loadComments(); }, [date_time, userId]); return ( @@ -109,10 +104,16 @@ const IndividualMoment: React.FC = ({ source={{uri: path_hash}} resizeMode={'cover'} /> + - {caption} + {elapsedTime} + {caption} ); }; @@ -155,6 +156,8 @@ const styles = StyleSheet.create({ position: 'relative', paddingBottom: '1%', paddingTop: '1%', + marginLeft: '5%', + marginRight: '5%', color: '#ffffff', fontWeight: 'bold', }, diff --git a/src/screens/profile/MomentCommentsScreen.tsx b/src/screens/profile/MomentCommentsScreen.tsx new file mode 100644 index 00000000..30dde8b4 --- /dev/null +++ b/src/screens/profile/MomentCommentsScreen.tsx @@ -0,0 +1,133 @@ +import * as React from 'react'; +import {RouteProp, useNavigation} from '@react-navigation/native'; +import {ProfileStackParams} from '../../routes/profile'; +import {CenteredView, CommentTile, OverlayView} from '../../components'; +import {CommentType} from '../../types'; +import {ScrollView, StyleSheet, Text, View} from 'react-native'; +import {SCREEN_WIDTH} from '../../utils/screenDimensions'; +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'; + +/** + * Comments Screen for an image uploaded + * Displays all comments for a particular moment uploaded by the user followed by a text area to add the comment. + * Comment is posted when return is pressed on the keypad. + */ + +type MomentCommentsScreenRouteProps = RouteProp< + ProfileStackParams, + 'MomentCommentsScreen' +>; + +interface MomentCommentsScreenProps { + route: MomentCommentsScreenRouteProps; +} + +const MomentCommentsScreen: React.FC = ({route}) => { + const navigation = useNavigation(); + const {isProfileView, moment_id} = route.params; + const [commentsList, setCommentsList] = React.useState([]); + const [newCommentsAvailable, setNewCommentsAvailable] = React.useState(true); + const {logout} = React.useContext(AuthContext); + + useEffect(() => { + const loadComments = async () => { + const token = await AsyncStorage.getItem('token'); + if (!token) { + logout(); + return; + } + getMomentComments(moment_id, setCommentsList, token); + setNewCommentsAvailable(false); + }; + if (newCommentsAvailable) { + loadComments(); + } + }, [newCommentsAvailable]); + + return ( + + + +