diff options
author | Ivan Chen <ivan@tagg.id> | 2021-02-01 16:01:03 -0500 |
---|---|---|
committer | Ivan Chen <ivan@tagg.id> | 2021-02-01 16:01:03 -0500 |
commit | 8d1013e86cf2d66671c337d49a80da157802ad86 (patch) | |
tree | 656b1656068bb6636919359d4faaf7051994ff74 /src | |
parent | 951d85348acef13ec7830629205c30ad5f766bee (diff) | |
parent | 7a09cc96bf1fe468a612bb44362bbef24fccc773 (diff) |
Merge branch 'master' into TMA-546-Onboarding-Page
Diffstat (limited to 'src')
59 files changed, 1219 insertions, 632 deletions
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/icons/back-arrow.svg b/src/assets/icons/back-arrow.svg new file mode 100644 index 00000000..aa203dea --- /dev/null +++ b/src/assets/icons/back-arrow.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:#000;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/icons/up_arrow.svg b/src/assets/icons/up_arrow.svg new file mode 100644 index 00000000..fc92b551 --- /dev/null +++ b/src/assets/icons/up_arrow.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 792 792"><defs></defs><path class="cls-1" d="M522.58,375.86l-18.19,16.86a13.06,13.06,0,0,1-18.46-.68l-62.6-67.45v219a22,22,0,0,1-22,22H393.1a22,22,0,0,1-22-22V322l-65,70.05a13.11,13.11,0,0,1-18.5.68l-18.19-16.86a13.12,13.12,0,0,1-.68-18.51l96.52-104,20.39-22,1.52-1.4a13.6,13.6,0,0,1,2.52-1.85,14.44,14.44,0,0,1,2.85-1.16,2.18,2.18,0,0,1,.88-.2,6.27,6.27,0,0,1,1.2-.2,11.3,11.3,0,0,1,1.16-.08h.44a11.3,11.3,0,0,1,1.16.08,6.58,6.58,0,0,1,1.28.2,4.48,4.48,0,0,1,.92.24c.48.12.93.32,1.41.48l1,.48a13.24,13.24,0,0,1,2.84,2l1.52,1.4,20.43,22,96.48,104A13.12,13.12,0,0,1,522.58,375.86Z" fill="currentColor"/></svg>
\ No newline at end of file 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 f8c0b6bc..56011f05 100644 --- a/src/components/comments/AddComment.tsx +++ b/src/components/comments/AddComment.tsx @@ -1,17 +1,20 @@ -import * as React from 'react'; +import React, {useEffect, useRef} from 'react'; import { Image, + Keyboard, KeyboardAvoidingView, Platform, StyleSheet, View, } from 'react-native'; -import AsyncStorage from '@react-native-community/async-storage'; -import {TaggBigInput} from '../onboarding'; -import {postMomentComment} from '../../services'; -import {logout} from '../../store/actions'; -import {useSelector, useDispatch} from 'react-redux'; +import {TextInput, TouchableOpacity} from 'react-native-gesture-handler'; +import {useDispatch, useSelector} from 'react-redux'; +import UpArrowIcon from '../../assets/icons/up_arrow.svg'; +import {TAGG_LIGHT_BLUE} from '../../constants'; +import {postComment} from '../../services'; +import {updateReplyPosted} from '../../store/actions'; import {RootState} from '../../store/rootreducer'; +import {SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils'; /** * This file provides the add comment view for a user. @@ -21,95 +24,154 @@ import {RootState} from '../../store/rootreducer'; 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} = useSelector((state: RootState) => state.user); const dispatch = useDispatch(); - const { - avatar, - user: {userId}, - } = useSelector((state: RootState) => state.user); - const handleCommentUpdate = (comment: string) => { - setComment(comment); - }; - - const postComment = async () => { - try { - const token = await AsyncStorage.getItem('token'); - if (!token) { - dispatch(logout()); - return; - } - const postedComment = await postMomentComment( - userId, - comment, - moment_id, - token, - ); + const addComment = async () => { + const trimmed = comment.trim(); + if (trimmed === '') { + return; + } + const postedComment = await postComment( + trimmed, + objectId, + isCommentInFocus, + ); - if (postedComment) { - //Set the current comment to en empty string if the comment was posted successfully. - handleCommentUpdate(''); + if (postedComment) { + setComment(''); - //Indicate the MomentCommentsScreen that it needs to download the new comments again - setNewCommentsAvailable(true); + //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}, + }), + ); } - } catch (err) { - console.log('Error while posting comment!'); + setNewCommentsAvailable(true); } }; + useEffect(() => { + const showKeyboard = () => setKeyboardVisible(true); + Keyboard.addListener('keyboardWillShow', showKeyboard); + return () => Keyboard.removeListener('keyboardWillShow', showKeyboard); + }, []); + + useEffect(() => { + const hideKeyboard = () => setKeyboardVisible(false); + Keyboard.addListener('keyboardWillHide', hideKeyboard); + 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'} - keyboardVerticalOffset={130}> - <View style={styles.container}> - <Image - style={styles.avatar} - source={ - avatar - ? {uri: avatar} - : require('../../assets/images/avatar-placeholder.png') - } - /> - <TaggBigInput - style={styles.text} - multiline - placeholder="Add a comment....." - placeholderTextColor="gray" - onChangeText={handleCommentUpdate} - onSubmitEditing={postComment} - value={comment} - /> + keyboardVerticalOffset={SCREEN_HEIGHT * 0.1}> + <View + style={[ + styles.container, + keyboardVisible ? styles.whiteBackround : {}, + ]}> + <View style={styles.textContainer}> + <Image + style={styles.avatar} + source={ + avatar + ? {uri: avatar} + : require('../../assets/images/avatar-placeholder.png') + } + /> + <TextInput + style={styles.text} + placeholder={placeholderText} + placeholderTextColor="grey" + onChangeText={setComment} + value={comment} + autoCorrect={false} + multiline={true} + ref={ref} + /> + <View style={styles.submitButton}> + <TouchableOpacity style={styles.submitButton} onPress={addComment}> + <UpArrowIcon width={35} height={35} color={'white'} /> + </TouchableOpacity> + </View> + </View> </View> </KeyboardAvoidingView> ); }; + const styles = StyleSheet.create({ - container: {flexDirection: 'row'}, + container: { + backgroundColor: '#f7f7f7', + alignItems: 'center', + width: SCREEN_WIDTH, + }, + textContainer: { + width: '95%', + flexDirection: 'row', + backgroundColor: '#e8e8e8', + alignItems: 'center', + justifyContent: 'space-between', + margin: '3%', + borderRadius: 25, + }, text: { - position: 'relative', - right: '18%', - backgroundColor: 'white', - width: '70%', - paddingLeft: '2%', - paddingRight: '2%', - paddingBottom: '1%', - paddingTop: '1%', - height: 60, + flex: 1, + padding: '1%', + marginHorizontal: '1%', }, avatar: { - height: 40, - width: 40, + height: 35, + width: 35, borderRadius: 30, - marginRight: 15, + marginRight: 10, + marginLeft: '3%', + marginVertical: '2%', + alignSelf: 'flex-end', + }, + submitButton: { + height: 35, + width: 35, + backgroundColor: TAGG_LIGHT_BLUE, + borderRadius: 999, + justifyContent: 'center', + alignItems: 'center', + marginRight: '3%', + marginVertical: '2%', + alignSelf: 'flex-end', + }, + whiteBackround: { + backgroundColor: '#fff', }, }); 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/AcceptDeclineButtons.tsx b/src/components/common/AcceptDeclineButtons.tsx index 221056c0..9caaffca 100644 --- a/src/components/common/AcceptDeclineButtons.tsx +++ b/src/components/common/AcceptDeclineButtons.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {StyleProp, StyleSheet, Text, View, ViewStyle} from 'react-native'; -import {TAGG_TEXT_LIGHT_BLUE} from '../../constants'; +import {TAGG_LIGHT_BLUE} from '../../constants'; import {ProfilePreviewType} from '../../types'; import {SCREEN_WIDTH} from '../../utils'; import {TouchableOpacity} from 'react-native-gesture-handler'; @@ -55,18 +55,18 @@ const styles = StyleSheet.create({ }, acceptButton: { padding: 0, - backgroundColor: TAGG_TEXT_LIGHT_BLUE, + backgroundColor: TAGG_LIGHT_BLUE, }, rejectButton: { borderWidth: 1, backgroundColor: 'white', - borderColor: TAGG_TEXT_LIGHT_BLUE, + borderColor: TAGG_LIGHT_BLUE, }, acceptButtonTitleColor: { color: 'white', }, rejectButtonTitleColor: { - color: TAGG_TEXT_LIGHT_BLUE, + color: TAGG_LIGHT_BLUE, }, buttonTitle: { padding: 0, diff --git a/src/components/common/GenericMoreInfoDrawer.tsx b/src/components/common/GenericMoreInfoDrawer.tsx index 098482ae..a23d7736 100644 --- a/src/components/common/GenericMoreInfoDrawer.tsx +++ b/src/components/common/GenericMoreInfoDrawer.tsx @@ -10,7 +10,7 @@ import { } from 'react-native'; import {useSafeAreaInsets} from 'react-native-safe-area-context'; import {BottomDrawer} from '.'; -import {TAGG_TEXT_LIGHT_BLUE} from '../../constants'; +import {TAGG_LIGHT_BLUE} from '../../constants'; import {SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils'; // conforms the JSX onPress attribute type @@ -87,7 +87,7 @@ const styles = StyleSheet.create({ panelButtonTitleCancel: { fontSize: 18, fontWeight: 'bold', - color: TAGG_TEXT_LIGHT_BLUE, + color: TAGG_LIGHT_BLUE, }, divider: {height: 1, borderWidth: 1, borderColor: '#e7e7e7'}, }); diff --git a/src/components/common/SocialLinkModal.tsx b/src/components/common/SocialLinkModal.tsx index b307a62c..41b044fe 100644 --- a/src/components/common/SocialLinkModal.tsx +++ b/src/components/common/SocialLinkModal.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {Modal, StyleSheet, Text, TouchableHighlight, View} from 'react-native'; import {TextInput} from 'react-native-gesture-handler'; -import { TAGG_TEXT_LIGHT_BLUE } from '../../constants'; +import { TAGG_LIGHT_BLUE } from '../../constants'; import {SCREEN_WIDTH} from '../../utils'; interface SocialLinkModalProps { @@ -105,7 +105,7 @@ const styles = StyleSheet.create({ fontSize: 14, /* identical to box height */ textAlign: 'center', - color: TAGG_TEXT_LIGHT_BLUE, + color: TAGG_LIGHT_BLUE, }, textInput: { height: 20, 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/moments/Moment.tsx b/src/components/moments/Moment.tsx index 7905e8a9..a6b553b1 100644 --- a/src/components/moments/Moment.tsx +++ b/src/components/moments/Moment.tsx @@ -11,9 +11,9 @@ import DownIcon from '../../assets/icons/down_icon.svg'; import PlusIcon from '../../assets/icons/plus_icon-01.svg'; import BigPlusIcon from '../../assets/icons/plus_icon-02.svg'; import UpIcon from '../../assets/icons/up_icon.svg'; -import {TAGG_TEXT_LIGHT_BLUE} from '../../constants'; +import {TAGG_LIGHT_BLUE} from '../../constants'; import {ERROR_UPLOAD_MOMENT_SHORT} from '../../constants/strings'; -import {SCREEN_WIDTH} from '../../utils'; +import {normalize, SCREEN_WIDTH} from '../../utils'; import MomentTile from './MomentTile'; interface MomentProps { @@ -87,7 +87,7 @@ const Moment: React.FC<MomentProps> = ({ width={19} height={19} onPress={() => move('up', title)} - color={TAGG_TEXT_LIGHT_BLUE} + color={TAGG_LIGHT_BLUE} style={{marginLeft: 5}} /> )} @@ -96,7 +96,7 @@ const Moment: React.FC<MomentProps> = ({ width={19} height={19} onPress={() => move('down', title)} - color={TAGG_TEXT_LIGHT_BLUE} + color={TAGG_LIGHT_BLUE} style={{marginLeft: 5}} /> )} @@ -111,7 +111,7 @@ const Moment: React.FC<MomentProps> = ({ width={21} height={21} onPress={() => navigateToImagePicker()} - color={TAGG_TEXT_LIGHT_BLUE} + color={TAGG_LIGHT_BLUE} style={{marginRight: 10}} /> {shouldAllowDeletion && ( @@ -171,15 +171,10 @@ const styles = StyleSheet.create({ alignItems: 'center', }, titleText: { - fontSize: 16, + fontSize: normalize(16), fontWeight: 'bold', - color: TAGG_TEXT_LIGHT_BLUE, + color: TAGG_LIGHT_BLUE, }, - // titleContainer: { - // flex: 1, - // flexDirection: 'row', - // justifyContent: 'flex-end', - // }, flexer: { flex: 1, }, diff --git a/src/components/moments/MomentPostContent.tsx b/src/components/moments/MomentPostContent.tsx index 93271fa1..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,9 +24,14 @@ 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 ( <View style={[styles.container, style]}> <Image 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..e0ae231e 100644 --- a/src/components/notifications/Notification.tsx +++ b/src/components/notifications/Notification.tsx @@ -1,25 +1,30 @@ 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 {useDispatch, useStore} from 'react-redux'; +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 +35,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 +49,29 @@ 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); } - }; - 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; @@ -70,7 +82,11 @@ const Notification: React.FC<NotificationProps> = (props) => { } }; if (notification_type === 'CMT' && notification_object) { - loadMomentImage(notification_object.moment_id); + loadMomentImage( + notification_object.moment_id + ? notification_object.moment_id + : notification_object.parent_comment.moment_id, + ); return () => { mounted = false; }; @@ -94,20 +110,58 @@ 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); } @@ -118,7 +172,9 @@ const Notification: React.FC<NotificationProps> = (props) => { }; 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()); }; @@ -137,8 +193,8 @@ const Notification: React.FC<NotificationProps> = (props) => { <Image style={styles.avatar} source={ - avatarURI - ? {uri: avatarURI, cache: 'only-if-cached'} + avatar + ? {uri: avatar} : require('../../assets/images/avatar-placeholder.png') } /> @@ -159,8 +215,8 @@ const Notification: React.FC<NotificationProps> = (props) => { </View> )} {notification_type === 'CMT' && notification_object && ( - <Image style={styles.moment} source={{uri: momentURI}} /> - )} + <Image style={styles.moment} source={{uri: momentURI}} /> + )} </TouchableWithoutFeedback> </> ); 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/onboarding/TaggBigInput.tsx b/src/components/onboarding/TaggBigInput.tsx index 4e8e1ef7..0e42bd13 100644 --- a/src/components/onboarding/TaggBigInput.tsx +++ b/src/components/onboarding/TaggBigInput.tsx @@ -1,5 +1,11 @@ import React from 'react'; -import {View, TextInput, StyleSheet, TextInputProps} from 'react-native'; +import { + View, + TextInput, + StyleSheet, + TextInputProps, + ViewStyle, +} from 'react-native'; import * as Animatable from 'react-native-animatable'; import {TAGG_LIGHT_PURPLE} from '../../constants'; @@ -8,13 +14,15 @@ interface TaggBigInputProps extends TextInputProps { invalidWarning?: string; attemptedSubmit?: boolean; width?: number | string; + containerStyle?: ViewStyle; } /** * An input component that receives all props a normal TextInput component does. TaggInput components grow to 60% of their parent's width by default, but this can be set using the `width` prop. */ const TaggBigInput = React.forwardRef((props: TaggBigInputProps, ref: any) => { return ( - <View style={styles.container}> + <View + style={props.containerStyle ? props.containerStyle : styles.container}> <TextInput style={[{width: props.width}, styles.input]} placeholderTextColor="#ddd" diff --git a/src/components/profile/Content.tsx b/src/components/profile/Content.tsx index e7fb566b..a35a5820 100644 --- a/src/components/profile/Content.tsx +++ b/src/components/profile/Content.tsx @@ -1,3 +1,4 @@ +import {useFocusEffect, useNavigation} from '@react-navigation/native'; import React, {useCallback, useEffect, useState} from 'react'; import { Alert, @@ -9,52 +10,49 @@ import { Text, View, } from 'react-native'; +import {TouchableOpacity} from 'react-native-gesture-handler'; import Animated from 'react-native-reanimated'; +import {useDispatch, useSelector, useStore} from 'react-redux'; +import {Cover} from '.'; +import GreyPlusLogo from '../../assets/icons/grey-plus-logo.svg'; +import {COVER_HEIGHT, TAGG_LIGHT_BLUE} from '../../constants'; import { - CategorySelectionScreenType, - FriendshipStatusType, - MomentCategoryType, - MomentType, - ProfilePreviewType, - ProfileType, - ScreenType, - UserType, -} from '../../types'; -import {COVER_HEIGHT, TAGG_TEXT_LIGHT_BLUE} from '../../constants'; + UPLOAD_MOMENT_PROMPT_THREE_HEADER, + UPLOAD_MOMENT_PROMPT_THREE_MESSAGE, + UPLOAD_MOMENT_PROMPT_TWO_HEADER, + UPLOAD_MOMENT_PROMPT_TWO_MESSAGE, +} from '../../constants/strings'; +import { + blockUnblockUser, + deleteUserMomentsForCategory, + friendUnfriendUser, + loadFriendsData, + updateMomentCategories, + updateUserXFriends, + updateUserXProfileAllScreens, +} from '../../store/actions'; +import { + EMPTY_MOMENTS_LIST, + EMPTY_PROFILE_PREVIEW_LIST, + NO_PROFILE, + NO_USER, +} from '../../store/initialStates'; +import {RootState} from '../../store/rootreducer'; +import {CategorySelectionScreenType, MomentType, ScreenType} from '../../types'; import { fetchUserX, getUserAsProfilePreviewType, moveCategory, + normalize, SCREEN_HEIGHT, userLogin, } from '../../utils'; -import TaggsBar from '../taggs/TaggsBar'; +import {TaggPrompt} from '../common'; import {Moment} from '../moments'; +import TaggsBar from '../taggs/TaggsBar'; import ProfileBody from './ProfileBody'; import ProfileCutout from './ProfileCutout'; import ProfileHeader from './ProfileHeader'; -import {useDispatch, useSelector, useStore} from 'react-redux'; -import {RootState} from '../../store/rootreducer'; -import { - friendUnfriendUser, - blockUnblockUser, - loadFriendsData, - updateUserXFriends, - updateMomentCategories, - deleteUserMomentsForCategory, - updateUserXProfileAllScreens, -} from '../../store/actions'; -import { - NO_USER, - NO_PROFILE, - EMPTY_PROFILE_PREVIEW_LIST, - EMPTY_MOMENTS_LIST, -} from '../../store/initialStates'; -import {Cover} from '.'; -import {TouchableOpacity} from 'react-native-gesture-handler'; -import {useFocusEffect, useNavigation} from '@react-navigation/native'; -import GreyPlusLogo from '../../assets/icons/grey-plus-logo.svg'; -import {TaggPrompt} from '../common'; interface ContentProps { y: Animated.Value<number>; @@ -113,9 +111,10 @@ const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => { const [isStageOnePromptClosed, setIsStageOnePromptClosed] = useState<boolean>( false, ); - const [isStageThreePromptClosed, setIsStageThreePromptClosed] = useState< - boolean - >(false); + const [ + isStageThreePromptClosed, + setIsStageThreePromptClosed, + ] = useState<boolean>(false); const onRefresh = useCallback(() => { const refrestState = async () => { @@ -284,7 +283,7 @@ const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => { momentCategories.filter((mc) => mc !== category), false, ), - ) + ); dispatch(deleteUserMomentsForCategory(category)); }, }, @@ -352,10 +351,8 @@ const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => { profile.profile_completion_stage === 2 && !isStageTwoPromptClosed && ( <TaggPrompt - messageHeader="Create a new category" - messageBody={ - 'Post your first moment to continue building your digital identity!' - } + messageHeader={UPLOAD_MOMENT_PROMPT_TWO_HEADER} + messageBody={UPLOAD_MOMENT_PROMPT_TWO_MESSAGE} logoType="" onClose={() => { setIsStageTwoPromptClosed(true); @@ -366,10 +363,8 @@ const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => { profile.profile_completion_stage === 3 && !isStageThreePromptClosed && ( <TaggPrompt - messageHeader="Continue to build your profile" - messageBody={ - 'Continue to personalize your own digital space in\nthis community by filling your profile with\ncategories and moments!' - } + messageHeader={UPLOAD_MOMENT_PROMPT_THREE_HEADER} + messageBody={UPLOAD_MOMENT_PROMPT_THREE_MESSAGE} logoType="" onClose={() => { setIsStageThreePromptClosed(true); @@ -423,7 +418,7 @@ const styles = StyleSheet.create({ flexDirection: 'column', }, createCategoryButton: { - backgroundColor: TAGG_TEXT_LIGHT_BLUE, + backgroundColor: TAGG_LIGHT_BLUE, justifyContent: 'center', alignItems: 'center', width: '70%', @@ -432,7 +427,7 @@ const styles = StyleSheet.create({ alignSelf: 'center', }, createCategoryButtonLabel: { - fontSize: 16, + fontSize: normalize(16), fontWeight: '500', color: 'white', }, @@ -443,7 +438,7 @@ const styles = StyleSheet.create({ marginVertical: '10%', }, noMomentsText: { - fontSize: 14, + fontSize: normalize(14), fontWeight: 'bold', color: 'gray', marginVertical: '8%', diff --git a/src/components/profile/FriendsCount.tsx b/src/components/profile/FriendsCount.tsx index 23a24787..851dbc3b 100644 --- a/src/components/profile/FriendsCount.tsx +++ b/src/components/profile/FriendsCount.tsx @@ -5,6 +5,7 @@ import {useNavigation} from '@react-navigation/native'; import {RootState} from '../../store/rootReducer'; import {useSelector} from 'react-redux'; import {ScreenType} from '../../types'; +import {normalize} from '../../utils'; interface FriendsCountProps extends ViewProps { userXId: string | undefined; @@ -16,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 @@ -55,11 +56,11 @@ const styles = StyleSheet.create({ }, count: { fontWeight: '700', - fontSize: 13, + fontSize: normalize(14), }, label: { fontWeight: '500', - fontSize: 13, + fontSize: normalize(14), }, }); diff --git a/src/components/profile/ProfileBody.tsx b/src/components/profile/ProfileBody.tsx index 6284ff59..1ee3ae2b 100644 --- a/src/components/profile/ProfileBody.tsx +++ b/src/components/profile/ProfileBody.tsx @@ -1,9 +1,9 @@ import React from 'react'; import {StyleSheet, View, Text, LayoutChangeEvent, Linking} from 'react-native'; -import {Button} from 'react-native-elements'; +import {Button, normalize} from 'react-native-elements'; import { TAGG_DARK_BLUE, - TAGG_TEXT_LIGHT_BLUE, + TAGG_LIGHT_BLUE, TOGGLE_BUTTON_TYPE, } from '../../constants'; import ToggleButton from './ToggleButton'; @@ -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 /> )} @@ -160,16 +160,15 @@ const styles = StyleSheet.create({ }, username: { fontWeight: '600', - fontSize: 16.5, + fontSize: normalize(12), marginBottom: '1%', - marginTop: '-3%', }, biography: { - fontSize: 16, + fontSize: normalize(12), marginBottom: '1.5%', }, website: { - fontSize: 16, + fontSize: normalize(12), color: TAGG_DARK_BLUE, marginBottom: '1%', }, @@ -177,16 +176,17 @@ const styles = StyleSheet.create({ justifyContent: 'center', alignItems: 'center', width: SCREEN_WIDTH * 0.4, - height: SCREEN_WIDTH * 0.09, - borderColor: TAGG_TEXT_LIGHT_BLUE, - borderWidth: 3, - borderRadius: 5, + height: SCREEN_WIDTH * 0.075, + borderColor: TAGG_LIGHT_BLUE, + borderWidth: 2, + borderRadius: 0, marginRight: '2%', + marginLeft: '1%', padding: 0, backgroundColor: 'transparent', }, requestedButtonTitle: { - color: TAGG_TEXT_LIGHT_BLUE, + color: TAGG_LIGHT_BLUE, padding: 0, fontSize: 14, fontWeight: '700', @@ -201,11 +201,14 @@ 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: 0, marginRight: '2%', - backgroundColor: TAGG_TEXT_LIGHT_BLUE, + marginLeft: '1%', + backgroundColor: TAGG_LIGHT_BLUE, }, }); diff --git a/src/components/profile/ProfileHeader.tsx b/src/components/profile/ProfileHeader.tsx index 8d502d97..7dad2a68 100644 --- a/src/components/profile/ProfileHeader.tsx +++ b/src/components/profile/ProfileHeader.tsx @@ -2,10 +2,10 @@ import React, {useState} from 'react'; import {StyleSheet, Text, View} from 'react-native'; import {useSelector} from 'react-redux'; import {UniversityIcon} from '.'; -import {NO_MOMENTS} from '../../store/initialStates'; +import {PROFILE_CUTOUT_TOP_Y} from '../../constants'; import {RootState} from '../../store/rootreducer'; import {ScreenType} from '../../types'; -import {SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils'; +import {normalize} from '../../utils'; import Avatar from './Avatar'; import FriendsCount from './FriendsCount'; import ProfileMoreInfoDrawer from './ProfileMoreInfoDrawer'; @@ -31,7 +31,6 @@ const ProfileHeader: React.FC<ProfileHeaderProps> = ({ : useSelector((state: RootState) => state.user); const [drawerVisible, setDrawerVisible] = useState(false); const [firstName, lastName] = [...name.split(' ')]; - return ( <View style={styles.container}> <ProfileMoreInfoDrawer @@ -59,13 +58,8 @@ const ProfileHeader: React.FC<ProfileHeaderProps> = ({ </View> )} <View style={styles.friendsAndUniversity}> - <FriendsCount - style={styles.friends} - screenType={screenType} - userXId={userXId} - /> + <FriendsCount screenType={screenType} userXId={userXId} /> <UniversityIcon - style={styles.university} university="brown" university_class={university_class} /> @@ -78,7 +72,7 @@ const ProfileHeader: React.FC<ProfileHeaderProps> = ({ const styles = StyleSheet.create({ container: { - top: SCREEN_HEIGHT / 2.4, + top: PROFILE_CUTOUT_TOP_Y * 1.02, width: '100%', position: 'absolute', }, @@ -87,35 +81,27 @@ const styles = StyleSheet.create({ }, header: { flexDirection: 'column', - justifyContent: 'center', + justifyContent: 'space-evenly', alignItems: 'center', - marginTop: SCREEN_WIDTH / 18.2, - marginLeft: SCREEN_WIDTH / 8, - marginBottom: SCREEN_WIDTH / 50, + marginRight: '15%', + marginLeft: '5%', + flex: 1, }, avatar: { - bottom: SCREEN_WIDTH / 80, - left: '10%', + marginLeft: '3%', + top: '-8%', }, name: { - marginLeft: SCREEN_WIDTH / 8, - fontSize: 17, + fontSize: normalize(17), fontWeight: '500', alignSelf: 'center', }, - friends: { - alignSelf: 'flex-start', - marginRight: SCREEN_WIDTH / 20, - }, - university: { - alignSelf: 'flex-end', - bottom: 3, - }, friendsAndUniversity: { flexDirection: 'row', - flex: 1, - marginLeft: SCREEN_WIDTH / 10, - marginTop: SCREEN_WIDTH / 40, + alignItems: 'center', + justifyContent: 'space-evenly', + width: '100%', + height: 50, }, }); diff --git a/src/components/profile/ProfileMoreInfoDrawer.tsx b/src/components/profile/ProfileMoreInfoDrawer.tsx index 76f0f27f..daa83eb3 100644 --- a/src/components/profile/ProfileMoreInfoDrawer.tsx +++ b/src/components/profile/ProfileMoreInfoDrawer.tsx @@ -4,7 +4,7 @@ import {StyleSheet, TouchableOpacity} from 'react-native'; import {useSelector} from 'react-redux'; import MoreIcon from '../../assets/icons/more_horiz-24px.svg'; import PersonOutline from '../../assets/ionicons/person-outline.svg'; -import {TAGG_DARK_BLUE, TAGG_TEXT_LIGHT_BLUE} from '../../constants'; +import {TAGG_DARK_BLUE, TAGG_LIGHT_BLUE} from '../../constants'; import {RootState} from '../../store/rootreducer'; import {SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils'; import {GenericMoreInfoDrawer} from '../common'; @@ -101,13 +101,12 @@ const styles = StyleSheet.create({ panelButtonTitleCancel: { fontSize: 18, fontWeight: 'bold', - color: TAGG_TEXT_LIGHT_BLUE, + color: TAGG_LIGHT_BLUE, }, divider: {height: 1, borderWidth: 1, borderColor: '#e7e7e7'}, more: { position: 'absolute', right: '5%', - marginTop: '4%', zIndex: 1, }, }); diff --git a/src/components/profile/ProfilePreview.tsx b/src/components/profile/ProfilePreview.tsx index b2c0a24d..38defb8d 100644 --- a/src/components/profile/ProfilePreview.tsx +++ b/src/components/profile/ProfilePreview.tsx @@ -1,32 +1,21 @@ -import React, {useEffect, useState, useContext} from 'react'; -import {ProfilePreviewType, ScreenType} from '../../types'; +import AsyncStorage from '@react-native-community/async-storage'; +import {useNavigation} from '@react-navigation/native'; +import React, {useEffect, useState} from 'react'; import { - View, - Text, + Alert, Image, StyleSheet, - ViewProps, + Text, TouchableOpacity, - Alert, + View, + ViewProps, } from 'react-native'; -import {useNavigation} from '@react-navigation/native'; -import RNFetchBlob from 'rn-fetch-blob'; -import AsyncStorage from '@react-native-community/async-storage'; -import {PROFILE_PHOTO_THUMBNAIL_ENDPOINT} from '../../constants'; -import {UserType, PreviewType} from '../../types'; -import {isUserBlocked, loadAvatar} from '../../services'; -import {useSelector, useDispatch, useStore} from 'react-redux'; +import {useDispatch, useSelector, useStore} from 'react-redux'; +import {ERROR_UNABLE_TO_VIEW_PROFILE} from '../../constants/strings'; +import {loadImageFromURL} from '../../services'; import {RootState} from '../../store/rootreducer'; -import {logout} from '../../store/actions'; +import {PreviewType, ProfilePreviewType, ScreenType} from '../../types'; import {checkIfUserIsBlocked, fetchUserX, userXInStore} from '../../utils'; -import {SearchResultsBackground} from '../search'; -import NavigationBar from 'src/routes/tabs'; -import {ERROR_UNABLE_TO_VIEW_PROFILE} from '../../constants/strings'; - -const NO_USER: UserType = { - userId: '', - username: '', -}; /** * This component returns user's profile picture friended by username as a touchable component. @@ -44,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. @@ -81,6 +65,7 @@ const ProfilePreview: React.FC<ProfilePreviewProps> = ({ username, first_name, last_name, + thumbnail_url, }; try { @@ -212,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/profile/ToggleButton.tsx b/src/components/profile/ToggleButton.tsx index 5d8f7874..236d811c 100644 --- a/src/components/profile/ToggleButton.tsx +++ b/src/components/profile/ToggleButton.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import {StyleSheet, Text} from 'react-native'; import {TouchableOpacity} from 'react-native-gesture-handler'; -import {TAGG_TEXT_LIGHT_BLUE} from '../../constants'; +import {TAGG_LIGHT_BLUE} from '../../constants'; import {getToggleButtonText, SCREEN_WIDTH} from '../../utils'; type ToggleButtonProps = { @@ -36,7 +36,7 @@ const styles = StyleSheet.create({ alignItems: 'center', width: SCREEN_WIDTH * 0.4, height: SCREEN_WIDTH * 0.08, - borderColor: TAGG_TEXT_LIGHT_BLUE, + borderColor: TAGG_LIGHT_BLUE, borderWidth: 3, borderRadius: 5, marginRight: '2%', @@ -45,10 +45,10 @@ const styles = StyleSheet.create({ fontWeight: 'bold', }, buttonColor: { - backgroundColor: TAGG_TEXT_LIGHT_BLUE, + backgroundColor: TAGG_LIGHT_BLUE, }, textColor: {color: 'white'}, buttonColorToggled: {backgroundColor: 'white'}, - textColorToggled: {color: TAGG_TEXT_LIGHT_BLUE}, + textColorToggled: {color: TAGG_LIGHT_BLUE}, }); export default ToggleButton; diff --git a/src/components/profile/UniversityIcon.tsx b/src/components/profile/UniversityIcon.tsx index 13586359..95aef8b9 100644 --- a/src/components/profile/UniversityIcon.tsx +++ b/src/components/profile/UniversityIcon.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {StyleSheet, ViewProps} from 'react-native'; import {Image, Text, View} from 'react-native-animatable'; -import {getUniversityClass} from '../../utils'; +import {getUniversityClass, normalize} from '../../utils'; export interface UniversityIconProps extends ViewProps { university: string; @@ -38,19 +38,19 @@ const UniversityIcon: React.FC<UniversityIconProps> = ({ const styles = StyleSheet.create({ container: { - flex: 1, flexDirection: 'column', flexWrap: 'wrap', justifyContent: 'center', + alignItems: 'center', + height: '100%', }, univClass: { - fontSize: 13, + fontSize: normalize(14), fontWeight: '500', }, icon: { - alignSelf: 'center', - width: 17, - height: 19, + width: normalize(17), + height: normalize(19), }, }); diff --git a/src/components/search/Explore.tsx b/src/components/search/Explore.tsx index c07c66b8..2a3bc749 100644 --- a/src/components/search/Explore.tsx +++ b/src/components/search/Explore.tsx @@ -4,6 +4,7 @@ import {useSelector} from 'react-redux'; import {EXPLORE_SECTION_TITLES} from '../../constants'; import {RootState} from '../../store/rootReducer'; import {ExploreSectionType} from '../../types'; +import {normalize} from '../../utils'; import ExploreSection from './ExploreSection'; const Explore: React.FC = () => { @@ -11,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> ); }; @@ -21,11 +23,10 @@ const Explore: React.FC = () => { const styles = StyleSheet.create({ container: { zIndex: 0, - // margin: '5%', }, header: { fontWeight: '700', - fontSize: 22, + fontSize: normalize(22), color: '#fff', marginBottom: '2%', margin: '5%', diff --git a/src/components/search/ExploreSection.tsx b/src/components/search/ExploreSection.tsx index 8e8b4988..025c8c3c 100644 --- a/src/components/search/ExploreSection.tsx +++ b/src/components/search/ExploreSection.tsx @@ -1,6 +1,7 @@ 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'; /** @@ -16,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 /> @@ -34,7 +38,7 @@ const styles = StyleSheet.create({ }, header: { fontWeight: '600', - fontSize: 20, + fontSize: normalize(18), color: '#fff', marginLeft: '5%', marginBottom: '5%', diff --git a/src/components/search/ExploreSectionUser.tsx b/src/components/search/ExploreSectionUser.tsx index 0bf68a20..b0cfe5c6 100644 --- a/src/components/search/ExploreSectionUser.tsx +++ b/src/components/search/ExploreSectionUser.tsx @@ -9,10 +9,10 @@ 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, userXInStore} from '../../utils'; +import {fetchUserX, normalize, userXInStore} from '../../utils'; /** * Search Screen for user recommendations and a search @@ -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 @@ -110,13 +104,13 @@ const styles = StyleSheet.create({ name: { fontWeight: '600', flexWrap: 'wrap', - fontSize: 16, + fontSize: normalize(16), color: '#fff', textAlign: 'center', }, username: { fontWeight: '400', - fontSize: 14, + fontSize: normalize(14), color: '#fff', }, }); diff --git a/src/components/search/RecentSearches.tsx b/src/components/search/RecentSearches.tsx index 8a06017c..bdbd5773 100644 --- a/src/components/search/RecentSearches.tsx +++ b/src/components/search/RecentSearches.tsx @@ -7,7 +7,7 @@ import { TouchableOpacityProps, } from 'react-native'; import {PreviewType, ProfilePreviewType, ScreenType} from 'src/types'; -import {TAGG_TEXT_LIGHT_BLUE} from '../../constants'; +import {TAGG_LIGHT_BLUE} from '../../constants'; import SearchResults from './SearchResults'; interface RecentSearchesProps extends TouchableOpacityProps { @@ -55,7 +55,7 @@ const styles = StyleSheet.create({ clear: { fontSize: 18, fontWeight: 'bold', - color: TAGG_TEXT_LIGHT_BLUE, + color: TAGG_LIGHT_BLUE, }, }); diff --git a/src/components/taggs/Tagg.tsx b/src/components/taggs/Tagg.tsx index 82ac07df..5fa8b395 100644 --- a/src/components/taggs/Tagg.tsx +++ b/src/components/taggs/Tagg.tsx @@ -22,6 +22,7 @@ import { ERROR_UNABLE_TO_FIND_PROFILE, SUCCESS_LINK, } from '../../constants/strings'; +import {normalize} from '../../utils'; interface TaggProps { social: string; @@ -165,7 +166,7 @@ const styles = StyleSheet.create({ justifyContent: 'space-between', alignItems: 'center', marginHorizontal: 15, - height: 90, + height: normalize(90), }, iconTap: { justifyContent: 'center', diff --git a/src/components/taggs/TwitterTaggPost.tsx b/src/components/taggs/TwitterTaggPost.tsx index c971a82c..0cfde857 100644 --- a/src/components/taggs/TwitterTaggPost.tsx +++ b/src/components/taggs/TwitterTaggPost.tsx @@ -6,7 +6,7 @@ import LinearGradient from 'react-native-linear-gradient'; import { AVATAR_DIM, TAGGS_GRADIENT, - TAGG_TEXT_LIGHT_BLUE, + TAGG_LIGHT_BLUE, } from '../../constants'; import {TwitterPostType} from '../../types'; import {handleOpenSocialUrlOnBrowser, SCREEN_WIDTH} from '../../utils'; @@ -237,7 +237,7 @@ const styles = StyleSheet.create({ }, replyShowThisThread: { fontSize: 15, - color: TAGG_TEXT_LIGHT_BLUE, + color: TAGG_LIGHT_BLUE, }, }); 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 b96d9438..ad43c337 100644 --- a/src/constants/constants.ts +++ b/src/constants/constants.ts @@ -1,13 +1,15 @@ import {ReactText} from 'react'; import {BackgroundGradientType, ExploreSectionType} from './../types/'; -import {SCREEN_WIDTH, SCREEN_HEIGHT} from '../utils'; +import {SCREEN_WIDTH, SCREEN_HEIGHT, isIPhoneX, normalize} from '../utils'; export const CHIN_HEIGHT = 34; -export const PROFILE_CUTOUT_TOP_Y = SCREEN_HEIGHT / 2.3; -export const PROFILE_CUTOUT_BOTTOM_Y = SCREEN_HEIGHT / 1.76; -export const PROFILE_CUTOUT_CORNER_X = SCREEN_WIDTH / 2.9; -export const PROFILE_CUTOUT_CORNER_Y = SCREEN_HEIGHT / 1.95; +export const PROFILE_CUTOUT_TOP_Y = SCREEN_HEIGHT * 0.435; +export const PROFILE_CUTOUT_BOTTOM_Y = isIPhoneX() + ? SCREEN_HEIGHT * 0.55 + : SCREEN_HEIGHT * 0.58; +export const PROFILE_CUTOUT_CORNER_X = SCREEN_WIDTH * 0.344; +export const PROFILE_CUTOUT_CORNER_Y = SCREEN_HEIGHT * 0.513; export const IMAGE_WIDTH = SCREEN_WIDTH; export const IMAGE_HEIGHT = SCREEN_WIDTH; @@ -17,7 +19,7 @@ export const AVATAR_DIM = 44; export const AVATAR_GRADIENT_DIM = 50; export const TAGG_ICON_DIM = 58; -export const TAGG_RING_DIM = 65; +export const TAGG_RING_DIM = normalize(60); export const INTEGRATED_SOCIAL_LIST: string[] = [ 'Instagram', @@ -59,7 +61,7 @@ export const SNAPCHAT_FONT_COLOR: string = '#FFFC00'; export const YOUTUBE_FONT_COLOR: string = '#FCA4A4'; export const TAGG_DARK_BLUE = '#4E699C'; -export const TAGG_TEXT_LIGHT_BLUE: string = '#698DD3'; +export const TAGG_LIGHT_BLUE: string = '#698DD3'; export const TAGG_LIGHT_PURPLE = '#F4DDFF'; export const TAGGS_GRADIENT = { diff --git a/src/constants/regex.ts b/src/constants/regex.ts index fe5ce3ab..7de36492 100644 --- a/src/constants/regex.ts +++ b/src/constants/regex.ts @@ -52,10 +52,10 @@ export const genderRegex: RegExp = /^$|^[A-Za-z\- ]{2,20}$/; /** * The phone regex has the following constraints * - must be 10 digits - * - accepts 012-345-6789 + * - accepts 0123456789 * */ -export const phoneRegex: RegExp = /([0-9]{10})/; +export const phoneRegex: RegExp = /^[0-9]{10}$/; /** * The code regex has the following constraints diff --git a/src/constants/strings.ts b/src/constants/strings.ts index b5344afd..9680320a 100644 --- a/src/constants/strings.ts +++ b/src/constants/strings.ts @@ -1,34 +1,38 @@ + /* 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_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'; export const ERROR_INVALID_VERIFICATION_CODE = 'Invalid verification code, try re-entering or tap the resend code button for a new code'; export const ERROR_INVALID_VERIFICATION_CODE_FORMAT = 'Please enter the 6 digit code sent to your phone'; export const ERROR_INVLAID_CODE = 'The code entered is not valid!'; -export const ERROR_LINK = (str: string) => `Unable to link with ${str}, Please check your login and try again`; -export const ERROR_LOGIN = 'There was a problem logging you in, please refresh and try again'; +export const ERROR_LINK = (str: string) => `Unable to link with ${str}, Please check your login and try again`; +export const ERROR_LOGIN = 'There was a problem logging you in, please refresh and try again'; export const ERROR_LOGIN_FAILED = 'Login failed. Check your username and passoword, and try again'; export const ERROR_NEXT_PAGE = 'There was a problem while loading the next page 😓, try again in a couple minutes'; export const ERROR_PROFILE_CREATION_SHORT = 'Profile creation failed 😓'; export const ERROR_PWD_ACCOUNT = (str: string) => `Please make sure that the email / username entered is registered with us. You may contact our customer support at ${str}`; -export const ERROR_REGISTRATION = (str: string) => `Registration failed 😔, ${str}`; +export const ERROR_REGISTRATION = (str: string) => `Registration failed 😔, ${str}`; export const ERROR_SELECT_CLASS_YEAR = 'Please select your Class Year'; export const ERROR_SERVER_DOWN = 'mhm, looks like our servers are down, please refresh and try again in a few mins'; -export const ERROR_SOMETHING_WENT_WRONG = "Oh dear, don’t worry someone will be held responsible for this error, In the meantime refresh the app"; +export const ERROR_SOMETHING_WENT_WRONG = 'Oh dear, don’t worry someone will be held responsible for this error, In the meantime refresh the app'; export const ERROR_SOMETHING_WENT_WRONG_REFRESH = "Ha, looks like this one's on us, please refresh and try again"; export const ERROR_SOMETHING_WENT_WRONG_RELOAD = "You broke it, Just kidding! we don't know what happened... Please reload the app and try again"; export const ERROR_UNABLE_TO_FIND_PROFILE = 'We were unable to find this profile. Please check username and try again'; @@ -45,3 +49,8 @@ export const SUCCESS_LINK = (str: string) => `Successfully linked ${str} 🎉`; export const SUCCESS_PIC_UPLOAD = 'Beautiful, the picture was uploaded successfully!'; export const SUCCESS_PWD_RESET = 'Your password was reset successfully!'; export const SUCCESS_VERIFICATION_CODE_SENT = 'New verification code sent! Check your phone messages for your code'; +export const UPLOAD_MOMENT_PROMPT_ONE_MESSAGE = 'Post your first moment to\n continue building your digital\nidentity!'; +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!';
\ No newline at end of file diff --git a/src/routes/main/MainStackNavigator.tsx b/src/routes/main/MainStackNavigator.tsx index bd838ef2..663aeaea 100644 --- a/src/routes/main/MainStackNavigator.tsx +++ b/src/routes/main/MainStackNavigator.tsx @@ -37,6 +37,7 @@ export type MainStackParams = { moment_id: string; userXId: string | undefined; screenType: ScreenType; + comment_id?: string; }; FriendsListScreen: { userXId: string | undefined; diff --git a/src/routes/main/MainStackScreen.tsx b/src/routes/main/MainStackScreen.tsx index b4eaa213..3e425101 100644 --- a/src/routes/main/MainStackScreen.tsx +++ b/src/routes/main/MainStackScreen.tsx @@ -54,7 +54,7 @@ const MainStackScreen: React.FC<MainStackProps> = ({route}) => { })(); const modalStyle: StackNavigationOptions = { - cardStyle: {backgroundColor: 'transparent'}, + cardStyle: {backgroundColor: 'rgba(80,80,80,0.9)'}, gestureDirection: 'vertical', cardOverlayEnabled: true, cardStyleInterpolator: ({current: {progress}}) => ({ @@ -64,14 +64,6 @@ const MainStackScreen: React.FC<MainStackProps> = ({route}) => { outputRange: [0, 0.25, 0.7, 1], }), }, - overlayStyle: { - backgroundColor: '#505050', - opacity: progress.interpolate({ - inputRange: [0, 1], - outputRange: [0, 0.9], - extrapolate: 'clamp', - }), - }, }), }; @@ -156,16 +148,25 @@ const MainStackScreen: React.FC<MainStackProps> = ({route}) => { name="IndividualMoment" component={IndividualMoment} options={{ - ...modalStyle, + gestureEnabled: false, + cardStyle: { + backgroundColor: 'rgba(0, 0, 0, 0.6)', + }, + cardOverlayEnabled: true, + cardStyleInterpolator: ({current: {progress}}) => ({ + cardStyle: { + opacity: progress.interpolate({ + inputRange: [0, 0.5, 0.9, 1], + outputRange: [0, 0.25, 0.7, 1], + }), + }, + }), }} initialParams={{screenType}} /> <MainStack.Screen name="MomentCommentsScreen" component={MomentCommentsScreen} - options={{ - ...modalStyle, - }} initialParams={{screenType}} /> <MainStack.Screen 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/RegistrationOne.tsx b/src/screens/onboarding/RegistrationOne.tsx index 2a1d884d..c9822f76 100644 --- a/src/screens/onboarding/RegistrationOne.tsx +++ b/src/screens/onboarding/RegistrationOne.tsx @@ -146,7 +146,7 @@ const RegistrationOne: React.FC<RegistrationOneProps> = ({navigation}) => { <Text style={styles.formHeader}>ENTER PHONE NUMBER</Text> </View> <TaggInput - maxLength={12} // currently only support US phone numbers + maxLength={10} // currently only support US phone numbers accessibilityHint="Enter your phone number." accessibilityLabel="Phone number input field." placeholder="Phone Number" @@ -154,7 +154,7 @@ const RegistrationOne: React.FC<RegistrationOneProps> = ({navigation}) => { textContentType="telephoneNumber" autoCapitalize="none" returnKeyType="next" - keyboardType="phone-pad" + keyboardType="number-pad" onChangeText={handlePhoneUpdate} blurOnSubmit={false} ref={phoneRef} diff --git a/src/screens/profile/EditProfile.tsx b/src/screens/profile/EditProfile.tsx index 3b3fa36e..7d3ca581 100644 --- a/src/screens/profile/EditProfile.tsx +++ b/src/screens/profile/EditProfile.tsx @@ -29,9 +29,10 @@ import { websiteRegex, bioRegex, genderRegex, + CLASS_YEAR_LIST, } from '../../constants'; import AsyncStorage from '@react-native-community/async-storage'; -import {ProfileStackParams} from '../../routes'; +import {MainStackParams} from '../../routes'; import Animated from 'react-native-reanimated'; import {HeaderHeight, SCREEN_HEIGHT} from '../../utils'; import {RootState} from '../../store/rootReducer'; @@ -47,12 +48,12 @@ import { import TaggLoadingIndicator from '../../components/common/TaggLoadingIndicator'; type EditProfileNavigationProp = StackNavigationProp< - ProfileStackParams, + MainStackParams, 'EditProfile' >; interface EditProfileProps { - route: RouteProp<ProfileStackParams, 'EditProfile'>; + route: RouteProp<MainStackParams, 'EditProfile'>; navigation: EditProfileNavigationProp; } @@ -65,7 +66,7 @@ const EditProfile: React.FC<EditProfileProps> = ({route, navigation}) => { const y: Animated.Value<number> = Animated.useValue(0); const {userId, username} = route.params; const { - profile: {website, biography, gender, snapchat, tiktok}, + profile: {website, biography, gender, snapchat, tiktok, university_class}, avatar, cover, } = useSelector((state: RootState) => state.user); @@ -99,6 +100,13 @@ const EditProfile: React.FC<EditProfileProps> = ({route, navigation}) => { isValidSnapchat: true, isValidTiktok: true, attemptedSubmit: false, + classYear: university_class, + }); + + var classYearList: Array<any> = []; + + CLASS_YEAR_LIST.map((value) => { + classYearList.push({label: value, value: value}); }); /** @@ -254,6 +262,14 @@ const EditProfile: React.FC<EditProfileProps> = ({route, navigation}) => { }); }; + const handleClassYearUpdate = (value: string) => { + const classYear = Number.parseInt(value); + setForm({ + ...form, + classYear, + }); + }; + const handleSubmit = useCallback(async () => { if (!form.largePic) { Alert.alert(ERROR_UPLOAD_LARGE_PROFILE_PIC); @@ -297,7 +313,7 @@ const EditProfile: React.FC<EditProfileProps> = ({route, navigation}) => { if (form.bio) { if (form.isValidBio) { - request.append('biography', form.bio); + request.append('biography', form.bio.trim()); } else { setForm({...form, attemptedSubmit: false}); setTimeout(() => setForm({...form, attemptedSubmit: true})); @@ -335,6 +351,15 @@ const EditProfile: React.FC<EditProfileProps> = ({route, navigation}) => { invalidFields = true; } + if (form.classYear !== university_class) { + if (!form.classYear) { + invalidFields = true; + Alert.alert('Please select a valid class year'); + } else { + request.append('university_class', form.classYear); + } + } + if (invalidFields) { return; } @@ -487,6 +512,19 @@ const EditProfile: React.FC<EditProfileProps> = ({route, navigation}) => { value={form.customGenderText} /> )} + + <TaggDropDown + value={form.classYear.toString()} + onValueChange={(value: string) => + handleClassYearUpdate(value) + } + items={classYearList} + placeholder={{ + label: 'Class Year', + value: null, + color: '#ddd', + }} + /> {snapchat !== '' && ( <View style={styles.row}> <SocialIcon social={'Snapchat'} style={styles.icon} /> diff --git a/src/screens/profile/IndividualMoment.tsx b/src/screens/profile/IndividualMoment.tsx index f13e1295..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} /> @@ -80,7 +77,7 @@ const IndividualMoment: React.FC<IndividualMomentProps> = ({ return ( <BlurView blurType="light" - blurAmount={10} + blurAmount={30} reducedTransparencyFallbackColor="white" style={styles.contentContainer}> <IndividualMomentTitleBar diff --git a/src/screens/profile/MomentCommentsScreen.tsx b/src/screens/profile/MomentCommentsScreen.tsx index ebe4da28..5c3b8579 100644 --- a/src/screens/profile/MomentCommentsScreen.tsx +++ b/src/screens/profile/MomentCommentsScreen.tsx @@ -1,17 +1,15 @@ -import * as React from 'react'; import {RouteProp, useNavigation} from '@react-navigation/native'; -import {ProfileStackParams} from '../../routes/main'; -import {CenteredView, CommentTile} from '../../components'; -import {CommentType} from '../../types'; -import {ScrollView, StyleSheet, Text, View} from 'react-native'; -import {SCREEN_WIDTH} from '../../utils/screenDimensions'; -import {Button} from 'react-native-elements'; +import React, {useState} from 'react'; +import {StyleSheet, Text, TouchableOpacity, View} from 'react-native'; +import {SafeAreaView} from 'react-native-safe-area-context'; +import BackIcon from '../../assets/icons/back-arrow.svg'; +import {TabsGradient} from '../../components'; import {AddComment} from '../../components/'; -import {useEffect} from 'react'; -import AsyncStorage from '@react-native-community/async-storage'; -import {getMomentComments} from '../..//services'; -import {useDispatch} from 'react-redux'; -import {logout} from '../../store/actions'; +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'; /** * Comments Screen for an image uploaded @@ -20,7 +18,7 @@ import {logout} from '../../store/actions'; */ type MomentCommentsScreenRouteProps = RouteProp< - ProfileStackParams, + MainStackParams, 'MomentCommentsScreen' >; @@ -30,109 +28,96 @@ 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(); - useEffect(() => { - const loadComments = async () => { - const token = await AsyncStorage.getItem('token'); - if (!token) { - dispatch(logout()); - return; - } - getMomentComments(moment_id, setCommentsList, token); - setNewCommentsAvailable(false); - }; - if (newCommentsAvailable) { - loadComments(); - } - }, [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 ( - <CenteredView> - <View style={styles.modalView}> + <View style={styles.background}> + <SafeAreaView> <View style={styles.header}> - <Button - title="X" - buttonStyle={styles.button} - titleStyle={styles.buttonText} + <TouchableOpacity + style={styles.headerButton} onPress={() => { navigation.pop(); - }} + }}> + <BackIcon height={'100%'} width={'100%'} color={'white'} /> + </TouchableOpacity> + <Text style={styles.headerText}>{commentsLength + ' Comments'}</Text> + </View> + <View style={styles.body}> + <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} + objectId={ + commentObjectInFocus ? commentObjectInFocus.comment_id : moment_id + } + isCommentInFocus={commentObjectInFocus ? true : false} /> - <Text style={styles.headerText}> - {commentsList.length + ' Comments'} - </Text> </View> - <ScrollView - style={styles.modalScrollView} - contentContainerStyle={styles.modalScrollViewContent}> - {commentsList && - commentsList.map((comment: CommentType) => ( - <CommentTile - key={comment.comment_id} - comment_object={comment} - screenType={screenType} - /> - ))} - </ScrollView> - <AddComment - setNewCommentsAvailable={setNewCommentsAvailable} - moment_id={moment_id} - /> - </View> - </CenteredView> + </SafeAreaView> + <TabsGradient /> + </View> ); }; const styles = StyleSheet.create({ - header: {flexDirection: 'row'}, + background: { + backgroundColor: 'white', + height: '100%', + }, + header: {justifyContent: 'center', padding: '3%'}, headerText: { - position: 'relative', - left: '180%', + position: 'absolute', alignSelf: 'center', - fontSize: 18, - fontWeight: '500', - }, - container: { - position: 'relative', - top: '5%', - left: '5%', - backgroundColor: 'white', - borderRadius: 5, - width: SCREEN_WIDTH / 1.1, - height: '55%', + fontSize: 20.5, + fontWeight: '600', }, - button: { - backgroundColor: 'transparent', + headerButton: { + width: '5%', + aspectRatio: 1, + padding: 0, + marginLeft: '5%', + alignSelf: 'flex-start', }, - buttonText: { + headerButtonText: { color: 'black', fontSize: 18, fontWeight: '400', }, - modalView: { - width: '85%', - height: '70%', - backgroundColor: '#fff', - shadowColor: '#000', - shadowOpacity: 30, - shadowOffset: {width: 0, height: 2}, - shadowRadius: 5, - borderRadius: 8, - paddingBottom: 15, + body: { + width: SCREEN_WIDTH * 0.9, + height: SCREEN_HEIGHT * 0.8, + paddingTop: '3%', + }, + scrollView: { paddingHorizontal: 20, - paddingTop: 5, - justifyContent: 'space-between', }, - modalScrollViewContent: { + scrollViewContent: { justifyContent: 'center', }, - modalScrollView: { - marginBottom: 10, - }, }); export default MomentCommentsScreen; diff --git a/src/screens/profile/MomentUploadPromptScreen.tsx b/src/screens/profile/MomentUploadPromptScreen.tsx index 6111985d..9d46c1e9 100644 --- a/src/screens/profile/MomentUploadPromptScreen.tsx +++ b/src/screens/profile/MomentUploadPromptScreen.tsx @@ -6,6 +6,7 @@ import CloseIcon from '../../assets/ionicons/close-outline.svg'; import {StyleSheet, Text, View} from 'react-native'; import {Moment} from '../../components'; import {Image} from 'react-native-animatable'; +import {UPLOAD_MOMENT_PROMPT_ONE_MESSAGE} from '../../constants/strings'; type MomentUploadPromptScreenRouteProp = RouteProp< MainStackParams, @@ -38,10 +39,7 @@ const MomentUploadPromptScreen: React.FC<MomentUploadPromptScreenProps> = ({ }} /> - <Text style={styles.text}> - Post your first moment to {'\n'} continue building your digital {'\n'}{' '} - identity! - </Text> + <Text style={styles.text}>{UPLOAD_MOMENT_PROMPT_ONE_MESSAGE}</Text> <Image source={require('../../assets/gifs/dotted-arrow-white.gif')} style={styles.arrowGif} @@ -54,6 +52,8 @@ const MomentUploadPromptScreen: React.FC<MomentUploadPromptScreenProps> = ({ screenType={screenType} handleMomentCategoryDelete={() => {}} shouldAllowDeletion={false} + showDownButton={false} + showUpButton={false} externalStyles={{ container: styles.momentContainer, titleText: styles.momentHeaderText, diff --git a/src/screens/search/SearchScreen.tsx b/src/screens/search/SearchScreen.tsx index 9f98b4d7..f0be7c9e 100644 --- a/src/screens/search/SearchScreen.tsx +++ b/src/screens/search/SearchScreen.tsx @@ -20,7 +20,7 @@ import { SearchResultsBackground, TabsGradient, } from '../../components'; -import {SEARCH_ENDPOINT, TAGG_TEXT_LIGHT_BLUE} from '../../constants'; +import {SEARCH_ENDPOINT, TAGG_LIGHT_BLUE} from '../../constants'; import {loadRecentlySearched, resetScreenType} from '../../store/actions'; import {RootState} from '../../store/rootReducer'; import {ProfilePreviewType, ScreenType, UserType} from '../../types'; @@ -36,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>>( @@ -46,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(); @@ -69,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: { @@ -213,7 +206,7 @@ const styles = StyleSheet.create({ clear: { fontSize: 17, fontWeight: 'bold', - color: TAGG_TEXT_LIGHT_BLUE, + color: TAGG_LIGHT_BLUE, }, image: { width: SCREEN_WIDTH, 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..4f9fb47a --- /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: true, + 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 735f2ed2..2354d18e 100644 --- a/src/services/MomentServices.ts +++ b/src/services/MomentService.ts @@ -1,101 +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, - token: string, -) => { - try { - const response = await fetch(COMMENTS_ENDPOINT + '?moment_id=' + momentId, { - method: 'GET', - headers: { - Authorization: 'Token ' + token, - }, - }); - const status = response.status; - if (status === 200) { - const comments = await response.json(); - callback(comments); - } else { - console.log('Could not load comments'); - } - } catch (error) { - console.log('Could not load comments', error); - } -}; - -//Post a comment on a moment -export const postMomentComment = async ( - commenter: string, - comment: string, - momentId: string, - token: string, -) => { - try { - const request = new FormData(); - request.append('moment_id', momentId); - request.append('commenter', commenter); - request.append('comment', comment); - const response = await fetch(COMMENTS_ENDPOINT, { - method: 'POST', - headers: { - Authorization: 'Token ' + token, - }, - body: request, - }); - if (response.status !== 200) { - throw 'server error'; - } - return await response.json(); - } catch (error) { - Alert.alert(ERROR_FAILED_TO_COMMENT); - return {}; - } -}; - -//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, @@ -192,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/UserProfileService.ts b/src/services/UserProfileService.ts index 75d7d367..b400843d 100644 --- a/src/services/UserProfileService.ts +++ b/src/services/UserProfileService.ts @@ -164,12 +164,8 @@ export const loadRecentlySearchedUsers = async () => { export const handlePasswordResetRequest = async (value: string) => { try { - const token = await AsyncStorage.getItem('token'); const response = await fetch(PASSWORD_RESET_ENDPOINT + 'request/', { method: 'POST', - headers: { - Authorization: 'Token ' + token, - }, body: JSON.stringify({ value, }), @@ -204,12 +200,8 @@ export const handlePasswordCodeVerification = async ( otp: string, ) => { try { - const token = await AsyncStorage.getItem('token'); const response = await fetch(PASSWORD_RESET_ENDPOINT + 'verify/', { method: 'POST', - headers: { - Authorization: 'Token ' + token, - }, body: JSON.stringify({ value, otp, @@ -239,12 +231,8 @@ export const handlePasswordCodeVerification = async ( export const handlePasswordReset = async (value: string, password: string) => { try { - const token = await AsyncStorage.getItem('token'); const response = await fetch(PASSWORD_RESET_ENDPOINT + 'reset/', { method: 'POST', - headers: { - Authorization: 'Token ' + token, - }, body: JSON.stringify({ value, password, 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..1775cd5f 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' @@ -170,7 +180,9 @@ export type NotificationType = { actor: ProfilePreviewType; verbage: string; notification_type: 'DFT' | 'FRD_REQ' | 'FRD_ACPT' | 'FRD_DEC' | 'CMT'; - notification_object: CommentType | undefined; + notification_object: CommentType | CommentThreadType | undefined; timestamp: string; unread: boolean; }; + +export type TypeOfComment = 'Comment' | 'Thread'; diff --git a/src/utils/index.ts b/src/utils/index.ts index f5352af1..629a0091 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,5 +1,4 @@ -export * from './screenDimensions'; -export * from './statusBarHeight'; +export * from './layouts'; export * from './moments'; export * from './common'; export * from './users'; diff --git a/src/utils/layouts.ts b/src/utils/layouts.ts new file mode 100644 index 00000000..e2f1f0b1 --- /dev/null +++ b/src/utils/layouts.ts @@ -0,0 +1,48 @@ +import {PixelRatio, Platform, StatusBar} from 'react-native'; +import {Dimensions} from 'react-native'; + +export const {width: SCREEN_WIDTH, height: SCREEN_HEIGHT} = Dimensions.get( + 'window', +); + +export const SCREEN_RATIO = SCREEN_HEIGHT / SCREEN_WIDTH; + +/** + * Working as of Q1 2021, latest iPhone is 12 + * iPhone 8/SE has a logical screen ratio of about 1.77 + * Rest has a logical screen ratio of about 2.16 + */ +export const isIPhoneX = () => + Platform.OS === 'ios' && !Platform.isPad && !Platform.isTVOS + ? SCREEN_RATIO > 2 + : false; + +// Taken from: https://github.com/react-navigation/react-navigation/issues/283 +export const HeaderHeight = Platform.select({ + ios: 44, + android: 56, + default: 64, +}); + +export const StatusBarHeight = Platform.select({ + ios: isIPhoneX() ? 44 : 20, + android: StatusBar.currentHeight, + default: 0, +}); + +export const AvatarHeaderHeight = (HeaderHeight + StatusBarHeight) * 1.3; + +/** + * This is a function for normalizing the font size for different devices, based on iphone 8. + * + * E.g. font size 13 on an iphone 8 is 13, but on an iPhone 11 is + * 14.5 + */ +export const normalize = (fontSize: number) => { + // based on iphone 8 logical screen width + const scale = SCREEN_WIDTH / 375; + let newSize = fontSize * scale; + // round to the nearest 0.5 + newSize = Math.round(PixelRatio.roundToNearestPixel(newSize) * 2) / 2; + return newSize; +}; diff --git a/src/utils/screenDimensions.ts b/src/utils/screenDimensions.ts deleted file mode 100644 index 56277ddc..00000000 --- a/src/utils/screenDimensions.ts +++ /dev/null @@ -1,6 +0,0 @@ -import {Dimensions} from 'react-native'; - -const {width, height} = Dimensions.get('window'); - -export const SCREEN_WIDTH = width; -export const SCREEN_HEIGHT = height; diff --git a/src/utils/statusBarHeight.ts b/src/utils/statusBarHeight.ts deleted file mode 100644 index b8eb7b33..00000000 --- a/src/utils/statusBarHeight.ts +++ /dev/null @@ -1,28 +0,0 @@ -import {Platform, StatusBar} from 'react-native'; -import {SCREEN_HEIGHT, SCREEN_WIDTH} from './screenDimensions'; - -const X_WIDTH = 375; -const X_HEIGHT = 812; -const XSMAX_WIDTH = 414; -const XSMAX_HEIGHT = 896; - -export const isIPhoneX = () => - Platform.OS === 'ios' && !Platform.isPad && !Platform.isTVOS - ? (SCREEN_WIDTH === X_WIDTH && SCREEN_HEIGHT === X_HEIGHT) || - (SCREEN_WIDTH === XSMAX_WIDTH && SCREEN_HEIGHT === XSMAX_HEIGHT) - : false; - -// Taken from: https://github.com/react-navigation/react-navigation/issues/283 -export const HeaderHeight = Platform.select({ - ios: 44, - android: 56, - default: 64, -}); - -export const StatusBarHeight = Platform.select({ - ios: isIPhoneX() ? 44 : 20, - android: StatusBar.currentHeight, - default: 0, -}); - -export const AvatarHeaderHeight = (HeaderHeight + StatusBarHeight) * 1.3; |