aboutsummaryrefslogtreecommitdiff
path: root/src/components/comments
diff options
context:
space:
mode:
authorIvan Chen <ivan@tagg.id>2021-02-01 16:01:03 -0500
committerIvan Chen <ivan@tagg.id>2021-02-01 16:01:03 -0500
commit8d1013e86cf2d66671c337d49a80da157802ad86 (patch)
tree656b1656068bb6636919359d4faaf7051994ff74 /src/components/comments
parent951d85348acef13ec7830629205c30ad5f766bee (diff)
parent7a09cc96bf1fe468a612bb44362bbef24fccc773 (diff)
Merge branch 'master' into TMA-546-Onboarding-Page
Diffstat (limited to 'src/components/comments')
-rw-r--r--src/components/comments/AddComment.tsx198
-rw-r--r--src/components/comments/CommentTile.tsx261
-rw-r--r--src/components/comments/CommentsContainer.tsx171
3 files changed, 534 insertions, 96 deletions
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;