diff options
27 files changed, 672 insertions, 76 deletions
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 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><defs><style>.cls-1{fill:#afb0ae;}</style></defs><path class="cls-1" d="M20,.3A19.7,19.7,0,1,0,39.7,20,19.7,19.7,0,0,0,20,.3ZM30.78,22.62A1.86,1.86,0,0,1,29,23.79H19a3.72,3.72,0,0,1-1.66-.18,1.58,1.58,0,0,1-.5-.32,2.09,2.09,0,0,1-.19-.21.44.44,0,0,1-.08-.11l-.07-.09-.06-.14a.53.53,0,0,1-.06-.13,6.17,6.17,0,0,1-.15-1.92V11A1.89,1.89,0,1,1,20,11v9h9a1.87,1.87,0,0,1,1.74,1.18,1.77,1.77,0,0,1,.15.73A1.71,1.71,0,0,1,30.78,22.62Z"/></svg>
\ 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 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 792 792"><defs><style>.cls-1{fill:#fff;}</style></defs><path class="cls-1" d="M529.09,727.42c-13.39.28-26.47,4.51-39.46,8-98.67,26.36-208.29,8.38-293.37-48.12S52.09,536.73,38.1,435.57a350.84,350.84,0,0,1-3.23-53.72A364.21,364.21,0,0,1,39.78,328a371.24,371.24,0,0,1,33-102.77,361.08,361.08,0,0,1,27.41-46.61A344.94,344.94,0,0,1,133.51,138a348.47,348.47,0,0,1,37.68-34.27,363.56,363.56,0,0,1,42.18-28.57,376.28,376.28,0,0,1,94.16-38.63,372.16,372.16,0,0,1,50.1-9.4,358.89,358.89,0,0,1,50.88-2.33,343.19,343.19,0,0,1,48.94,4.75C579.91,50.49,688.52,139.18,733.51,255s24.75,254.55-51.46,352.65c-8.07,10.4-16.91,20.7-20.93,33.23-3.38,10.53-3.1,21.83-2.78,32.89l2.25,78.08c.13,4.52.06,9.6-3.15,12.79-4.57,4.55-12.21,2.46-18.2.07l-65.65-26.15c-13.22-5.27-26.77-10.61-41-11.09C531.43,727.4,530.26,727.4,529.09,727.42Z"/></svg>
\ 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<AddCommentProps> = ({ + 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 ( + <View style={styles.container}> + <Image + style={styles.avatar} + source={ + avatar + ? {uri: avatar} + : require('../../assets/images/avatar-placeholder.png') + } + /> + <TaggBigInput + style={styles.text} + multiline + placeholder="Add a comment....." + placeholderTextColor="gray" + onChangeText={handleCommentUpdate} + onSubmitEditing={postComment} + value={comment} + /> + </View> + ); +}; +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<CommentTileProps> = ({comment_object}) => { + const timePosted = getTimePosted(comment_object.date_time); + return ( + <View style={styles.container}> + <ProfilePreview + profilePreview={{ + id: comment_object.commenter__id, + username: comment_object.commenter__username, + first_name: '', + last_name: '', + }} + isComment={true} + /> + <View style={styles.body}> + <Text style={styles.comment}>{comment_object.comment}</Text> + <View style={styles.clockIconAndTime}> + <ClockIcon style={styles.clockIcon} /> + <Text style={styles.date_time}>{' ' + timePosted}</Text> + </View> + </View> + </View> + ); +}; + +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<CommentsCountProps> = ({ + comments_count, + isProfileView, + moment_id, +}) => { + const navigation = useNavigation(); + const navigateToCommentsScreen = async () => { + navigation.navigate('MomentCommentsScreen', { + isProfileView: isProfileView, + moment_id: moment_id, + }); + }; + return ( + <> + <TouchableOpacity onPress={() => navigateToCommentsScreen()}> + <CommentIcon style={styles.image} /> + <Text style={styles.count}> + {comments_count !== '0' ? comments_count : ''} + </Text> + </TouchableOpacity> + </> + ); +}; + +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/profile/CaptionScreenHeader.tsx b/src/components/moments/CaptionScreenHeader.tsx index 4715b4ef..4715b4ef 100644 --- a/src/components/profile/CaptionScreenHeader.tsx +++ b/src/components/moments/CaptionScreenHeader.tsx diff --git a/src/components/profile/Moment.tsx b/src/components/moments/Moment.tsx index 1ec5511e..1ec5511e 100644 --- a/src/components/profile/Moment.tsx +++ b/src/components/moments/Moment.tsx diff --git a/src/components/profile/MomentTile.tsx b/src/components/moments/MomentTile.tsx index 70b20d40..70b20d40 100644 --- a/src/components/profile/MomentTile.tsx +++ b/src/components/moments/MomentTile.tsx 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/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<ContentProps> = ({y, isProfileView}) => { }); setImagesMap(map); - console.log(map); }, [imagesList]); useEffect(() => { @@ -56,7 +55,7 @@ const Content: React.FC<ContentProps> = ({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/search/SearchResult.tsx b/src/components/profile/ProfilePreview.tsx index 04624004..c527746a 100644 --- a/src/components/search/SearchResult.tsx +++ b/src/components/profile/ProfilePreview.tsx @@ -19,11 +19,23 @@ const NO_USER: UserType = { username: '', }; -interface SearchResultProps extends ViewProps { +/** + * 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 SearchResult: React.FC<SearchResultProps> = ({ +const ProfilePreview: React.FC<ProfilePreviewProps> = ({ profilePreview: {username, first_name, last_name, id}, + isComment, style, }) => { const navigation = useNavigation(); @@ -78,58 +90,75 @@ const SearchResult: React.FC<SearchResultProps> = ({ 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(); + 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); } - recentlySearchedList.unshift(user); } + } else { + recentlySearchedList = [user]; + } + + try { + let recentlySearchedListString = JSON.stringify(recentlySearchedList); + await AsyncStorage.setItem( + '@recently_searched_users', + recentlySearchedListString, + ); + } catch (e) { + console.log(e); } - } 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); + 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 ( <TouchableOpacity onPress={addToRecentlyStoredAndNavigateToProfile} style={[styles.container, style]}> <Image - style={styles.avatar} + style={avatarStyle} source={ avatarURI ? {uri: avatarURI} @@ -137,8 +166,12 @@ const SearchResult: React.FC<SearchResultProps> = ({ } /> <View style={styles.nameContainer}> - <Text style={styles.username}>@{username}</Text> - <Text style={styles.name}>{first_name.concat(' ', last_name)}</Text> + <Text style={usernameStyle}>{usernameToDisplay}</Text> + {first_name ? ( + <Text style={styles.name}>{first_name.concat(' ', last_name)}</Text> + ) : ( + React.Fragment + )} </View> </TouchableOpacity> ); @@ -149,24 +182,35 @@ const styles = StyleSheet.create({ flexDirection: 'row', alignItems: 'center', }, - avatar: { + 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', }, - username: { + searchUsername: { fontSize: 18, fontWeight: '500', }, + commentUsername: { + fontSize: 16, + fontWeight: '500', + }, name: { fontSize: 16, color: '#333', }, }); -export default SearchResult; +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/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<ProfilePreviewType>; @@ -9,10 +9,11 @@ const SearchResults: React.FC<SearchResultsProps> = ({results}) => { return ( <View> {results.map((profilePreview) => ( - <SearchResult + <ProfilePreview style={styles.result} key={profilePreview.id} {...{profilePreview}} + isComment={false} /> ))} </View> 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<ProfileStackParams, 'Profile'>; interface ProfileStackProps { @@ -76,6 +90,18 @@ const Profile: React.FC<ProfileStackProps> = ({route}) => { options={{headerShown: false}} initialParams={{isProfileView: isProfileView}} /> + <ProfileStack.Screen + name="ProfileView" + component={ProfileScreen} + options={{headerShown: false}} + initialParams={{isProfileView: isProfileView}} + /> + <ProfileStack.Screen + name="MomentCommentsScreen" + component={MomentCommentsScreen} + options={{headerShown: false}} + initialParams={{isProfileView: isProfileView}} + /> </ProfileStack.Navigator> ); }; 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<ProfileStackParams>(); 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<IndividualMomentProps> = ({ const {isProfileView} = route.params; const { user: {userId}, + logout, } = React.useContext(AuthContext); 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 handleCaptionUpdate = (caption: string) => { setCaption(caption); }; @@ -58,35 +68,20 @@ const IndividualMoment: React.FC<IndividualMomentProps> = ({ 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<IndividualMomentProps> = ({ source={{uri: path_hash}} resizeMode={'cover'} /> + <View style={styles.bodyContainer}> - <Animated.Text style={styles.text}>{caption}</Animated.Text> + <CommentsCount + comments_count={comments_count} + isProfileView={isProfileView} + moment_id={moment_id} + /> <Animated.Text style={styles.text}>{elapsedTime}</Animated.Text> </View> + <Animated.Text style={styles.text}>{caption}</Animated.Text> </View> ); }; @@ -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<MomentCommentsScreenProps> = ({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 ( + <CenteredView> + <View style={styles.modalView}> + <View style={styles.header}> + <Button + title="X" + buttonStyle={styles.button} + titleStyle={styles.buttonText} + onPress={() => { + navigation.goBack(); + }} + /> + <Text style={styles.headerText}> + {commentsList.length + ' Comments'} + </Text> + </View> + <ScrollView + style={styles.modalScrollView} + contentContainerStyle={styles.modalScrollViewContent}> + {commentsList && + commentsList.map((comment: CommentType) => ( + <CommentTile key={comment.comment_id} comment_object={comment} /> + ))} + </ScrollView> + <AddComment + setNewCommentsAvailable={setNewCommentsAvailable} + moment_id={moment_id} + /> + </View> + </CenteredView> + ); +}; + +const styles = StyleSheet.create({ + header: {flexDirection: 'row'}, + headerText: { + position: 'relative', + left: '180%', + alignSelf: 'center', + fontSize: 18, + fontWeight: '500', + }, + container: { + position: 'relative', + top: '5%', + left: '5%', + backgroundColor: 'white', + borderRadius: 5, + width: SCREEN_WIDTH / 1.1, + height: '55%', + }, + button: { + backgroundColor: 'transparent', + }, + buttonText: { + color: 'black', + fontSize: 18, + fontWeight: '400', + }, + modalView: { + width: '85%', + height: '70%', + backgroundColor: '#fff', + shadowColor: '#000', + shadowOpacity: 30, + shadowOffset: {width: 0, height: 2}, + shadowRadius: 5, + borderRadius: 8, + paddingBottom: 15, + paddingHorizontal: 20, + paddingTop: 5, + justifyContent: 'space-between', + }, + modalScrollViewContent: { + justifyContent: 'center', + }, + modalScrollView: { + marginBottom: 10, + }, +}); + +export default MomentCommentsScreen; diff --git a/src/screens/profile/SocialMediaTaggs.tsx b/src/screens/profile/SocialMediaTaggs.tsx index 5a2e638e..0ac6d1ef 100644 --- a/src/screens/profile/SocialMediaTaggs.tsx +++ b/src/screens/profile/SocialMediaTaggs.tsx @@ -1,6 +1,6 @@ import {RouteProp} from '@react-navigation/native'; import React from 'react'; -import {ScrollView, StatusBar, StyleSheet, View} from 'react-native'; +import {Alert, ScrollView, StatusBar, StyleSheet, View} from 'react-native'; import LinearGradient from 'react-native-linear-gradient'; import {AVATAR_GRADIENT} from '../../constants'; import { @@ -43,6 +43,7 @@ const SocialMediaTaggs: React.FC<SocialMediaTaggsProps> = ({route}) => { socialAccounts, } = context; + const handle = socialAccounts[socialMediaType].handle; const posts = socialAccounts[socialMediaType].posts || []; const headerHeight = headerBarHeightWithImage(); diff --git a/src/screens/profile/index.ts b/src/screens/profile/index.ts index 6319c17d..9dfbe409 100644 --- a/src/screens/profile/index.ts +++ b/src/screens/profile/index.ts @@ -2,3 +2,4 @@ export {default as ProfileScreen} from './ProfileScreen'; export {default as SocialMediaTaggs} from './SocialMediaTaggs'; export {default as CaptionScreen} from './CaptionScreen'; export {default as IndividualMoment} from './IndividualMoment'; +export {default as MomentCommentsScreen} from './MomentCommentsScreen'; diff --git a/src/services/MomentServices.ts b/src/services/MomentServices.ts new file mode 100644 index 00000000..60f516ce --- /dev/null +++ b/src/services/MomentServices.ts @@ -0,0 +1,98 @@ +//Common moments api abstracted out here + +import {COMMENTS_ENDPOINT} from '../constants'; +import {Alert} from 'react-native'; + +//Get all comments for a moment +export const getMomentComments = async ( + momentId: string, + callback: Function, + token: string, +) => { + try { + const response = await fetch(COMMENTS_ENDPOINT + '?moment_id=' + momentId, { + method: 'GET', + headers: { + Authorization: 'Token ' + token, + }, + }); + const status = response.status; + if (status === 200) { + const comments = await response.json(); + callback(comments); + } else { + console.log('Could not load comments'); + } + } catch (error) { + console.log('Could not load comments', error); + } +}; + +//Post a comment on a moment +export const postMomentComment = async ( + commenter: string, + comment: string, + momentId: string, + token: string, +) => { + try { + const request = new FormData(); + request.append('moment_id', momentId); + request.append('commenter', commenter); + request.append('comment', comment); + const response = await fetch(COMMENTS_ENDPOINT, { + method: 'POST', + headers: { + Authorization: 'Token ' + token, + }, + body: request, + }); + const status = response.status; + if (status === 200) { + const response_data = await response.json(); + return response_data; + } else { + Alert.alert('Something went wrong! ðŸ˜', 'Not able to post a comment'); + return {}; + } + } catch (error) { + Alert.alert( + 'Something went wrong! ðŸ˜', + 'Not able to post a comment', + error, + ); + return {}; + } +}; + +//Get count of comments for a moment +export const getMomentCommentsCount = async ( + momentId: string, + callback: Function, + token: string, +) => { + try { + const response = await fetch(COMMENTS_ENDPOINT + `${momentId}/`, { + method: 'GET', + headers: { + Authorization: 'Token ' + token, + }, + }); + const status = response.status; + if (status === 200) { + const response_data = await response.json(); + callback(response_data['count']); + } else { + console.log( + 'Something went wrong! ðŸ˜', + 'Not able to retrieve comments count', + ); + } + } catch (error) { + console.log( + 'Something went wrong! ðŸ˜', + 'Not able to retrieve comments count', + error, + ); + } +}; diff --git a/src/services/index.ts b/src/services/index.ts index 5cd06cfe..2abcef95 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1 +1,2 @@ export * from './UserProfileService'; +export * from './MomentServices'; diff --git a/src/types/types.ts b/src/types/types.ts index aa46fd36..f9929017 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -74,3 +74,11 @@ export interface MomentType { path_hash: string; moment_id: string; } + +export interface CommentType { + comment_id: string, + comment: string, + date_time: string, + commenter__id: string, + commenter__username: string +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 5bc168e3..a7e45979 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,2 +1,3 @@ export * from './screenDimensions'; export * from './statusBarHeight'; +export * from './moments'; diff --git a/src/utils/moments.ts b/src/utils/moments.ts new file mode 100644 index 00000000..0f8021cb --- /dev/null +++ b/src/utils/moments.ts @@ -0,0 +1,33 @@ +import moment from 'moment'; + +//A util that calculates the difference between a given time and current time +//Returns the difference in the largest possible unit of time (days > hours > minutes > seconds) + +export const getTimePosted = (date_time: string) => { + const datePosted = moment(date_time); + const now = moment(); + var time = date_time; + var difference = now.diff(datePosted, 'seconds'); + + //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'); + } + //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'); + } + return time; +}; |