diff options
Diffstat (limited to 'src')
42 files changed, 1044 insertions, 456 deletions
diff --git a/src/App.tsx b/src/App.tsx index 18fadf64..ea3617dc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,17 +1,11 @@ -import React, {useEffect} from 'react'; import {NavigationContainer} from '@react-navigation/native'; -import Routes from './routes'; +import React from 'react'; import {Provider} from 'react-redux'; -import store from './store/configureStore'; -import {fcmService} from './services/FCMService'; import {navigationRef} from './RootNavigation'; +import Routes from './routes'; +import store from './store/configureStore'; const App = () => { - useEffect(() => { - fcmService.setUpPushNotifications(); - // TODO: If permissions are not there, deactivateFcmService - }); - return ( /** * This is the provider from the redux store, it acts as the root provider for our application diff --git a/src/assets/icons/back-arrow-colored.svg b/src/assets/icons/back-arrow-colored.svg new file mode 100644 index 00000000..123426d7 --- /dev/null +++ b/src/assets/icons/back-arrow-colored.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 385.86 696.76"><defs><style>.cls-1{fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round;stroke-width:77.17px;}</style></defs><polyline class="cls-1" points="347.28 38.58 38.58 351.69 347.28 658.17"/></svg>
\ No newline at end of file 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/assets/ionicons/trash-outline.svg b/src/assets/ionicons/trash-outline.svg new file mode 100644 index 00000000..4920b56a --- /dev/null +++ b/src/assets/ionicons/trash-outline.svg @@ -0,0 +1 @@ +<svg xmlns='http://www.w3.org/2000/svg' class='ionicon' viewBox='0 0 512 512'><title>Trash</title><path d='M112 112l20 320c.95 18.49 14.4 32 32 32h184c17.67 0 30.87-13.51 32-32l20-320' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='32'/><path stroke='currentColor' stroke-linecap='round' stroke-miterlimit='10' stroke-width='32' d='M80 112h352'/><path d='M192 112V72h0a23.93 23.93 0 0124-24h80a23.93 23.93 0 0124 24h0v40M256 176v224M184 176l8 224M328 176l-8 224' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='32'/></svg>
\ No newline at end of file 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 diff --git a/src/constants/api.ts b/src/constants/api.ts index 701070eb..32631be0 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -30,6 +30,7 @@ export const MOMENT_CATEGORY_ENDPOINT: string = API_URL + 'moment-category/'; export const NOTIFICATIONS_ENDPOINT: string = API_URL + 'notifications/'; export const DISCOVER_ENDPOINT: string = API_URL + 'discover/'; export const WAITLIST_USER_ENDPOINT: string = API_URL + 'waitlist-user/'; +export const COMMENT_THREAD_ENDPOINT: string = API_URL + 'reply/'; // Register as FCM device export const FCM_ENDPOINT: string = API_URL + 'fcm/'; 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 a1793658..9680320a 100644 --- a/src/constants/strings.ts +++ b/src/constants/strings.ts @@ -1,18 +1,22 @@ + /* eslint-disable */ // Below is the regex to convert this into a csv for the Google Sheet // export const (.*) = .*?(['|"|`])(.*)\2; // replace with: $1\t$3 +export const ADD_COMMENT_TEXT = (username?: string) => username ? `Reply to ${username}` : 'Add a comment...' export const COMING_SOON_MSG = 'Creating more fun things for you, surprises coming soon 😉'; export const ERROR_AUTHENTICATION = 'An error occurred during authentication. Please login again!'; export const ERROR_CATEGORY_CREATION = 'There was a problem creating your categories. Please refresh and try again.'; 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'; export const ERROR_FAILED_LOGIN_INFO = 'Login failed, please try re-entering your login information'; export const ERROR_FAILED_TO_COMMENT = 'Unable to post comment, refresh and try again!'; +export const ERROR_FAILED_TO_DELETE_COMMENT = 'Unable to delete comment, refresh and try again!'; export const ERROR_INVALID_INVITATION_CODE = 'Invitation code invalid, try again or talk to the friend that sent it 😬'; export const ERROR_INVALID_LOGIN = 'Invalid login, Please login again'; export const ERROR_INVALID_PWD_CODE = 'Looks like you have entered the wrong code, please try again'; @@ -49,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/Routes.tsx b/src/routes/Routes.tsx index a14f1576..a5383a47 100644 --- a/src/routes/Routes.tsx +++ b/src/routes/Routes.tsx @@ -7,6 +7,7 @@ import {userLogin} from '../utils'; import SplashScreen from 'react-native-splash-screen'; import messaging from '@react-native-firebase/messaging'; import {updateNewNotificationReceived} from '../store/actions'; +import {fcmService} from '../services'; const Routes: React.FC = () => { const { @@ -39,6 +40,13 @@ const Routes: React.FC = () => { } }, [dispatch, userId]); + useEffect(() => { + if (userId) { + fcmService.setUpPushNotifications(); + fcmService.sendFcmTokenToServer(); + } + }); + return userId ? <NavigationBar /> : <Onboarding />; }; diff --git a/src/routes/main/MainStackNavigator.tsx b/src/routes/main/MainStackNavigator.tsx index e038d2c2..74993af9 100644 --- a/src/routes/main/MainStackNavigator.tsx +++ b/src/routes/main/MainStackNavigator.tsx @@ -40,6 +40,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/CategorySelection.tsx b/src/screens/onboarding/CategorySelection.tsx index a3acbbb7..94dd44b2 100644 --- a/src/screens/onboarding/CategorySelection.tsx +++ b/src/screens/onboarding/CategorySelection.tsx @@ -17,7 +17,7 @@ import {Background, MomentCategory} from '../../components'; import {MOMENT_CATEGORIES} from '../../constants'; import {ERROR_SOMETHING_WENT_WRONG} from '../../constants/strings'; import {OnboardingStackParams} from '../../routes'; -import {fcmService, postMomentCategories} from '../../services'; +import {postMomentCategories} from '../../services'; import { updateIsOnboardedUser, updateMomentCategories, @@ -169,7 +169,6 @@ const CategorySelection: React.FC<CategorySelectionProps> = ({ const token = await getTokenOrLogout(dispatch); await postMomentCategories(selectedCategories, token); userLogin(dispatch, {userId: userId, username: username}); - fcmService.sendFcmTokenToServer(); } else { dispatch( updateMomentCategories( 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 6b82b31c..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 { @@ -70,7 +67,7 @@ const IndividualMoment: React.FC<IndividualMomentProps> = ({ style={styles.postContent} momentId={item.moment_id} caption={item.caption} - pathHash={item.path_hash} + pathHash={item.moment_url} dateTime={item.date_created} screenType={screenType} /> diff --git a/src/screens/profile/MomentCommentsScreen.tsx b/src/screens/profile/MomentCommentsScreen.tsx index 2bceafc9..5c3b8579 100644 --- a/src/screens/profile/MomentCommentsScreen.tsx +++ b/src/screens/profile/MomentCommentsScreen.tsx @@ -1,19 +1,13 @@ import {RouteProp, useNavigation} from '@react-navigation/native'; -import React, {useEffect, useRef} from 'react'; -import { - ScrollView, - StyleSheet, - Text, - TouchableOpacity, - View, -} from 'react-native'; +import React, {useState} from 'react'; +import {StyleSheet, Text, TouchableOpacity, View} from 'react-native'; import {SafeAreaView} from 'react-native-safe-area-context'; -import {useDispatch} from 'react-redux'; -import {getMomentComments} from '../..//services'; import BackIcon from '../../assets/icons/back-arrow.svg'; -import {CommentTile, TabsGradient} from '../../components'; +import {TabsGradient} from '../../components'; import {AddComment} from '../../components/'; -import {ProfileStackParams} from '../../routes/main'; +import CommentsContainer from '../../components/comments/CommentsContainer'; +import {ADD_COMMENT_TEXT} from '../../constants/strings'; +import {MainStackParams} from '../../routes/main'; import {CommentType} from '../../types'; import {SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils'; @@ -24,7 +18,7 @@ import {SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils'; */ type MomentCommentsScreenRouteProps = RouteProp< - ProfileStackParams, + MainStackParams, 'MomentCommentsScreen' >; @@ -34,26 +28,16 @@ interface MomentCommentsScreenProps { const MomentCommentsScreen: React.FC<MomentCommentsScreenProps> = ({route}) => { const navigation = useNavigation(); - const {moment_id, screenType} = route.params; - const [commentsList, setCommentsList] = React.useState([]); + const {moment_id, screenType, comment_id} = route.params; + + //Receives comment length from child CommentsContainer + const [commentsLength, setCommentsLength] = useState<number>(0); const [newCommentsAvailable, setNewCommentsAvailable] = React.useState(true); - const dispatch = useDispatch(); - const ref = useRef<ScrollView>(null); - useEffect(() => { - const loadComments = async () => { - getMomentComments(moment_id, setCommentsList); - setNewCommentsAvailable(false); - }; - if (newCommentsAvailable) { - loadComments(); - setTimeout(() => { - ref.current?.scrollToEnd({ - animated: true, - }); - }, 500); - } - }, [dispatch, moment_id, newCommentsAvailable]); + //Keeps track of the current comments object in focus so that the application knows which comment to post a reply to + const [commentObjectInFocus, setCommentObjectInFocus] = useState< + CommentType | undefined + >(undefined); return ( <View style={styles.background}> @@ -66,27 +50,31 @@ const MomentCommentsScreen: React.FC<MomentCommentsScreenProps> = ({route}) => { }}> <BackIcon height={'100%'} width={'100%'} color={'white'} /> </TouchableOpacity> - <Text style={styles.headerText}> - {commentsList.length + ' Comments'} - </Text> + <Text style={styles.headerText}>{commentsLength + ' Comments'}</Text> </View> <View style={styles.body}> - <ScrollView - ref={ref} - style={styles.scrollView} - contentContainerStyle={styles.scrollViewContent}> - {commentsList && - commentsList.map((comment: CommentType) => ( - <CommentTile - key={comment.comment_id} - comment_object={comment} - screenType={screenType} - /> - ))} - </ScrollView> + <CommentsContainer + objectId={moment_id} + commentId={comment_id} + screenType={screenType} + setCommentsLength={setCommentsLength} + newCommentsAvailable={newCommentsAvailable} + setNewCommentsAvailable={setNewCommentsAvailable} + setCommentObjectInFocus={setCommentObjectInFocus} + commentObjectInFocus={commentObjectInFocus} + typeOfComment={'Comment'} + /> <AddComment + placeholderText={ + commentObjectInFocus + ? ADD_COMMENT_TEXT(commentObjectInFocus.commenter.username) + : ADD_COMMENT_TEXT() + } setNewCommentsAvailable={setNewCommentsAvailable} - moment_id={moment_id} + objectId={ + commentObjectInFocus ? commentObjectInFocus.comment_id : moment_id + } + isCommentInFocus={commentObjectInFocus ? true : false} /> </View> </SafeAreaView> @@ -120,7 +108,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/screens/search/SearchScreen.tsx b/src/screens/search/SearchScreen.tsx index 059bd968..f0be7c9e 100644 --- a/src/screens/search/SearchScreen.tsx +++ b/src/screens/search/SearchScreen.tsx @@ -2,7 +2,6 @@ import AsyncStorage from '@react-native-community/async-storage'; import {useFocusEffect} from '@react-navigation/native'; import React, {useCallback, useEffect, useState} from 'react'; import { - Dimensions, Keyboard, RefreshControl, ScrollView, @@ -37,9 +36,7 @@ const NO_USER: UserType = { */ const SearchScreen: React.FC = () => { - const {recentSearches, explores} = useSelector( - (state: RootState) => state.taggUsers, - ); + const {recentSearches} = useSelector((state: RootState) => state.taggUsers); const [query, setQuery] = useState<string>(''); const [results, setResults] = useState<Array<ProfilePreviewType>>([]); const [recents, setRecents] = useState<Array<ProfilePreviewType>>( @@ -47,7 +44,6 @@ const SearchScreen: React.FC = () => { ); const [searching, setSearching] = useState(false); const top = Animated.useValue(-SCREEN_HEIGHT); - const [user, setUser] = useState<UserType>(NO_USER); const [refreshing, setRefreshing] = useState<boolean>(false); const dispatch = useDispatch(); @@ -70,10 +66,6 @@ const SearchScreen: React.FC = () => { const loadResults = async (q: string) => { try { const token = await AsyncStorage.getItem('token'); - if (!token) { - setUser(NO_USER); - return; - } const response = await fetch(`${SEARCH_ENDPOINT}?query=${q}`, { method: 'GET', headers: { diff --git a/src/services/CommentService.ts b/src/services/CommentService.ts new file mode 100644 index 00000000..2faaa8db --- /dev/null +++ b/src/services/CommentService.ts @@ -0,0 +1,118 @@ +import AsyncStorage from '@react-native-community/async-storage'; +import {Alert} from 'react-native'; +import {COMMENTS_ENDPOINT, COMMENT_THREAD_ENDPOINT} from '../constants'; +import {ERROR_FAILED_TO_COMMENT} from '../constants/strings'; +import {CommentType} from '../types'; + +export const getComments = async ( + objectId: string, + fetchThreads: boolean, +): Promise<CommentType[]> => { + let comments: CommentType[] = []; + try { + const token = await AsyncStorage.getItem('token'); + const endpoint = fetchThreads + ? COMMENT_THREAD_ENDPOINT + '?comment_id=' + : COMMENTS_ENDPOINT + '?moment_id='; + const response = await fetch(endpoint + objectId, { + method: 'GET', + headers: { + Authorization: 'Token ' + token, + }, + }); + const status = response.status; + if (status === 200) { + comments = await response.json(); + } else { + console.log('Could not load comments'); + } + } catch (error) { + console.log('Could not load comments', error); + } + return comments; +}; + +export const postComment = async ( + comment: string, + objectId: string, + postThread: boolean, +) => { + try { + const token = await AsyncStorage.getItem('token'); + const request = new FormData(); + request.append('comment', comment); + if (postThread) { + request.append('comment_id', objectId); + } else { + request.append('moment_id', objectId); + } + const endpoint = postThread ? COMMENT_THREAD_ENDPOINT : COMMENTS_ENDPOINT; + const response = await fetch(endpoint, { + method: 'POST', + headers: { + Authorization: 'Token ' + token, + }, + body: request, + }); + if (response.status !== 200) { + throw 'server error'; + } + return await response.json(); + } catch (error) { + Alert.alert(ERROR_FAILED_TO_COMMENT); + return undefined; + } +}; + +//Get count of comments for a moment +export const getCommentsCount = async ( + objectId: string, + fetchThread: boolean, +): Promise<string> => { + let comments_count: string = ''; + try { + const token = await AsyncStorage.getItem('token'); + const endpoint = fetchThread ? COMMENT_THREAD_ENDPOINT : COMMENTS_ENDPOINT; + const response = await fetch(endpoint + `${objectId}/`, { + method: 'GET', + headers: { + Authorization: 'Token ' + token, + }, + }); + const status = response.status; + if (status === 200) { + const response_data = await response.json(); + comments_count = 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, + ); + } + return comments_count; +}; + +export const deleteComment = async (id: string, isThread: boolean) => { + try { + const token = await AsyncStorage.getItem('token'); + const url = isThread ? COMMENT_THREAD_ENDPOINT : COMMENTS_ENDPOINT; + const response = await fetch(url + `${id}/`, { + method: 'DELETE', + headers: { + Authorization: 'Token ' + token, + }, + }); + return response.status === 200; + } catch (error) { + console.log('Failed to delete a comment'); + console.log(error); + return false; + } +}; diff --git a/src/services/CommonService.ts b/src/services/CommonService.ts new file mode 100644 index 00000000..dfbbf70e --- /dev/null +++ b/src/services/CommonService.ts @@ -0,0 +1,22 @@ +import RNFetchBlob from 'rn-fetch-blob'; + +export const loadImageFromURL = async (url: string) => { + try { + if (!url) { + return undefined; + } + const response = await RNFetchBlob.config({ + fileCache: false, + appendExt: 'jpg', + }).fetch('GET', url); + const status = response.info().status; + if (status === 200) { + return response.path(); + } else { + return undefined; + } + } catch (error) { + console.log(error); + return undefined; + } +}; diff --git a/src/services/ExploreServices.ts b/src/services/ExploreService.ts index ca4f1b69..980258be 100644 --- a/src/services/ExploreServices.ts +++ b/src/services/ExploreService.ts @@ -1,5 +1,4 @@ import AsyncStorage from '@react-native-community/async-storage'; -import {getDeviceToken} from 'react-native-device-info'; import {ALL_USERS_ENDPOINT, DISCOVER_ENDPOINT} from '../constants'; import {EMPTY_EXPLORE_SECTIONS} from '../store/initialStates'; import {ExploreSectionType, ProfilePreviewType} from '../types'; diff --git a/src/services/MomentServices.ts b/src/services/MomentService.ts index 7bad6d4c..2354d18e 100644 --- a/src/services/MomentServices.ts +++ b/src/services/MomentService.ts @@ -1,100 +1,9 @@ import AsyncStorage from '@react-native-community/async-storage'; -import {Alert} from 'react-native'; import RNFetchBlob from 'rn-fetch-blob'; -import { - COMMENTS_ENDPOINT, - MOMENTS_ENDPOINT, - MOMENT_THUMBNAIL_ENDPOINT, -} from '../constants'; -import {ERROR_FAILED_TO_COMMENT} from '../constants/strings'; +import {MOMENTS_ENDPOINT, MOMENT_THUMBNAIL_ENDPOINT} from '../constants'; import {MomentType} from '../types'; import {checkImageUploadStatus} from '../utils'; -//Get all comments for a moment -export const getMomentComments = async ( - momentId: string, - callback: Function, -) => { - try { - const token = await AsyncStorage.getItem('token'); - 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); - } -}; - -export const postMomentComment = async ( - commenter: string, - comment: string, - momentId: string, -) => { - try { - const token = await AsyncStorage.getItem('token'); - 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, - }); - if (response.status !== 200) { - throw 'server error'; - } - return await response.json(); - } catch (error) { - Alert.alert(ERROR_FAILED_TO_COMMENT); - return undefined; - } -}; - -//Get count of comments for a moment -export const getMomentCommentsCount = async ( - momentId: string, - callback: Function, -) => { - try { - const token = await AsyncStorage.getItem('token'); - 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, - ); - } -}; - export const postMoment: ( fileName: string, uri: string, @@ -191,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/services/UserFriendsServices.ts b/src/services/UserFriendsService.ts index f2e15824..f2e15824 100644 --- a/src/services/UserFriendsServices.ts +++ b/src/services/UserFriendsService.ts diff --git a/src/services/index.ts b/src/services/index.ts index 56cefddd..9c168d4f 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1,11 +1,13 @@ export * from './UserProfileService'; export * from './SocialLinkingService'; -export * from './MomentServices'; -export * from './ExploreServices'; -export * from './UserFriendsServices'; +export * from './MomentService'; +export * from './ExploreService'; +export * from './UserFriendsService'; export * from './ReportingService'; export * from './BlockUserService'; export * from './MomentCategoryService'; export * from './NotificationService'; export * from './FCMService'; export * from './WaitlistUserService'; +export * from './CommonService'; +export * from './CommentService'; 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 d9d0b56b..f1ba12f4 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -11,6 +11,7 @@ export interface ProfilePreviewType { username: string; first_name: string; last_name: string; + thumbnail_url: string; } export type FriendshipStatusType = 'friends' | 'requested' | 'no_record'; @@ -83,17 +84,26 @@ export interface MomentType { caption: string; date_created: string; moment_category: string; - path_hash: string; + moment_url: string; + thumbnail_url: string; } -export interface CommentType { +export interface CommentBaseType { comment_id: string; comment: string; date_created: string; - moment_id: string; commenter: ProfilePreviewType; } +export interface CommentType extends CommentBaseType { + moment_id: string; + replies_count: number; +} + +export interface CommentThreadType extends CommentBaseType { + parent_comment: CommentType; +} + export type PreviewType = | 'Comment' | 'Search' @@ -155,6 +165,7 @@ export enum CategorySelectionScreenType { export enum BackgroundGradientType { Light, Dark, + Notification } /** @@ -169,8 +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'; |