diff options
23 files changed, 519 insertions, 249 deletions
diff --git a/ios/Frontend/Info.plist b/ios/Frontend/Info.plist index 0f036f56..dd9ebb83 100644 --- a/ios/Frontend/Info.plist +++ b/ios/Frontend/Info.plist @@ -53,7 +53,7 @@ <key>NSMainNibFile</key> <string>LaunchScreen</string> <key>NSPhotoLibraryUsageDescription</key> - <string>Tagg would like to access your photos.</string> + <string>This lets you share photos from your library and select profile displays</string> <key>UIAppFonts</key> <array> <string>Feather.ttf</string> diff --git a/src/assets/images/welcome.png b/src/assets/images/welcome.png Binary files differindex 46ab4f9f..62b46077 100644 --- a/src/assets/images/welcome.png +++ b/src/assets/images/welcome.png diff --git a/src/components/comments/AddComment.tsx b/src/components/comments/AddComment.tsx index 44f49748..56011f05 100644 --- a/src/components/comments/AddComment.tsx +++ b/src/components/comments/AddComment.tsx @@ -8,10 +8,11 @@ import { View, } from 'react-native'; import {TextInput, TouchableOpacity} from 'react-native-gesture-handler'; -import {useSelector} from 'react-redux'; +import {useDispatch, useSelector} from 'react-redux'; import UpArrowIcon from '../../assets/icons/up_arrow.svg'; import {TAGG_LIGHT_BLUE} from '../../constants'; import {postComment} from '../../services'; +import {updateReplyPosted} from '../../store/actions'; import {RootState} from '../../store/rootreducer'; import {SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils'; @@ -38,6 +39,7 @@ const AddComment: React.FC<AddCommentProps> = ({ const [keyboardVisible, setKeyboardVisible] = React.useState(false); const {avatar} = useSelector((state: RootState) => state.user); + const dispatch = useDispatch(); const addComment = async () => { const trimmed = comment.trim(); @@ -52,6 +54,18 @@ const AddComment: React.FC<AddCommentProps> = ({ if (postedComment) { setComment(''); + + //Set new reply posted object + //This helps us show the latest reply on top + //Data set is kind of stale but it works + if (isCommentInFocus) { + dispatch( + updateReplyPosted({ + comment_id: postedComment.comment_id, + parent_comment: {comment_id: objectId}, + }), + ); + } setNewCommentsAvailable(true); } }; diff --git a/src/components/comments/CommentTile.tsx b/src/components/comments/CommentTile.tsx index b631a985..be113523 100644 --- a/src/components/comments/CommentTile.tsx +++ b/src/components/comments/CommentTile.tsx @@ -1,3 +1,4 @@ +/* eslint-disable radix */ import React, {Fragment, useEffect, useRef, useState} from 'react'; import {Text, View} from 'react-native-animatable'; import {ProfilePreview} from '../profile'; @@ -11,8 +12,10 @@ import Arrow from '../../assets/icons/back-arrow-colored.svg'; import Trash from '../../assets/ionicons/trash-outline.svg'; import CommentsContainer from './CommentsContainer'; import Swipeable from 'react-native-gesture-handler/Swipeable'; -import {deleteComment} from '../../services'; +import {deleteComment, getCommentsCount} from '../../services'; import {ERROR_FAILED_TO_DELETE_COMMENT} from '../../constants/strings'; +import {useSelector} from 'react-redux'; +import {RootState} from '../../store/rootReducer'; /** * Displays users's profile picture, comment posted by them and the time difference between now and when a comment was posted. @@ -39,10 +42,13 @@ const CommentTile: React.FC<CommentTileProps> = ({ }) => { const timePosted = getTimePosted(comment_object.date_created); const [showReplies, setShowReplies] = useState<boolean>(false); + const [showKeyboard, setShowKeyboard] = useState<boolean>(false); const [newThreadAvailable, setNewThreadAvailable] = useState(true); const swipeRef = useRef<Swipeable>(null); const isThread = typeOfComment === 'Thread'; + const {replyPosted} = useSelector((state: RootState) => state.user); + /** * Bubbling up, for handling a new comment in a thread. */ @@ -52,23 +58,43 @@ const CommentTile: React.FC<CommentTileProps> = ({ } }, [newCommentsAvailable]); + useEffect(() => { + if (replyPosted && typeOfComment === 'Comment') { + if (replyPosted.parent_comment.comment_id === comment_object.comment_id) { + setShowReplies(true); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [replyPosted]); + /** * Case : A COMMENT IS IN FOCUS && REPLY SECTION IS HIDDEN * Bring the current comment to focus * Case : No COMMENT IS IN FOCUS && REPLY SECTION IS SHOWN * Unfocus comment in focus - * In any case toggle value of showReplies */ - const toggleReplies = () => { - if (setCommentObjectInFocus) { - if (!showReplies) { - setCommentObjectInFocus(comment_object); - } else { - setNewThreadAvailable(true); - setNewCommentsAvailable(true); - setCommentObjectInFocus(undefined); + const toggleAddComment = () => { + //Do not allow user to reply to a thread + if (!isThread) { + if (setCommentObjectInFocus) { + if (!showKeyboard) { + setCommentObjectInFocus(comment_object); + } else { + setCommentObjectInFocus(undefined); + } } + setShowKeyboard(!showKeyboard); } + }; + + const toggleReplies = async () => { + if (showReplies) { + //To update count of replies in case we deleted a reply + comment_object.replies_count = parseInt( + await getCommentsCount(comment_object.comment_id, true), + ); + } + setNewThreadAvailable(true); setShowReplies(!showReplies); }; @@ -104,7 +130,7 @@ const CommentTile: React.FC<CommentTileProps> = ({ ); }; - const renderRightActions = (progress) => + const renderRightActions = (progress: Animated.AnimatedInterpolation) => canDelete ? ( <View style={styles.swipeActions}> {renderRightAction('Delete', '#c42634', progress)} @@ -127,42 +153,44 @@ const CommentTile: React.FC<CommentTileProps> = ({ previewType={'Comment'} screenType={screenType} /> - <TouchableOpacity style={styles.body} onPress={toggleReplies}> + <TouchableOpacity style={styles.body} onPress={toggleAddComment}> <Text style={styles.comment}>{comment_object.comment}</Text> <View style={styles.clockIconAndTime}> <ClockIcon style={styles.clockIcon} /> <Text style={styles.date_time}>{' ' + timePosted}</Text> <View style={styles.flexer} /> - - {/*** Show replies text only if there are some replies present */} - {typeOfComment === 'Comment' && comment_object.replies_count > 0 && ( - <View style={styles.repliesTextAndIconContainer}> - <Text style={styles.repliesText}>{getRepliesText()}</Text> - <Arrow - width={12} - height={11} - color={TAGG_LIGHT_BLUE} - style={ - !showReplies - ? styles.repliesDownArrow - : styles.repliesUpArrow - } - /> - </View> - )} </View> </TouchableOpacity> + {/*** Show replies text only if there are some replies present */} + {typeOfComment === 'Comment' && comment_object.replies_count > 0 && ( + <TouchableOpacity + style={styles.repliesTextAndIconContainer} + onPress={toggleReplies}> + <Text style={styles.repliesText}>{getRepliesText()}</Text> + <Arrow + width={12} + height={11} + color={TAGG_LIGHT_BLUE} + style={ + !showReplies ? styles.repliesDownArrow : styles.repliesUpArrow + } + /> + </TouchableOpacity> + )} </View> {/*** Show replies if toggle state is true */} {showReplies && ( - <CommentsContainer - objectId={comment_object.comment_id} - screenType={screenType} - setNewCommentsAvailable={setNewThreadAvailable} - newCommentsAvailable={newThreadAvailable} - typeOfComment={'Thread'} - /> + <View> + <CommentsContainer + objectId={comment_object.comment_id} + screenType={screenType} + setNewCommentsAvailable={setNewThreadAvailable} + newCommentsAvailable={newThreadAvailable} + typeOfComment={'Thread'} + commentId={replyPosted?.comment_id} + /> + </View> )} </Swipeable> ); @@ -173,9 +201,10 @@ const styles = StyleSheet.create({ borderBottomWidth: 1, borderColor: 'lightgray', backgroundColor: 'white', - paddingTop: '3%', flexDirection: 'column', flex: 1, + paddingTop: '3%', + paddingBottom: '5%', marginLeft: '7%', }, swipeActions: { @@ -190,7 +219,6 @@ const styles = StyleSheet.create({ comment: { marginBottom: '2%', marginRight: '2%', - top: -5, }, date_time: { color: 'gray', @@ -203,7 +231,7 @@ const styles = StyleSheet.create({ }, clockIconAndTime: { flexDirection: 'row', - marginBottom: '3%', + marginTop: '3%', }, flexer: { flex: 1, @@ -211,23 +239,25 @@ const styles = StyleSheet.create({ repliesTextAndIconContainer: { flexDirection: 'row', alignItems: 'center', + marginTop: '5%', + marginLeft: 56, }, repliesText: { color: TAGG_LIGHT_BLUE, fontWeight: '500', fontSize: normalize(12), - marginRight: '5%', + marginRight: '1%', }, repliesBody: { width: SCREEN_WIDTH, }, repliesDownArrow: { transform: [{rotate: '270deg'}], - marginTop: '7%', + marginTop: '1%', }, repliesUpArrow: { transform: [{rotate: '90deg'}], - marginTop: '7%', + marginTop: '1%', }, actionText: { color: 'white', diff --git a/src/components/comments/CommentsContainer.tsx b/src/components/comments/CommentsContainer.tsx index d8134caf..c72da2b7 100644 --- a/src/components/comments/CommentsContainer.tsx +++ b/src/components/comments/CommentsContainer.tsx @@ -1,16 +1,18 @@ import React, {useEffect, useRef, useState} from 'react'; import {StyleSheet} from 'react-native'; -import {ScrollView} from 'react-native-gesture-handler'; +import {FlatList} from 'react-native-gesture-handler'; import {useDispatch, useSelector} from 'react-redux'; import {CommentTile} from '.'; import {getComments} from '../../services'; +import {updateReplyPosted} from '../../store/actions'; import {RootState} from '../../store/rootReducer'; import {CommentType, ScreenType, TypeOfComment} from '../../types'; +import {SCREEN_HEIGHT} from '../../utils'; export type CommentsContainerProps = { screenType: ScreenType; - //objectId can be either moment_id or comment_id objectId: string; + commentId?: string; setCommentsLength?: (count: number) => void; newCommentsAvailable: boolean; setNewCommentsAvailable: (value: boolean) => void; @@ -32,34 +34,36 @@ const CommentsContainer: React.FC<CommentsContainerProps> = ({ typeOfComment, setCommentObjectInFocus, commentObjectInFocus, + commentId, }) => { const {username: loggedInUsername} = useSelector( (state: RootState) => state.user.user, ); const [commentsList, setCommentsList] = useState<CommentType[]>([]); const dispatch = useDispatch(); - const ref = useRef<ScrollView>(null); + const ref = useRef<FlatList<CommentType>>(null); useEffect(() => { - //Scroll only if a new comment and not a reply was posted - const shouldScroll = () => - typeOfComment === 'Comment' && !commentObjectInFocus; const loadComments = async () => { - const comments = await getComments(objectId, typeOfComment === 'Thread'); - setCommentsList(comments); - if (setCommentsLength) { - setCommentsLength(comments.length); - } - setNewCommentsAvailable(false); + await getComments(objectId, typeOfComment === 'Thread').then( + (comments) => { + if (comments && subscribedToLoadComments) { + setCommentsList(comments); + if (setCommentsLength) { + setCommentsLength(comments.length); + } + setNewCommentsAvailable(false); + } + }, + ); }; + let subscribedToLoadComments = true; if (newCommentsAvailable) { loadComments(); - if (shouldScroll()) { - setTimeout(() => { - ref.current?.scrollToEnd(); - }, 500); - } } + return () => { + subscribedToLoadComments = false; + }; }, [ dispatch, objectId, @@ -67,28 +71,93 @@ const CommentsContainer: React.FC<CommentsContainerProps> = ({ setNewCommentsAvailable, setCommentsLength, typeOfComment, - commentObjectInFocus, ]); + // eslint-disable-next-line no-shadow + const swapCommentTo = (commentId: string, toIndex: number) => { + const index = commentsList.findIndex( + (item) => item.comment_id === commentId, + ); + if (index > 0) { + let comments = [...commentsList]; + const temp = comments[index]; + comments[index] = comments[toIndex]; + comments[toIndex] = temp; + setCommentsList(comments); + } + }; + + useEffect(() => { + //Scroll only if a new comment and not a reply was posted + const shouldScroll = () => + typeOfComment === 'Comment' && !commentObjectInFocus; + + const performAction = () => { + if (commentId) { + swapCommentTo(commentId, 0); + } else if (shouldScroll()) { + setTimeout(() => { + ref.current?.scrollToEnd({animated: true}); + }, 500); + } + }; + if (commentsList) { + //Bring the relevant comment to top if a comment id is present else scroll if necessary + performAction(); + } + + //Clean up the reply id present in store + return () => { + if (commentId && typeOfComment === 'Thread') { + setTimeout(() => { + dispatch(updateReplyPosted(undefined)); + }, 200); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [commentsList, commentId]); + + //WIP : TODO : Bring the comment in focus above the keyboard + // useEffect(() => { + // if (commentObjectInFocus && commentsList.length >= 3) { + // swapCommentTo(commentObjectInFocus.comment_id, 2); + // } + // // eslint-disable-next-line react-hooks/exhaustive-deps + // }, [commentObjectInFocus]); + + const ITEM_HEIGHT = SCREEN_HEIGHT / 7.0; + + const renderComment = ({item}: {item: CommentType}) => ( + <CommentTile + key={item.comment_id} + comment_object={item} + screenType={screenType} + typeOfComment={typeOfComment} + setCommentObjectInFocus={setCommentObjectInFocus} + newCommentsAvailable={newCommentsAvailable} + setNewCommentsAvailable={setNewCommentsAvailable} + canDelete={item.commenter.username === loggedInUsername} + /> + ); + return ( - <ScrollView + <FlatList + data={commentsList} ref={ref} - style={styles.scrollView} - contentContainerStyle={styles.scrollViewContent}> - {commentsList && - commentsList.map((comment: CommentType) => ( - <CommentTile - key={comment.comment_id} - comment_object={comment} - screenType={screenType} - typeOfComment={typeOfComment} - setCommentObjectInFocus={setCommentObjectInFocus} - newCommentsAvailable={newCommentsAvailable} - setNewCommentsAvailable={setNewCommentsAvailable} - canDelete={comment.commenter.username === loggedInUsername} - /> - ))} - </ScrollView> + keyExtractor={(item, index) => index.toString()} + decelerationRate={'fast'} + snapToAlignment={'start'} + snapToInterval={ITEM_HEIGHT} + renderItem={renderComment} + showsVerticalScrollIndicator={false} + contentContainerStyle={styles.scrollViewContent} + getItemLayout={(data, index) => ({ + length: ITEM_HEIGHT, + offset: ITEM_HEIGHT * index, + index, + })} + pagingEnabled + /> ); }; diff --git a/src/components/common/TaggSquareButton.tsx b/src/components/common/TaggSquareButton.tsx new file mode 100644 index 00000000..4fe61b95 --- /dev/null +++ b/src/components/common/TaggSquareButton.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import { + GestureResponderEvent, + StyleSheet, + Text, + TouchableOpacity, + ViewProps, + ViewStyle, +} from 'react-native'; +import {normalize, SCREEN_WIDTH} from '../../utils'; + +interface TaggSquareButtonProps extends ViewProps { + onPress: (event: GestureResponderEvent) => void; + title: string; + mode: 'normal' | 'large'; + color: 'purple' | 'white'; + style?: ViewStyle; +} + +const TaggSquareButton: React.FC<TaggSquareButtonProps> = (props) => { + const buttonStyles = (() => { + switch (props.color) { + case 'purple': + return {backgroundColor: '#8F01FF'}; + case 'white': + default: + return {backgroundColor: 'white'}; + } + })(); + switch (props.mode) { + case 'large': + return ( + <TouchableOpacity + onPress={props.onPress} + style={[styles.largeButton, buttonStyles, props.style]}> + <Text style={styles.largeLabel}>{props.title}</Text> + </TouchableOpacity> + ); + case 'normal': + default: + return ( + <TouchableOpacity + onPress={props.onPress} + style={[styles.normalButton, buttonStyles, props.style]}> + <Text style={styles.normalLabel}>{props.title}</Text> + </TouchableOpacity> + ); + } +}; + +const styles = StyleSheet.create({ + largeButton: { + justifyContent: 'center', + alignItems: 'center', + width: '70%', + height: '10%', + borderRadius: 5, + }, + largeLabel: { + fontSize: normalize(26), + fontWeight: '500', + color: '#eee', + }, + normalButton: { + justifyContent: 'center', + alignItems: 'center', + width: SCREEN_WIDTH * 0.45, + aspectRatio: 3.7, + borderRadius: 5, + marginBottom: '5%', + }, + normalLabel: { + fontSize: normalize(20), + fontWeight: '500', + color: '#78A0EF', + }, +}); + +export default TaggSquareButton; diff --git a/src/components/common/index.ts b/src/components/common/index.ts index 30ec6d02..95854ba8 100644 --- a/src/components/common/index.ts +++ b/src/components/common/index.ts @@ -21,3 +21,4 @@ export {default as TaggPopUp} from './TaggPopup'; export {default as TaggPrompt} from './TaggPrompt'; export {default as AcceptDeclineButtons} from './AcceptDeclineButtons'; export {default as FriendsButton} from './FriendsButton'; +export {default as TaggSquareButton} from './TaggSquareButton'; diff --git a/src/components/notifications/Notification.tsx b/src/components/notifications/Notification.tsx index e648b554..951a5bf6 100644 --- a/src/components/notifications/Notification.tsx +++ b/src/components/notifications/Notification.tsx @@ -1,18 +1,31 @@ import {useNavigation} from '@react-navigation/native'; import React, {useEffect, useState} from 'react'; -import {Image, StyleSheet, Text, View} from 'react-native'; +import {Alert, Image, StyleSheet, Text, View} from 'react-native'; import {TouchableWithoutFeedback} from 'react-native-gesture-handler'; +import LinearGradient from 'react-native-linear-gradient'; import {useDispatch, useStore} from 'react-redux'; -import {loadImageFromURL, loadMomentThumbnail} from '../../services'; +import {BACKGROUND_GRADIENT_MAP} from '../../constants'; +import {ERROR_DELETED_OBJECT} from '../../constants/strings'; +import { + loadImageFromURL, + loadMoments, + loadMomentThumbnail, +} from '../../services'; import { acceptFriendRequest, declineFriendRequest, loadUserNotifications, + updateReplyPosted, updateUserXFriends, } from '../../store/actions'; import {RootState} from '../../store/rootReducer'; import {MomentType, NotificationType, ScreenType} from '../../types'; -import {fetchUserX, SCREEN_HEIGHT, userXInStore} from '../../utils'; +import { + fetchUserX, + getTokenOrLogout, + SCREEN_HEIGHT, + userXInStore, +} from '../../utils'; import AcceptDeclineButtons from '../common/AcceptDeclineButtons'; interface NotificationProps { @@ -40,27 +53,51 @@ const Notification: React.FC<NotificationProps> = (props) => { const [avatar, setAvatar] = useState<string | undefined>(undefined); const [momentURI, setMomentURI] = useState<string | undefined>(undefined); - const backgroundColor = unread ? '#DCF1F1' : 'rgba(0,0,0,0)'; + const [onTapLoadProfile, setOnTapLoadProfile] = useState<boolean>(false); useEffect(() => { (async () => { const response = await loadImageFromURL(thumbnail_url); if (response) { setAvatar(response); + } else { + setAvatar(undefined); } })(); }, []); useEffect(() => { + if (onTapLoadProfile) { + fetchUserX(dispatch, {userId: id, username: username}, screenType); + } + return () => { + setOnTapLoadProfile(false); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [onTapLoadProfile]); + + useEffect(() => { let mounted = true; const loadMomentImage = async (moment_id: string) => { const response = await loadMomentThumbnail(moment_id); if (mounted && response) { setMomentURI(response); + } else { + // if not set to empty, it will re-use the previous notification's state + setMomentURI(undefined); } }; - if (notification_type === 'CMT' && notification_object) { - loadMomentImage(notification_object.moment_id); + if ( + (notification_type === 'CMT' || + notification_type === 'MOM_3+' || + notification_type === 'MOM_FRIEND') && + notification_object + ) { + loadMomentImage( + notification_object.moment_id + ? notification_object.moment_id + : notification_object.parent_comment.moment_id, + ); return () => { mounted = false; }; @@ -84,31 +121,85 @@ const Notification: React.FC<NotificationProps> = (props) => { }); break; case 'CMT': - // find the moment we need to display - const moment = loggedInUserMoments?.find( - (m) => m.moment_id === notification_object?.moment_id, + //Notification object is set to null if the comment / comment_thread / moment gets deleted + if (!notification_object) { + Alert.alert(ERROR_DELETED_OBJECT); + break; + } + let {moment_id} = notification_object; + let {comment_id} = notification_object; + + //If this is a thread, get comment_id and moment_id from parent_comment + if (!notification_object?.moment_id) { + moment_id = notification_object?.parent_comment?.moment_id; + comment_id = notification_object?.parent_comment?.comment_id; + } + + // Now find the moment we need to display + let moment: MomentType | undefined = loggedInUserMoments?.find( + (m) => m.moment_id === moment_id, ); + let userXId; + + // If moment does not belong to the logged in user, then the comment was probably a reply to logged in user's comment + // on userX's moment + // Load moments for userX + if (!moment) { + let moments: MomentType[] = []; + try { + //Populate local state in the mean time + setOnTapLoadProfile(true); + const token = await getTokenOrLogout(dispatch); + moments = await loadMoments(id, token); + } catch (err) { + console.log(err); + } + moment = moments?.find((m) => m.moment_id === moment_id); + userXId = id; + } + + //Now if moment was found, navigate to the respective moment if (moment) { + if (notification_object?.parent_comment) { + dispatch(updateReplyPosted(notification_object)); + } navigation.push('IndividualMoment', { moment, - userXId: undefined, // we're only viewing our own moment here + userXId: userXId, // we're only viewing our own moment here screenType, }); setTimeout(() => { navigation.push('MomentCommentsScreen', { - moment_id: moment.moment_id, + moment_id: moment_id, screenType, + comment_id: comment_id, }); }, 500); } break; + case 'MOM_3+': + case 'MOM_FRIEND': + const object = notification_object as MomentType; + await fetchUserX( + dispatch, + {userId: id, username: username}, + screenType, + ); + navigation.push('IndividualMoment', { + moment: object, + userXId: id, + screenType, + }); + break; default: break; } }; const handleAcceptRequest = async () => { - await dispatch(acceptFriendRequest({id, username, first_name, last_name})); + await dispatch( + acceptFriendRequest({id, username, first_name, last_name, thumbnail_url}), + ); await dispatch(updateUserXFriends(id, state)); dispatch(loadUserNotifications()); }; @@ -118,48 +209,57 @@ const Notification: React.FC<NotificationProps> = (props) => { dispatch(loadUserNotifications()); }; - return ( - <> - <TouchableWithoutFeedback - style={[styles.container, {backgroundColor}]} - onPress={onNotificationTap}> - <View style={styles.avatarContainer}> - <Image - style={styles.avatar} - source={ - avatar - ? {uri: avatar, cache: 'only-if-cached'} - : require('../../assets/images/avatar-placeholder.png') - } + const renderContent = () => ( + <TouchableWithoutFeedback + style={styles.container} + onPress={onNotificationTap}> + <View style={styles.avatarContainer}> + <Image + style={styles.avatar} + source={ + avatar + ? {uri: avatar} + : require('../../assets/images/avatar-placeholder.png') + } + /> + </View> + <View style={styles.contentContainer}> + <Text style={styles.actorName}> + {first_name} {last_name} + </Text> + <Text>{verbage}</Text> + </View> + {notification_type === 'FRD_REQ' && ( + <View style={styles.buttonsContainer}> + <AcceptDeclineButtons + requester={{id, username, first_name, last_name}} + onAccept={handleAcceptRequest} + onReject={handleDeclineFriendRequest} /> </View> - <View style={styles.contentContainer}> - <Text style={styles.actorName}> - {first_name} {last_name} - </Text> - <Text>{verbage}</Text> - </View> - {notification_type === 'FRD_REQ' && ( - <View style={styles.buttonsContainer}> - <AcceptDeclineButtons - requester={{id, username, first_name, last_name}} - onAccept={handleAcceptRequest} - onReject={handleDeclineFriendRequest} - /> - </View> - )} - {notification_type === 'CMT' && notification_object && ( + )} + {(notification_type === 'CMT' || + notification_type === 'MOM_3+' || + notification_type === 'MOM_FRIEND') && + notification_object && ( <Image style={styles.moment} source={{uri: momentURI}} /> )} - </TouchableWithoutFeedback> - </> + </TouchableWithoutFeedback> + ); + + return unread ? ( + <LinearGradient colors={BACKGROUND_GRADIENT_MAP[2]} useAngle angle={90}> + {renderContent()} + </LinearGradient> + ) : ( + renderContent() ); }; const styles = StyleSheet.create({ container: { flexDirection: 'row', - height: SCREEN_HEIGHT / 10, + height: Math.round(SCREEN_HEIGHT / 10), flex: 1, alignItems: 'center', }, diff --git a/src/components/profile/ProfileBody.tsx b/src/components/profile/ProfileBody.tsx index 555811c9..106b20a7 100644 --- a/src/components/profile/ProfileBody.tsx +++ b/src/components/profile/ProfileBody.tsx @@ -178,9 +178,8 @@ const styles = StyleSheet.create({ height: SCREEN_WIDTH * 0.075, borderColor: TAGG_LIGHT_BLUE, borderWidth: 2, - borderRadius: 0, + borderRadius: 3, marginRight: '2%', - marginLeft: '1%', padding: 0, backgroundColor: 'transparent', }, @@ -204,9 +203,8 @@ const styles = StyleSheet.create({ padding: 0, borderWidth: 2, borderColor: TAGG_LIGHT_BLUE, - borderRadius: 0, + borderRadius: 3, marginRight: '2%', - marginLeft: '1%', backgroundColor: TAGG_LIGHT_BLUE, }, }); diff --git a/src/constants/constants.ts b/src/constants/constants.ts index ad43c337..7fcc457f 100644 --- a/src/constants/constants.ts +++ b/src/constants/constants.ts @@ -130,6 +130,10 @@ export const BACKGROUND_GRADIENT_MAP: Record< > = { [BackgroundGradientType.Light]: ['#9F00FF', '#27EAE9'], [BackgroundGradientType.Dark]: ['#421566', '#385D5E'], + [BackgroundGradientType.Notification]: [ + 'rgba(143, 1, 255, 0.5)', + 'rgba(110, 231, 231, 0.5)', + ], }; export const CLASS_YEAR_LIST: Array<string> = [ diff --git a/src/constants/strings.ts b/src/constants/strings.ts index b0886513..9680320a 100644 --- a/src/constants/strings.ts +++ b/src/constants/strings.ts @@ -10,6 +10,7 @@ export const ERROR_CATEGORY_CREATION = 'There was a problem creating your catego export const ERROR_CATEGORY_UPDATE = 'There was a problem updating your categories. Please refresh and try again'; export const ERROR_DELETE_CATEGORY = 'There was a problem while deleting category. Please try again'; export const ERROR_DELETE_MOMENT = 'Unable to delete moment, please try again later!'; +export const ERROR_DELETED_OBJECT = 'Oh sad! Looks like the comment / moment was deleted by the user'; export const ERROR_DOUBLE_CHECK_CONNECTION = 'Please double-check your network connection and retry'; export const ERROR_DUP_OLD_PWD = 'You may not use a previously used password'; export const ERROR_EMAIL_IN_USE = 'Email already in use, please try another one'; @@ -52,4 +53,4 @@ export const UPLOAD_MOMENT_PROMPT_ONE_MESSAGE = 'Post your first moment to\n con export const UPLOAD_MOMENT_PROMPT_THREE_HEADER = 'Continue to build your profile'; export const UPLOAD_MOMENT_PROMPT_THREE_MESSAGE = 'Continue to personalize your own digital space in\nthis community by filling your profile with\ncategories and moments!'; export const UPLOAD_MOMENT_PROMPT_TWO_HEADER = 'Create a new category'; -export const UPLOAD_MOMENT_PROMPT_TWO_MESSAGE = 'You can now create new categories \nand continue to fill your profile with moments!'; +export const UPLOAD_MOMENT_PROMPT_TWO_MESSAGE = 'You can now create new categories \nand continue to fill your profile with moments!';
\ No newline at end of file diff --git a/src/routes/main/MainStackNavigator.tsx b/src/routes/main/MainStackNavigator.tsx index bd838ef2..663aeaea 100644 --- a/src/routes/main/MainStackNavigator.tsx +++ b/src/routes/main/MainStackNavigator.tsx @@ -37,6 +37,7 @@ export type MainStackParams = { moment_id: string; userXId: string | undefined; screenType: ScreenType; + comment_id?: string; }; FriendsListScreen: { userXId: string | undefined; diff --git a/src/screens/main/NotificationsScreen.tsx b/src/screens/main/NotificationsScreen.tsx index 4bdee942..d9952aa8 100644 --- a/src/screens/main/NotificationsScreen.tsx +++ b/src/screens/main/NotificationsScreen.tsx @@ -70,7 +70,7 @@ const NotificationsScreen: React.FC = () => { //Called when user leaves the screen return () => resetNewNotificationFlag(); - }, [newNotificationReceived]), + }, [newNotificationReceived, dispatch, refreshNotifications]), ); // handles storing and fetching the "previously viewed" information diff --git a/src/screens/onboarding/Login.tsx b/src/screens/onboarding/Login.tsx index 8974e000..2db039c1 100644 --- a/src/screens/onboarding/Login.tsx +++ b/src/screens/onboarding/Login.tsx @@ -11,16 +11,11 @@ import { StyleSheet, Text, TouchableOpacity, - View, } from 'react-native'; import SplashScreen from 'react-native-splash-screen'; import {useDispatch} from 'react-redux'; -import {Background, SubmitButton, TaggInput} from '../../components'; -import { - LOGIN_ENDPOINT, - TAGG_LIGHT_PURPLE, - usernameRegex, -} from '../../constants'; +import {Background, TaggInput, TaggSquareButton} from '../../components'; +import {LOGIN_ENDPOINT, usernameRegex} from '../../constants'; import { ERROR_DOUBLE_CHECK_CONNECTION, ERROR_FAILED_LOGIN_INFO, @@ -31,7 +26,7 @@ import { import {OnboardingStackParams} from '../../routes/onboarding'; import {fcmService} from '../../services'; import {BackgroundGradientType, UserType} from '../../types'; -import {userLogin} from '../../utils'; +import {normalize, userLogin} from '../../utils'; type VerificationScreenRouteProp = RouteProp<OnboardingStackParams, 'Login'>; type VerificationScreenNavigationProp = StackNavigationProp< @@ -215,45 +210,6 @@ const Login: React.FC<LoginProps> = ({navigation}: LoginProps) => { </TouchableOpacity> ); - /** - * Login screen login button. - */ - const LoginButton = () => ( - <SubmitButton - text="Let's Start!" - color="#fff" - style={styles.button} - accessibilityLabel="Let's Start!" - accessibilityHint="Select this after entering your tagg username and password" - onPress={handleLogin} - /> - ); - - /** - * Login screen registration prompt. - */ - const RegistrationPrompt = () => ( - <View style={styles.newUserContainer}> - <Text - accessible={true} - accessibilityLabel="New to tagg?" - style={styles.newUser}> - New to tagg?{' '} - </Text> - <TouchableOpacity - accessibilityLabel="Get started." - accessibilityHint="Select this if you do not have a tagg account"> - <Text - accessible={true} - accessibilityLabel="Get started" - style={styles.getStarted} - onPress={startRegistrationProcess}> - Get started! - </Text> - </TouchableOpacity> - </View> - ); - return ( <Background centered @@ -300,9 +256,19 @@ const Login: React.FC<LoginProps> = ({navigation}: LoginProps) => { ref={inputRef} /> <ForgotPassword /> - <LoginButton /> + <TaggSquareButton + onPress={handleLogin} + title={'Login'} + mode={'normal'} + color={'white'} + /> + <TaggSquareButton + onPress={startRegistrationProcess} + title={'Sign up'} + mode={'normal'} + color={'purple'} + /> </KeyboardAvoidingView> - <RegistrationPrompt /> </Background> ); }; @@ -322,46 +288,17 @@ const styles = StyleSheet.create({ marginBottom: '10%', }, forgotPassword: { - marginTop: 10, - marginBottom: 15, + alignSelf: 'flex-start', + marginVertical: '1%', + borderBottomWidth: 1, + paddingBottom: '1%', + left: '3%', + borderBottomColor: 'white', + marginBottom: '8%', }, forgotPasswordText: { - fontSize: 14, + fontSize: normalize(14), color: '#fff', - textDecorationLine: 'underline', - }, - start: { - width: 144, - height: 36, - justifyContent: 'center', - alignItems: 'center', - backgroundColor: '#fff', - borderRadius: 18, - marginBottom: '15%', - }, - startDisabled: { - backgroundColor: '#ddd', - }, - startText: { - fontSize: 16, - color: '#78a0ef', - fontWeight: 'bold', - }, - newUserContainer: { - flexDirection: 'row', - color: '#fff', - }, - newUser: { - fontSize: 14, - color: TAGG_LIGHT_PURPLE, - }, - getStarted: { - fontSize: 14, - color: '#fff', - textDecorationLine: 'underline', - }, - button: { - marginVertical: '10%', }, }); diff --git a/src/screens/onboarding/WelcomeScreen.tsx b/src/screens/onboarding/WelcomeScreen.tsx index 96d3f929..bfb1a127 100644 --- a/src/screens/onboarding/WelcomeScreen.tsx +++ b/src/screens/onboarding/WelcomeScreen.tsx @@ -1,10 +1,10 @@ +import {StackNavigationProp} from '@react-navigation/stack'; import * as React from 'react'; -import {StyleSheet, View, Text, Image, TouchableOpacity} from 'react-native'; -import {SCREEN_WIDTH} from '../../utils'; -import {Background} from '../../components'; +import {Image, StyleSheet, Text, View} from 'react-native'; +import {Background, TaggSquareButton} from '../../components'; import {OnboardingStackParams} from '../../routes'; -import {StackNavigationProp} from '@react-navigation/stack'; import {BackgroundGradientType} from '../../types'; +import {SCREEN_WIDTH} from '../../utils'; type WelcomeScreenNavigationProps = StackNavigationProp< OnboardingStackParams, @@ -36,9 +36,13 @@ const WelcomeScreen: React.FC<WelcomeScreenProps> = ({navigation}) => { you are, along with all the moments that comprehensively define you! </Text> </View> - <TouchableOpacity onPress={handleNext} style={styles.nextButton}> - <Text style={styles.nextButtonLabel}>Next</Text> - </TouchableOpacity> + <TaggSquareButton + onPress={handleNext} + title={'Next'} + mode={'large'} + color={'purple'} + style={styles.nextButton} + /> </Background> ); }; @@ -75,20 +79,7 @@ const styles = StyleSheet.create({ marginHorizontal: '10%', }, nextButton: { - backgroundColor: '#8F01FF', - justifyContent: 'center', - alignItems: 'center', - width: '70%', - height: '10%', - borderRadius: 5, - borderWidth: 1, - borderColor: '#8F01FF', marginBottom: '15%', }, - nextButtonLabel: { - fontSize: 30, - fontWeight: '500', - color: '#ddd', - }, }); export default WelcomeScreen; diff --git a/src/screens/profile/IndividualMoment.tsx b/src/screens/profile/IndividualMoment.tsx index ea0c8fb6..8c1dc327 100644 --- a/src/screens/profile/IndividualMoment.tsx +++ b/src/screens/profile/IndividualMoment.tsx @@ -4,12 +4,12 @@ import {StackNavigationProp} from '@react-navigation/stack'; import React from 'react'; import {FlatList, StyleSheet, View} from 'react-native'; import {useSelector} from 'react-redux'; -import {ProfileStackParams} from 'src/routes/main/ProfileStack'; import { IndividualMomentTitleBar, MomentPostContent, MomentPostHeader, } from '../../components'; +import {MainStackParams} from '../../routes'; import {RootState} from '../../store/rootreducer'; import {MomentType} from '../../types'; import {SCREEN_HEIGHT, SCREEN_WIDTH, StatusBarHeight} from '../../utils'; @@ -17,12 +17,9 @@ import {SCREEN_HEIGHT, SCREEN_WIDTH, StatusBarHeight} from '../../utils'; /** * Individual moment view opened when user clicks on a moment tile */ -type IndividualMomentRouteProp = RouteProp< - ProfileStackParams, - 'IndividualMoment' ->; +type IndividualMomentRouteProp = RouteProp<MainStackParams, 'IndividualMoment'>; type IndividualMomentNavigationProp = StackNavigationProp< - ProfileStackParams, + MainStackParams, 'IndividualMoment' >; interface IndividualMomentProps { diff --git a/src/screens/profile/MomentCommentsScreen.tsx b/src/screens/profile/MomentCommentsScreen.tsx index 9a841d10..ec193db5 100644 --- a/src/screens/profile/MomentCommentsScreen.tsx +++ b/src/screens/profile/MomentCommentsScreen.tsx @@ -28,7 +28,7 @@ interface MomentCommentsScreenProps { const MomentCommentsScreen: React.FC<MomentCommentsScreenProps> = ({route}) => { const navigation = useNavigation(); - const {moment_id, screenType} = route.params; + const {moment_id, screenType, comment_id} = route.params; //Receives comment length from child CommentsContainer const [commentsLength, setCommentsLength] = useState<number>(0); @@ -55,6 +55,7 @@ const MomentCommentsScreen: React.FC<MomentCommentsScreenProps> = ({route}) => { <View style={styles.body}> <CommentsContainer objectId={moment_id} + commentId={comment_id} screenType={screenType} setCommentsLength={setCommentsLength} newCommentsAvailable={newCommentsAvailable} @@ -109,7 +110,7 @@ const styles = StyleSheet.create({ fontWeight: '400', }, body: { - width: SCREEN_WIDTH, + width: SCREEN_WIDTH * 0.9, height: SCREEN_HEIGHT * 0.8, paddingTop: '3%', }, diff --git a/src/services/CommonService.ts b/src/services/CommonService.ts index 4f9fb47a..dfbbf70e 100644 --- a/src/services/CommonService.ts +++ b/src/services/CommonService.ts @@ -6,7 +6,7 @@ export const loadImageFromURL = async (url: string) => { return undefined; } const response = await RNFetchBlob.config({ - fileCache: true, + fileCache: false, appendExt: 'jpg', }).fetch('GET', url); const status = response.info().status; diff --git a/src/services/MomentService.ts b/src/services/MomentService.ts index 0110a0d6..2354d18e 100644 --- a/src/services/MomentService.ts +++ b/src/services/MomentService.ts @@ -100,7 +100,7 @@ export const loadMomentThumbnail = async (momentId: string) => { try { const token = await AsyncStorage.getItem('token'); const response = await RNFetchBlob.config({ - fileCache: true, + fileCache: false, appendExt: 'jpg', }).fetch('GET', MOMENT_THUMBNAIL_ENDPOINT + `${momentId}/`, { Authorization: 'Token ' + token, diff --git a/src/store/actions/user.ts b/src/store/actions/user.ts index 0b1ea789..5f49a103 100644 --- a/src/store/actions/user.ts +++ b/src/store/actions/user.ts @@ -1,3 +1,4 @@ +import { CommentThreadType } from './../../types/types'; import {RootState} from '../rootReducer'; import {UserType} from '../../types/types'; import {loadProfileInfo, loadAvatar, loadCover} from '../../services'; @@ -9,6 +10,7 @@ import { profileCompletionStageUpdated, setIsOnboardedUser, setNewNotificationReceived, + setReplyPosted, } from '../reducers'; import {getTokenOrLogout} from '../../utils'; @@ -111,6 +113,21 @@ export const updateNewNotificationReceived = ( } }; +export const updateReplyPosted = ( + replyPosted: CommentThreadType | undefined, +): ThunkAction<Promise<void>, RootState, unknown, Action<string>> => async ( + dispatch, +) => { + try { + dispatch({ + type: setReplyPosted.type, + payload: {replyPosted}, + }); + } catch (error) { + console.log(error); + } +}; + export const logout = (): ThunkAction< Promise<void>, RootState, diff --git a/src/store/initialStates.ts b/src/store/initialStates.ts index 2a5b76db..8d137a5d 100644 --- a/src/store/initialStates.ts +++ b/src/store/initialStates.ts @@ -1,3 +1,4 @@ +import {CommentThreadType} from './../types/types'; import { ExploreSectionType, MomentType, @@ -44,6 +45,7 @@ export const NO_USER_DATA = { cover: <string | null>'', isOnboardedUser: false, newNotificationReceived: false, + replyPosted: <CommentThreadType | undefined>undefined, }; export const NO_FRIENDS_DATA = { diff --git a/src/store/reducers/userReducer.ts b/src/store/reducers/userReducer.ts index ce497677..1e575339 100644 --- a/src/store/reducers/userReducer.ts +++ b/src/store/reducers/userReducer.ts @@ -53,6 +53,10 @@ const userDataSlice = createSlice({ setNewNotificationReceived: (state, action) => { state.newNotificationReceived = action.payload.newNotificationReceived; }, + + setReplyPosted: (state, action) => { + state.replyPosted = action.payload.replyPosted; + }, }, }); @@ -63,5 +67,6 @@ export const { profileCompletionStageUpdated, setIsOnboardedUser, setNewNotificationReceived, + setReplyPosted, } = userDataSlice.actions; export const userDataReducer = userDataSlice.reducer; diff --git a/src/types/types.ts b/src/types/types.ts index 1d800423..f1ba12f4 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -88,13 +88,20 @@ export interface MomentType { thumbnail_url: string; } -export interface CommentType { +export interface CommentBaseType { comment_id: string; comment: string; date_created: string; + commenter: ProfilePreviewType; +} + +export interface CommentType extends CommentBaseType { moment_id: string; replies_count: number; - commenter: ProfilePreviewType; +} + +export interface CommentThreadType extends CommentBaseType { + parent_comment: CommentType; } export type PreviewType = @@ -158,6 +165,7 @@ export enum CategorySelectionScreenType { export enum BackgroundGradientType { Light, Dark, + Notification } /** @@ -172,10 +180,25 @@ export type TaggPopupType = { export type NotificationType = { actor: ProfilePreviewType; verbage: string; - notification_type: 'DFT' | 'FRD_REQ' | 'FRD_ACPT' | 'FRD_DEC' | 'CMT'; - notification_object: CommentType | undefined; + notification_type: TypeOfNotification; + notification_object: CommentType | CommentThreadType | MomentType | undefined; timestamp: string; unread: boolean; }; export type TypeOfComment = 'Comment' | 'Thread'; +export type TypeOfNotification = + // notification_object is undefined + | 'DFT' + // notification_object is undefined + | 'FRD_REQ' + // notification_object is undefined + | 'FRD_ACPT' + // notification_object is undefined + | 'FRD_DEC' + // notification_object is CommentType || CommentThreadType + | 'CMT' + // notification_object is MomentType + | 'MOM_3+' + // notification_object is MomentType + | 'MOM_FRIEND'; |