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