diff options
Diffstat (limited to 'src/components')
| -rw-r--r-- | src/components/comments/AddComment.tsx | 67 | ||||
| -rw-r--r-- | src/components/comments/CommentTile.tsx | 261 | ||||
| -rw-r--r-- | src/components/comments/CommentsContainer.tsx | 171 | ||||
| -rw-r--r-- | src/components/common/TaggDatePicker.tsx | 2 | ||||
| -rw-r--r-- | src/components/common/TaggSquareButton.tsx | 79 | ||||
| -rw-r--r-- | src/components/common/index.ts | 1 | ||||
| -rw-r--r-- | src/components/moments/MomentPostContent.tsx | 8 | ||||
| -rw-r--r-- | src/components/moments/MomentTile.tsx | 3 | ||||
| -rw-r--r-- | src/components/notifications/Notification.tsx | 210 | ||||
| -rw-r--r-- | src/components/onboarding/BirthDatePicker.tsx | 2 | ||||
| -rw-r--r-- | src/components/profile/FriendsCount.tsx | 6 | ||||
| -rw-r--r-- | src/components/profile/ProfileBody.tsx | 16 | ||||
| -rw-r--r-- | src/components/profile/ProfilePreview.tsx | 42 | ||||
| -rw-r--r-- | src/components/search/Explore.tsx | 7 | ||||
| -rw-r--r-- | src/components/search/ExploreSection.tsx | 15 | ||||
| -rw-r--r-- | src/components/search/ExploreSectionUser.tsx | 18 |
16 files changed, 738 insertions, 170 deletions
diff --git a/src/components/comments/AddComment.tsx b/src/components/comments/AddComment.tsx index 24b3473c..56011f05 100644 --- a/src/components/comments/AddComment.tsx +++ b/src/components/comments/AddComment.tsx @@ -1,4 +1,4 @@ -import React, {useEffect} from 'react'; +import React, {useEffect, useRef} from 'react'; import { Image, Keyboard, @@ -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 {postMomentComment} from '../../services'; +import {postComment} from '../../services'; +import {updateReplyPosted} from '../../store/actions'; import {RootState} from '../../store/rootreducer'; import {SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils'; @@ -23,30 +24,48 @@ import {SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils'; export interface AddCommentProps { setNewCommentsAvailable: Function; - moment_id: string; + objectId: string; + placeholderText: string; + isCommentInFocus: boolean; } const AddComment: React.FC<AddCommentProps> = ({ setNewCommentsAvailable, - moment_id, + objectId, + placeholderText, + isCommentInFocus, }) => { const [comment, setComment] = React.useState(''); const [keyboardVisible, setKeyboardVisible] = React.useState(false); - const { - avatar, - user: {userId}, - } = useSelector((state: RootState) => state.user); + const {avatar} = useSelector((state: RootState) => state.user); + const dispatch = useDispatch(); - const postComment = async () => { - const postedComment = await postMomentComment( - userId, - comment.trim(), - moment_id, + const addComment = async () => { + const trimmed = comment.trim(); + if (trimmed === '') { + return; + } + const postedComment = await postComment( + trimmed, + objectId, + isCommentInFocus, ); 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); } }; @@ -63,6 +82,15 @@ const AddComment: React.FC<AddCommentProps> = ({ return () => Keyboard.removeListener('keyboardWillHide', hideKeyboard); }, []); + const ref = useRef<TextInput>(null); + + //If a comment is in Focus, bring the keyboard up so user is able to type in a reply + useEffect(() => { + if (isCommentInFocus) { + ref.current?.focus(); + } + }, [isCommentInFocus]); + return ( <KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} @@ -70,7 +98,7 @@ const AddComment: React.FC<AddCommentProps> = ({ <View style={[ styles.container, - keyboardVisible ? {backgroundColor: '#fff'} : {}, + keyboardVisible ? styles.whiteBackround : {}, ]}> <View style={styles.textContainer}> <Image @@ -83,15 +111,16 @@ const AddComment: React.FC<AddCommentProps> = ({ /> <TextInput style={styles.text} - placeholder="Add a comment..." + placeholder={placeholderText} placeholderTextColor="grey" onChangeText={setComment} value={comment} autoCorrect={false} multiline={true} + ref={ref} /> <View style={styles.submitButton}> - <TouchableOpacity style={styles.submitButton} onPress={postComment}> + <TouchableOpacity style={styles.submitButton} onPress={addComment}> <UpArrowIcon width={35} height={35} color={'white'} /> </TouchableOpacity> </View> @@ -100,6 +129,7 @@ const AddComment: React.FC<AddCommentProps> = ({ </KeyboardAvoidingView> ); }; + const styles = StyleSheet.create({ container: { backgroundColor: '#f7f7f7', @@ -140,6 +170,9 @@ const styles = StyleSheet.create({ marginVertical: '2%', alignSelf: 'flex-end', }, + whiteBackround: { + backgroundColor: '#fff', + }, }); export default AddComment; diff --git a/src/components/comments/CommentTile.tsx b/src/components/comments/CommentTile.tsx index 47f25a53..be113523 100644 --- a/src/components/comments/CommentTile.tsx +++ b/src/components/comments/CommentTile.tsx @@ -1,10 +1,21 @@ -import React from 'react'; +/* eslint-disable radix */ +import React, {Fragment, useEffect, useRef, useState} from 'react'; import {Text, View} from 'react-native-animatable'; import {ProfilePreview} from '../profile'; -import {CommentType, ScreenType} from '../../types'; -import {StyleSheet} from 'react-native'; -import {getTimePosted} from '../../utils'; +import {CommentType, ScreenType, TypeOfComment} from '../../types'; +import {Alert, Animated, StyleSheet} from 'react-native'; import ClockIcon from '../../assets/icons/clock-icon-01.svg'; +import {TAGG_LIGHT_BLUE} from '../../constants'; +import {RectButton, TouchableOpacity} from 'react-native-gesture-handler'; +import {getTimePosted, normalize, SCREEN_WIDTH} from '../../utils'; +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, 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. @@ -13,54 +24,205 @@ import ClockIcon from '../../assets/icons/clock-icon-01.svg'; interface CommentTileProps { comment_object: CommentType; screenType: ScreenType; + typeOfComment: TypeOfComment; + setCommentObjectInFocus?: (comment: CommentType | undefined) => void; + newCommentsAvailable: boolean; + setNewCommentsAvailable: (available: boolean) => void; + canDelete: boolean; } const CommentTile: React.FC<CommentTileProps> = ({ comment_object, screenType, + typeOfComment, + setCommentObjectInFocus, + newCommentsAvailable, + setNewCommentsAvailable, + canDelete, }) => { 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. + */ + useEffect(() => { + if (newCommentsAvailable) { + setNewThreadAvailable(true); + } + }, [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 + */ + 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); + }; + + /** + * Method to compute text to be shown for replies button + */ + const getRepliesText = () => + showReplies + ? 'Hide' + : comment_object.replies_count > 0 + ? `Replies (${comment_object.replies_count})` + : 'Replies'; + + const renderRightAction = (text: string, color: string, progress) => { + const pressHandler = async () => { + swipeRef.current?.close(); + const success = await deleteComment(comment_object.comment_id, isThread); + if (success) { + setNewCommentsAvailable(true); + } else { + Alert.alert(ERROR_FAILED_TO_DELETE_COMMENT); + } + }; + return ( + <Animated.View> + <RectButton + style={[styles.rightAction, {backgroundColor: color}]} + onPress={pressHandler}> + <Trash width={normalize(25)} height={normalize(25)} color={'white'} /> + <Text style={styles.actionText}>{text}</Text> + </RectButton> + </Animated.View> + ); + }; + + const renderRightActions = (progress: Animated.AnimatedInterpolation) => + canDelete ? ( + <View style={styles.swipeActions}> + {renderRightAction('Delete', '#c42634', progress)} + </View> + ) : ( + <Fragment /> + ); + return ( - <View style={styles.container}> - <ProfilePreview - profilePreview={{ - id: comment_object.commenter.id, - username: comment_object.commenter.username, - first_name: comment_object.commenter.first_name, - last_name: comment_object.commenter.last_name, - }} - previewType={'Comment'} - screenType={screenType} - /> - <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> + <Swipeable + ref={swipeRef} + renderRightActions={renderRightActions} + rightThreshold={40} + friction={2} + containerStyle={styles.swipableContainer}> + <View + style={[styles.container, isThread ? styles.moreMarginWithThread : {}]}> + <ProfilePreview + profilePreview={comment_object.commenter} + previewType={'Comment'} + screenType={screenType} + /> + <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} /> + </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> - </View> + + {/*** Show replies if toggle state is true */} + {showReplies && ( + <View> + <CommentsContainer + objectId={comment_object.comment_id} + screenType={screenType} + setNewCommentsAvailable={setNewThreadAvailable} + newCommentsAvailable={newThreadAvailable} + typeOfComment={'Thread'} + commentId={replyPosted?.comment_id} + /> + </View> + )} + </Swipeable> ); }; const styles = StyleSheet.create({ container: { - marginLeft: '3%', - marginRight: '3%', borderBottomWidth: 1, borderColor: 'lightgray', - marginBottom: '3%', + backgroundColor: 'white', + flexDirection: 'column', + flex: 1, + paddingTop: '3%', + paddingBottom: '5%', + marginLeft: '7%', + }, + swipeActions: { + flexDirection: 'row', + }, + moreMarginWithThread: { + marginLeft: '14%', }, body: { marginLeft: 56, }, comment: { - position: 'relative', - top: -5, marginBottom: '2%', + marginRight: '2%', }, date_time: { color: 'gray', + fontSize: normalize(12), }, clockIcon: { width: 12, @@ -69,7 +231,50 @@ const styles = StyleSheet.create({ }, clockIconAndTime: { flexDirection: 'row', - marginBottom: '3%', + marginTop: '3%', + }, + flexer: { + flex: 1, + }, + repliesTextAndIconContainer: { + flexDirection: 'row', + alignItems: 'center', + marginTop: '5%', + marginLeft: 56, + }, + repliesText: { + color: TAGG_LIGHT_BLUE, + fontWeight: '500', + fontSize: normalize(12), + marginRight: '1%', + }, + repliesBody: { + width: SCREEN_WIDTH, + }, + repliesDownArrow: { + transform: [{rotate: '270deg'}], + marginTop: '1%', + }, + repliesUpArrow: { + transform: [{rotate: '90deg'}], + marginTop: '1%', + }, + actionText: { + color: 'white', + fontSize: normalize(12), + fontWeight: '500', + backgroundColor: 'transparent', + paddingHorizontal: '5%', + marginTop: '5%', + }, + rightAction: { + alignItems: 'center', + flex: 1, + justifyContent: 'center', + flexDirection: 'column', + }, + swipableContainer: { + backgroundColor: 'white', }, }); diff --git a/src/components/comments/CommentsContainer.tsx b/src/components/comments/CommentsContainer.tsx new file mode 100644 index 00000000..c72da2b7 --- /dev/null +++ b/src/components/comments/CommentsContainer.tsx @@ -0,0 +1,171 @@ +import React, {useEffect, useRef, useState} from 'react'; +import {StyleSheet} from 'react-native'; +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; + typeOfComment: TypeOfComment; + setCommentObjectInFocus?: (comment: CommentType | undefined) => void; + commentObjectInFocus?: CommentType; +}; + +/** + * Comments Container to be used for both comments and replies + */ + +const CommentsContainer: React.FC<CommentsContainerProps> = ({ + screenType, + objectId, + setCommentsLength, + newCommentsAvailable, + setNewCommentsAvailable, + typeOfComment, + setCommentObjectInFocus, + commentObjectInFocus, + commentId, +}) => { + const {username: loggedInUsername} = useSelector( + (state: RootState) => state.user.user, + ); + const [commentsList, setCommentsList] = useState<CommentType[]>([]); + const dispatch = useDispatch(); + const ref = useRef<FlatList<CommentType>>(null); + + useEffect(() => { + const loadComments = async () => { + 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(); + } + return () => { + subscribedToLoadComments = false; + }; + }, [ + dispatch, + objectId, + newCommentsAvailable, + setNewCommentsAvailable, + setCommentsLength, + typeOfComment, + ]); + + // 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 ( + <FlatList + data={commentsList} + ref={ref} + 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 + /> + ); +}; + +const styles = StyleSheet.create({ + scrollView: {}, + scrollViewContent: { + justifyContent: 'center', + }, +}); + +export default CommentsContainer; diff --git a/src/components/common/TaggDatePicker.tsx b/src/components/common/TaggDatePicker.tsx index 059bf620..f929b41d 100644 --- a/src/components/common/TaggDatePicker.tsx +++ b/src/components/common/TaggDatePicker.tsx @@ -12,7 +12,7 @@ interface TaggDatePickerProps { const TaggDatePicker: React.FC<TaggDatePickerProps> = (props) => { const [date, setDate] = useState( props.date - ? new Date(moment(props.date).add(1, 'day').format('YYYY-MM-DD')) + ? new Date(moment(props.date).add(1, 'day').format('MM-DD-YYYY')) : undefined, ); return ( 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 61c7fa26..a5718c1e 100644 --- a/src/components/common/index.ts +++ b/src/components/common/index.ts @@ -20,3 +20,4 @@ export {default as GenericMoreInfoDrawer} from './GenericMoreInfoDrawer'; export {default as TaggPopUp} from './TaggPopup'; export {default as TaggPrompt} from './TaggPrompt'; export {default as AcceptDeclineButtons} from './AcceptDeclineButtons'; +export {default as TaggSquareButton} from './TaggSquareButton'; diff --git a/src/components/moments/MomentPostContent.tsx b/src/components/moments/MomentPostContent.tsx index 508b6d9f..d68ceaa3 100644 --- a/src/components/moments/MomentPostContent.tsx +++ b/src/components/moments/MomentPostContent.tsx @@ -1,6 +1,6 @@ import React, {useEffect} from 'react'; import {Image, StyleSheet, Text, View, ViewProps} from 'react-native'; -import {getMomentCommentsCount} from '../../services'; +import {getCommentsCount} from '../../services'; import {ScreenType} from '../../types'; import {getTimePosted, SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils'; import {CommentsCount} from '../comments'; @@ -24,8 +24,12 @@ const MomentPostContent: React.FC<MomentPostContentProps> = ({ const [comments_count, setCommentsCount] = React.useState(''); useEffect(() => { + const fetchCommentsCount = async () => { + const count = await getCommentsCount(momentId, false); + setCommentsCount(count); + }; setElapsedTime(getTimePosted(dateTime)); - getMomentCommentsCount(momentId, setCommentsCount); + fetchCommentsCount(); }, [dateTime, momentId]); return ( diff --git a/src/components/moments/MomentTile.tsx b/src/components/moments/MomentTile.tsx index 16e91c82..69701192 100644 --- a/src/components/moments/MomentTile.tsx +++ b/src/components/moments/MomentTile.tsx @@ -15,7 +15,6 @@ const MomentTile: React.FC<MomentTileProps> = ({ }) => { const navigation = useNavigation(); - const {path_hash} = moment; return ( <TouchableOpacity onPress={() => { @@ -26,7 +25,7 @@ const MomentTile: React.FC<MomentTileProps> = ({ }); }}> <View style={styles.image}> - <Image style={styles.image} source={{uri: path_hash}} /> + <Image style={styles.image} source={{uri: moment.thumbnail_url}} /> </View> </TouchableOpacity> ); diff --git a/src/components/notifications/Notification.tsx b/src/components/notifications/Notification.tsx index e6d16f82..951a5bf6 100644 --- a/src/components/notifications/Notification.tsx +++ b/src/components/notifications/Notification.tsx @@ -1,25 +1,32 @@ import {useNavigation} from '@react-navigation/native'; import React, {useEffect, useState} from 'react'; -import {Image, StyleSheet, Text, View} from 'react-native'; -import {Button} from 'react-native-elements'; +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 {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 {acceptFriendRequest} from '../../store/actions'; -import {NotificationType, ProfilePreviewType, ScreenType, MomentType} from '../../types'; +import {RootState} from '../../store/rootReducer'; +import {MomentType, NotificationType, ScreenType} from '../../types'; import { fetchUserX, + getTokenOrLogout, SCREEN_HEIGHT, - SCREEN_WIDTH, userXInStore, } from '../../utils'; import AcceptDeclineButtons from '../common/AcceptDeclineButtons'; -import {loadAvatar, loadMomentThumbnail} from '../../services'; - interface NotificationProps { item: NotificationType; @@ -30,7 +37,7 @@ interface NotificationProps { const Notification: React.FC<NotificationProps> = (props) => { const { item: { - actor: {id, username, first_name, last_name}, + actor: {id, username, first_name, last_name, thumbnail_url}, verbage, notification_type, notification_object, @@ -44,22 +51,30 @@ const Notification: React.FC<NotificationProps> = (props) => { const state: RootState = useStore().getState(); const dispatch = useDispatch(); - const [avatarURI, setAvatarURI] = useState<string | undefined>(undefined); + 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(() => { - let mounted = true; - const loadAvatarImage = async (user_id: string) => { - const response = await loadAvatar(user_id, true); - if (mounted) { - setAvatarURI(response); + (async () => { + const response = await loadImageFromURL(thumbnail_url); + if (response) { + setAvatar(response); + } else { + setAvatar(undefined); } - }; - loadAvatarImage(id); + })(); + }, []); + + useEffect(() => { + if (onTapLoadProfile) { + fetchUserX(dispatch, {userId: id, username: username}, screenType); + } return () => { - mounted = false; + setOnTapLoadProfile(false); }; - }, [id]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [onTapLoadProfile]); useEffect(() => { let mounted = true; @@ -67,10 +82,22 @@ const Notification: React.FC<NotificationProps> = (props) => { 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; }; @@ -94,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()); }; @@ -128,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={ - avatarURI - ? {uri: avatarURI, 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 && ( - <Image style={styles.moment} source={{uri: momentURI}} /> )} - </TouchableWithoutFeedback> - </> + {(notification_type === 'CMT' || + notification_type === 'MOM_3+' || + notification_type === 'MOM_FRIEND') && + notification_object && ( + <Image style={styles.moment} source={{uri: momentURI}} /> + )} + </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/onboarding/BirthDatePicker.tsx b/src/components/onboarding/BirthDatePicker.tsx index 0fc597c3..6bef5798 100644 --- a/src/components/onboarding/BirthDatePicker.tsx +++ b/src/components/onboarding/BirthDatePicker.tsx @@ -45,7 +45,7 @@ const BirthDatePicker = React.forwardRef( ref={ref} {...props}> {(updated || props.showPresetdate) && date - ? moment(date).format('YYYY-MM-DD') + ? moment(date).format('MM-DD-YYYY') : 'Date of Birth'} </Text> </TouchableOpacity> diff --git a/src/components/profile/FriendsCount.tsx b/src/components/profile/FriendsCount.tsx index 9647710e..851dbc3b 100644 --- a/src/components/profile/FriendsCount.tsx +++ b/src/components/profile/FriendsCount.tsx @@ -17,10 +17,10 @@ const FriendsCount: React.FC<FriendsCountProps> = ({ userXId, screenType, }) => { - const count = (userXId + const {friends} = userXId ? useSelector((state: RootState) => state.userX[screenType][userXId]) - : useSelector((state: RootState) => state.friends) - )?.friends.length; + : useSelector((state: RootState) => state.friends); + const count = friends ? friends.length : 0; const displayedCount: string = count < 5e3 diff --git a/src/components/profile/ProfileBody.tsx b/src/components/profile/ProfileBody.tsx index d10e2e15..f2d75519 100644 --- a/src/components/profile/ProfileBody.tsx +++ b/src/components/profile/ProfileBody.tsx @@ -105,8 +105,8 @@ const ProfileBody: React.FC<ProfileBodyProps> = ({ {friendship_status === 'friends' && ( <Button title={'Unfriend'} - buttonStyle={styles.button} - titleStyle={styles.buttonTitle} + buttonStyle={styles.requestedButton} + titleStyle={styles.requestedButtonTitle} onPress={handleFriendUnfriend} // unfriend, no record status /> )} @@ -176,10 +176,10 @@ const styles = StyleSheet.create({ justifyContent: 'center', alignItems: 'center', width: SCREEN_WIDTH * 0.4, - height: SCREEN_WIDTH * 0.09, + height: SCREEN_WIDTH * 0.075, borderColor: TAGG_LIGHT_BLUE, - borderWidth: 3, - borderRadius: 5, + borderWidth: 2, + borderRadius: 3, marginRight: '2%', padding: 0, backgroundColor: 'transparent', @@ -200,9 +200,11 @@ const styles = StyleSheet.create({ justifyContent: 'center', alignItems: 'center', width: SCREEN_WIDTH * 0.4, - height: SCREEN_WIDTH * 0.09, + height: SCREEN_WIDTH * 0.075, padding: 0, - borderRadius: 5, + borderWidth: 2, + borderColor: TAGG_LIGHT_BLUE, + borderRadius: 3, marginRight: '2%', backgroundColor: TAGG_LIGHT_BLUE, }, diff --git a/src/components/profile/ProfilePreview.tsx b/src/components/profile/ProfilePreview.tsx index 389ca367..38defb8d 100644 --- a/src/components/profile/ProfilePreview.tsx +++ b/src/components/profile/ProfilePreview.tsx @@ -12,21 +12,11 @@ import { } from 'react-native'; import {useDispatch, useSelector, useStore} from 'react-redux'; import {ERROR_UNABLE_TO_VIEW_PROFILE} from '../../constants/strings'; -import {loadAvatar} from '../../services'; +import {loadImageFromURL} from '../../services'; import {RootState} from '../../store/rootreducer'; -import { - PreviewType, - ProfilePreviewType, - ScreenType, - UserType, -} from '../../types'; +import {PreviewType, ProfilePreviewType, ScreenType} from '../../types'; import {checkIfUserIsBlocked, fetchUserX, userXInStore} from '../../utils'; -const NO_USER: UserType = { - userId: '', - username: '', -}; - /** * This component returns user's profile picture friended by username as a touchable component. * What happens when someone clicks on this component is partly decided by the prop isComment. @@ -43,28 +33,23 @@ interface ProfilePreviewProps extends ViewProps { } const ProfilePreview: React.FC<ProfilePreviewProps> = ({ - profilePreview: {username, first_name, last_name, id}, + profilePreview: {username, first_name, last_name, id, thumbnail_url}, previewType, screenType, }) => { const navigation = useNavigation(); const {user: loggedInUser} = useSelector((state: RootState) => state.user); - const [avatarURI, setAvatarURI] = useState<string | null>(null); - const [user, setUser] = useState<UserType>(NO_USER); + const [avatar, setAvatar] = useState<string | null>(null); const dispatch = useDispatch(); + useEffect(() => { - let mounted = true; - const loadAvatarImage = async () => { - const response = await loadAvatar(id, true); - if (mounted) { - setAvatarURI(response); + (async () => { + const response = await loadImageFromURL(thumbnail_url); + if (response) { + setAvatar(response); } - }; - loadAvatarImage(); - return () => { - mounted = false; - }; - }, [id]); + })(); + }, []); /** * Adds a searched user to the recently searched cache if they're tapped on. @@ -80,6 +65,7 @@ const ProfilePreview: React.FC<ProfilePreviewProps> = ({ username, first_name, last_name, + thumbnail_url, }; try { @@ -211,8 +197,8 @@ const ProfilePreview: React.FC<ProfilePreviewProps> = ({ <Image style={avatarStyle} source={ - avatarURI - ? {uri: avatarURI} + avatar + ? {uri: avatar} : require('../../assets/images/avatar-placeholder.png') } /> diff --git a/src/components/search/Explore.tsx b/src/components/search/Explore.tsx index 4a71249b..2a3bc749 100644 --- a/src/components/search/Explore.tsx +++ b/src/components/search/Explore.tsx @@ -12,9 +12,10 @@ const Explore: React.FC = () => { return ( <View style={styles.container}> <Text style={styles.header}>Search Profiles</Text> - {EXPLORE_SECTION_TITLES.map((title: ExploreSectionType) => ( - <ExploreSection key={title} title={title} users={explores[title]} /> - ))} + {explores && + EXPLORE_SECTION_TITLES.map((title: ExploreSectionType) => ( + <ExploreSection key={title} title={title} users={explores[title]} /> + ))} </View> ); }; diff --git a/src/components/search/ExploreSection.tsx b/src/components/search/ExploreSection.tsx index 17079e77..025c8c3c 100644 --- a/src/components/search/ExploreSection.tsx +++ b/src/components/search/ExploreSection.tsx @@ -1,5 +1,5 @@ import React, {Fragment} from 'react'; -import {ScrollView, StyleSheet, Text, View} from 'react-native'; +import {FlatList, StyleSheet, Text, View} from 'react-native'; import {ProfilePreviewType} from '../../types'; import {normalize} from '../../utils'; import ExploreSectionUser from './ExploreSectionUser'; @@ -17,12 +17,15 @@ const ExploreSection: React.FC<ExploreSectionProps> = ({title, users}) => { return users.length !== 0 ? ( <View style={styles.container}> <Text style={styles.header}>{title}</Text> - <ScrollView horizontal showsHorizontalScrollIndicator={false}> - <View style={styles.padding} /> - {users.map((user) => ( + <FlatList + data={users} + ListHeaderComponent={<View style={styles.padding} />} + renderItem={({item: user}: {item: ProfilePreviewType}) => ( <ExploreSectionUser key={user.id} user={user} style={styles.user} /> - ))} - </ScrollView> + )} + showsHorizontalScrollIndicator={false} + horizontal + /> </View> ) : ( <Fragment /> diff --git a/src/components/search/ExploreSectionUser.tsx b/src/components/search/ExploreSectionUser.tsx index 68e077e3..b0cfe5c6 100644 --- a/src/components/search/ExploreSectionUser.tsx +++ b/src/components/search/ExploreSectionUser.tsx @@ -9,7 +9,7 @@ import { } from 'react-native'; import LinearGradient from 'react-native-linear-gradient'; import {useDispatch, useSelector, useStore} from 'react-redux'; -import {loadAvatar} from '../../services'; +import {loadImageFromURL} from '../../services'; import {RootState} from '../../store/rootReducer'; import {ProfilePreviewType, ScreenType} from '../../types'; import {fetchUserX, normalize, userXInStore} from '../../utils'; @@ -36,18 +36,13 @@ const ExploreSectionUser: React.FC<ExploreSectionUserProps> = ({ const dispatch = useDispatch(); useEffect(() => { - let mounted = true; - const loadAvatarImage = async () => { - const response = await loadAvatar(id, true); - if (mounted) { + (async () => { + const response = await loadImageFromURL(user.thumbnail_url); + if (response) { setAvatar(response); } - }; - loadAvatarImage(); - return () => { - mounted = false; - }; - }, [user]); + })(); + }, []); const handlePress = async () => { if (!userXInStore(state, screenType, user.id)) { @@ -63,7 +58,6 @@ const ExploreSectionUser: React.FC<ExploreSectionUserProps> = ({ screenType, }); }; - return ( <TouchableOpacity style={[styles.container, style]} onPress={handlePress}> <LinearGradient |
