aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ios/Frontend/Info.plist2
-rw-r--r--src/App.tsx12
-rw-r--r--src/assets/icons/back-arrow-colored.svg1
-rw-r--r--src/assets/images/welcome.pngbin304406 -> 415560 bytes
-rw-r--r--src/assets/ionicons/trash-outline.svg1
-rw-r--r--src/components/comments/AddComment.tsx67
-rw-r--r--src/components/comments/CommentTile.tsx261
-rw-r--r--src/components/comments/CommentsContainer.tsx171
-rw-r--r--src/components/common/TaggDatePicker.tsx2
-rw-r--r--src/components/common/TaggSquareButton.tsx79
-rw-r--r--src/components/common/index.ts1
-rw-r--r--src/components/moments/MomentPostContent.tsx8
-rw-r--r--src/components/moments/MomentTile.tsx3
-rw-r--r--src/components/notifications/Notification.tsx210
-rw-r--r--src/components/onboarding/BirthDatePicker.tsx2
-rw-r--r--src/components/profile/FriendsCount.tsx6
-rw-r--r--src/components/profile/ProfileBody.tsx16
-rw-r--r--src/components/profile/ProfilePreview.tsx42
-rw-r--r--src/components/search/Explore.tsx7
-rw-r--r--src/components/search/ExploreSection.tsx15
-rw-r--r--src/components/search/ExploreSectionUser.tsx18
-rw-r--r--src/constants/api.ts1
-rw-r--r--src/constants/constants.ts4
-rw-r--r--src/constants/strings.ts6
-rw-r--r--src/routes/Routes.tsx8
-rw-r--r--src/routes/main/MainStackNavigator.tsx1
-rw-r--r--src/screens/main/NotificationsScreen.tsx2
-rw-r--r--src/screens/onboarding/CategorySelection.tsx3
-rw-r--r--src/screens/onboarding/Login.tsx109
-rw-r--r--src/screens/onboarding/WelcomeScreen.tsx31
-rw-r--r--src/screens/profile/IndividualMoment.tsx11
-rw-r--r--src/screens/profile/MomentCommentsScreen.tsx86
-rw-r--r--src/screens/search/SearchScreen.tsx10
-rw-r--r--src/services/CommentService.ts118
-rw-r--r--src/services/CommonService.ts22
-rw-r--r--src/services/ExploreService.ts (renamed from src/services/ExploreServices.ts)1
-rw-r--r--src/services/MomentService.ts (renamed from src/services/MomentServices.ts)95
-rw-r--r--src/services/UserFriendsService.ts (renamed from src/services/UserFriendsServices.ts)0
-rw-r--r--src/services/index.ts8
-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.ts38
43 files changed, 1045 insertions, 457 deletions
diff --git a/ios/Frontend/Info.plist b/ios/Frontend/Info.plist
index 2483dee8..0d75ca35 100644
--- a/ios/Frontend/Info.plist
+++ b/ios/Frontend/Info.plist
@@ -55,7 +55,7 @@
<key>NSMainNibFile</key>
<string>LaunchScreen</string>
<key>NSPhotoLibraryUsageDescription</key>
- <string>Tagg would like to access your photos.</string>
+ <string>This lets you share photos from your library and select profile displays</string>
<key>UIAppFonts</key>
<array>
<string>Feather.ttf</string>
diff --git a/src/App.tsx b/src/App.tsx
index 18fadf64..ea3617dc 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,17 +1,11 @@
-import React, {useEffect} from 'react';
import {NavigationContainer} from '@react-navigation/native';
-import Routes from './routes';
+import React from 'react';
import {Provider} from 'react-redux';
-import store from './store/configureStore';
-import {fcmService} from './services/FCMService';
import {navigationRef} from './RootNavigation';
+import Routes from './routes';
+import store from './store/configureStore';
const App = () => {
- useEffect(() => {
- fcmService.setUpPushNotifications();
- // TODO: If permissions are not there, deactivateFcmService
- });
-
return (
/**
* This is the provider from the redux store, it acts as the root provider for our application
diff --git a/src/assets/icons/back-arrow-colored.svg b/src/assets/icons/back-arrow-colored.svg
new file mode 100644
index 00000000..123426d7
--- /dev/null
+++ b/src/assets/icons/back-arrow-colored.svg
@@ -0,0 +1 @@
+<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 385.86 696.76"><defs><style>.cls-1{fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round;stroke-width:77.17px;}</style></defs><polyline class="cls-1" points="347.28 38.58 38.58 351.69 347.28 658.17"/></svg> \ No newline at end of file
diff --git a/src/assets/images/welcome.png b/src/assets/images/welcome.png
index 46ab4f9f..62b46077 100644
--- a/src/assets/images/welcome.png
+++ b/src/assets/images/welcome.png
Binary files differ
diff --git a/src/assets/ionicons/trash-outline.svg b/src/assets/ionicons/trash-outline.svg
new file mode 100644
index 00000000..4920b56a
--- /dev/null
+++ b/src/assets/ionicons/trash-outline.svg
@@ -0,0 +1 @@
+<svg xmlns='http://www.w3.org/2000/svg' class='ionicon' viewBox='0 0 512 512'><title>Trash</title><path d='M112 112l20 320c.95 18.49 14.4 32 32 32h184c17.67 0 30.87-13.51 32-32l20-320' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='32'/><path stroke='currentColor' stroke-linecap='round' stroke-miterlimit='10' stroke-width='32' d='M80 112h352'/><path d='M192 112V72h0a23.93 23.93 0 0124-24h80a23.93 23.93 0 0124 24h0v40M256 176v224M184 176l8 224M328 176l-8 224' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='32'/></svg> \ No newline at end of file
diff --git a/src/components/comments/AddComment.tsx b/src/components/comments/AddComment.tsx
index 24b3473c..56011f05 100644
--- a/src/components/comments/AddComment.tsx
+++ b/src/components/comments/AddComment.tsx
@@ -1,4 +1,4 @@
-import React, {useEffect} from 'react';
+import React, {useEffect, useRef} from 'react';
import {
Image,
Keyboard,
@@ -8,10 +8,11 @@ import {
View,
} from 'react-native';
import {TextInput, TouchableOpacity} from 'react-native-gesture-handler';
-import {useSelector} from 'react-redux';
+import {useDispatch, useSelector} from 'react-redux';
import UpArrowIcon from '../../assets/icons/up_arrow.svg';
import {TAGG_LIGHT_BLUE} from '../../constants';
-import {postMomentComment} from '../../services';
+import {postComment} from '../../services';
+import {updateReplyPosted} from '../../store/actions';
import {RootState} from '../../store/rootreducer';
import {SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils';
@@ -23,30 +24,48 @@ import {SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils';
export interface AddCommentProps {
setNewCommentsAvailable: Function;
- moment_id: string;
+ objectId: string;
+ placeholderText: string;
+ isCommentInFocus: boolean;
}
const AddComment: React.FC<AddCommentProps> = ({
setNewCommentsAvailable,
- moment_id,
+ objectId,
+ placeholderText,
+ isCommentInFocus,
}) => {
const [comment, setComment] = React.useState('');
const [keyboardVisible, setKeyboardVisible] = React.useState(false);
- const {
- avatar,
- user: {userId},
- } = useSelector((state: RootState) => state.user);
+ const {avatar} = useSelector((state: RootState) => state.user);
+ const dispatch = useDispatch();
- const postComment = async () => {
- const postedComment = await postMomentComment(
- userId,
- comment.trim(),
- moment_id,
+ const addComment = async () => {
+ const trimmed = comment.trim();
+ if (trimmed === '') {
+ return;
+ }
+ const postedComment = await postComment(
+ trimmed,
+ objectId,
+ isCommentInFocus,
);
if (postedComment) {
setComment('');
+
+ //Set new reply posted object
+ //This helps us show the latest reply on top
+ //Data set is kind of stale but it works
+ if (isCommentInFocus) {
+ dispatch(
+ updateReplyPosted({
+ comment_id: postedComment.comment_id,
+ parent_comment: {comment_id: objectId},
+ }),
+ );
+ }
setNewCommentsAvailable(true);
}
};
@@ -63,6 +82,15 @@ const AddComment: React.FC<AddCommentProps> = ({
return () => Keyboard.removeListener('keyboardWillHide', hideKeyboard);
}, []);
+ const ref = useRef<TextInput>(null);
+
+ //If a comment is in Focus, bring the keyboard up so user is able to type in a reply
+ useEffect(() => {
+ if (isCommentInFocus) {
+ ref.current?.focus();
+ }
+ }, [isCommentInFocus]);
+
return (
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
@@ -70,7 +98,7 @@ const AddComment: React.FC<AddCommentProps> = ({
<View
style={[
styles.container,
- keyboardVisible ? {backgroundColor: '#fff'} : {},
+ keyboardVisible ? styles.whiteBackround : {},
]}>
<View style={styles.textContainer}>
<Image
@@ -83,15 +111,16 @@ const AddComment: React.FC<AddCommentProps> = ({
/>
<TextInput
style={styles.text}
- placeholder="Add a comment..."
+ placeholder={placeholderText}
placeholderTextColor="grey"
onChangeText={setComment}
value={comment}
autoCorrect={false}
multiline={true}
+ ref={ref}
/>
<View style={styles.submitButton}>
- <TouchableOpacity style={styles.submitButton} onPress={postComment}>
+ <TouchableOpacity style={styles.submitButton} onPress={addComment}>
<UpArrowIcon width={35} height={35} color={'white'} />
</TouchableOpacity>
</View>
@@ -100,6 +129,7 @@ const AddComment: React.FC<AddCommentProps> = ({
</KeyboardAvoidingView>
);
};
+
const styles = StyleSheet.create({
container: {
backgroundColor: '#f7f7f7',
@@ -140,6 +170,9 @@ const styles = StyleSheet.create({
marginVertical: '2%',
alignSelf: 'flex-end',
},
+ whiteBackround: {
+ backgroundColor: '#fff',
+ },
});
export default AddComment;
diff --git a/src/components/comments/CommentTile.tsx b/src/components/comments/CommentTile.tsx
index 47f25a53..be113523 100644
--- a/src/components/comments/CommentTile.tsx
+++ b/src/components/comments/CommentTile.tsx
@@ -1,10 +1,21 @@
-import React from 'react';
+/* eslint-disable radix */
+import React, {Fragment, useEffect, useRef, useState} from 'react';
import {Text, View} from 'react-native-animatable';
import {ProfilePreview} from '../profile';
-import {CommentType, ScreenType} from '../../types';
-import {StyleSheet} from 'react-native';
-import {getTimePosted} from '../../utils';
+import {CommentType, ScreenType, TypeOfComment} from '../../types';
+import {Alert, Animated, StyleSheet} from 'react-native';
import ClockIcon from '../../assets/icons/clock-icon-01.svg';
+import {TAGG_LIGHT_BLUE} from '../../constants';
+import {RectButton, TouchableOpacity} from 'react-native-gesture-handler';
+import {getTimePosted, normalize, SCREEN_WIDTH} from '../../utils';
+import Arrow from '../../assets/icons/back-arrow-colored.svg';
+import Trash from '../../assets/ionicons/trash-outline.svg';
+import CommentsContainer from './CommentsContainer';
+import Swipeable from 'react-native-gesture-handler/Swipeable';
+import {deleteComment, getCommentsCount} from '../../services';
+import {ERROR_FAILED_TO_DELETE_COMMENT} from '../../constants/strings';
+import {useSelector} from 'react-redux';
+import {RootState} from '../../store/rootReducer';
/**
* Displays users's profile picture, comment posted by them and the time difference between now and when a comment was posted.
@@ -13,54 +24,205 @@ import ClockIcon from '../../assets/icons/clock-icon-01.svg';
interface CommentTileProps {
comment_object: CommentType;
screenType: ScreenType;
+ typeOfComment: TypeOfComment;
+ setCommentObjectInFocus?: (comment: CommentType | undefined) => void;
+ newCommentsAvailable: boolean;
+ setNewCommentsAvailable: (available: boolean) => void;
+ canDelete: boolean;
}
const CommentTile: React.FC<CommentTileProps> = ({
comment_object,
screenType,
+ typeOfComment,
+ setCommentObjectInFocus,
+ newCommentsAvailable,
+ setNewCommentsAvailable,
+ canDelete,
}) => {
const timePosted = getTimePosted(comment_object.date_created);
+ const [showReplies, setShowReplies] = useState<boolean>(false);
+ const [showKeyboard, setShowKeyboard] = useState<boolean>(false);
+ const [newThreadAvailable, setNewThreadAvailable] = useState(true);
+ const swipeRef = useRef<Swipeable>(null);
+ const isThread = typeOfComment === 'Thread';
+
+ const {replyPosted} = useSelector((state: RootState) => state.user);
+
+ /**
+ * Bubbling up, for handling a new comment in a thread.
+ */
+ useEffect(() => {
+ if (newCommentsAvailable) {
+ setNewThreadAvailable(true);
+ }
+ }, [newCommentsAvailable]);
+
+ useEffect(() => {
+ if (replyPosted && typeOfComment === 'Comment') {
+ if (replyPosted.parent_comment.comment_id === comment_object.comment_id) {
+ setShowReplies(true);
+ }
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [replyPosted]);
+
+ /**
+ * Case : A COMMENT IS IN FOCUS && REPLY SECTION IS HIDDEN
+ * Bring the current comment to focus
+ * Case : No COMMENT IS IN FOCUS && REPLY SECTION IS SHOWN
+ * Unfocus comment in focus
+ */
+ const toggleAddComment = () => {
+ //Do not allow user to reply to a thread
+ if (!isThread) {
+ if (setCommentObjectInFocus) {
+ if (!showKeyboard) {
+ setCommentObjectInFocus(comment_object);
+ } else {
+ setCommentObjectInFocus(undefined);
+ }
+ }
+ setShowKeyboard(!showKeyboard);
+ }
+ };
+
+ const toggleReplies = async () => {
+ if (showReplies) {
+ //To update count of replies in case we deleted a reply
+ comment_object.replies_count = parseInt(
+ await getCommentsCount(comment_object.comment_id, true),
+ );
+ }
+ setNewThreadAvailable(true);
+ setShowReplies(!showReplies);
+ };
+
+ /**
+ * Method to compute text to be shown for replies button
+ */
+ const getRepliesText = () =>
+ showReplies
+ ? 'Hide'
+ : comment_object.replies_count > 0
+ ? `Replies (${comment_object.replies_count})`
+ : 'Replies';
+
+ const renderRightAction = (text: string, color: string, progress) => {
+ const pressHandler = async () => {
+ swipeRef.current?.close();
+ const success = await deleteComment(comment_object.comment_id, isThread);
+ if (success) {
+ setNewCommentsAvailable(true);
+ } else {
+ Alert.alert(ERROR_FAILED_TO_DELETE_COMMENT);
+ }
+ };
+ return (
+ <Animated.View>
+ <RectButton
+ style={[styles.rightAction, {backgroundColor: color}]}
+ onPress={pressHandler}>
+ <Trash width={normalize(25)} height={normalize(25)} color={'white'} />
+ <Text style={styles.actionText}>{text}</Text>
+ </RectButton>
+ </Animated.View>
+ );
+ };
+
+ const renderRightActions = (progress: Animated.AnimatedInterpolation) =>
+ canDelete ? (
+ <View style={styles.swipeActions}>
+ {renderRightAction('Delete', '#c42634', progress)}
+ </View>
+ ) : (
+ <Fragment />
+ );
+
return (
- <View style={styles.container}>
- <ProfilePreview
- profilePreview={{
- id: comment_object.commenter.id,
- username: comment_object.commenter.username,
- first_name: comment_object.commenter.first_name,
- last_name: comment_object.commenter.last_name,
- }}
- previewType={'Comment'}
- screenType={screenType}
- />
- <View style={styles.body}>
- <Text style={styles.comment}>{comment_object.comment}</Text>
- <View style={styles.clockIconAndTime}>
- <ClockIcon style={styles.clockIcon} />
- <Text style={styles.date_time}>{' ' + timePosted}</Text>
- </View>
+ <Swipeable
+ ref={swipeRef}
+ renderRightActions={renderRightActions}
+ rightThreshold={40}
+ friction={2}
+ containerStyle={styles.swipableContainer}>
+ <View
+ style={[styles.container, isThread ? styles.moreMarginWithThread : {}]}>
+ <ProfilePreview
+ profilePreview={comment_object.commenter}
+ previewType={'Comment'}
+ screenType={screenType}
+ />
+ <TouchableOpacity style={styles.body} onPress={toggleAddComment}>
+ <Text style={styles.comment}>{comment_object.comment}</Text>
+ <View style={styles.clockIconAndTime}>
+ <ClockIcon style={styles.clockIcon} />
+ <Text style={styles.date_time}>{' ' + timePosted}</Text>
+ <View style={styles.flexer} />
+ </View>
+ </TouchableOpacity>
+ {/*** Show replies text only if there are some replies present */}
+ {typeOfComment === 'Comment' && comment_object.replies_count > 0 && (
+ <TouchableOpacity
+ style={styles.repliesTextAndIconContainer}
+ onPress={toggleReplies}>
+ <Text style={styles.repliesText}>{getRepliesText()}</Text>
+ <Arrow
+ width={12}
+ height={11}
+ color={TAGG_LIGHT_BLUE}
+ style={
+ !showReplies ? styles.repliesDownArrow : styles.repliesUpArrow
+ }
+ />
+ </TouchableOpacity>
+ )}
</View>
- </View>
+
+ {/*** Show replies if toggle state is true */}
+ {showReplies && (
+ <View>
+ <CommentsContainer
+ objectId={comment_object.comment_id}
+ screenType={screenType}
+ setNewCommentsAvailable={setNewThreadAvailable}
+ newCommentsAvailable={newThreadAvailable}
+ typeOfComment={'Thread'}
+ commentId={replyPosted?.comment_id}
+ />
+ </View>
+ )}
+ </Swipeable>
);
};
const styles = StyleSheet.create({
container: {
- marginLeft: '3%',
- marginRight: '3%',
borderBottomWidth: 1,
borderColor: 'lightgray',
- marginBottom: '3%',
+ backgroundColor: 'white',
+ flexDirection: 'column',
+ flex: 1,
+ paddingTop: '3%',
+ paddingBottom: '5%',
+ marginLeft: '7%',
+ },
+ swipeActions: {
+ flexDirection: 'row',
+ },
+ moreMarginWithThread: {
+ marginLeft: '14%',
},
body: {
marginLeft: 56,
},
comment: {
- position: 'relative',
- top: -5,
marginBottom: '2%',
+ marginRight: '2%',
},
date_time: {
color: 'gray',
+ fontSize: normalize(12),
},
clockIcon: {
width: 12,
@@ -69,7 +231,50 @@ const styles = StyleSheet.create({
},
clockIconAndTime: {
flexDirection: 'row',
- marginBottom: '3%',
+ marginTop: '3%',
+ },
+ flexer: {
+ flex: 1,
+ },
+ repliesTextAndIconContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginTop: '5%',
+ marginLeft: 56,
+ },
+ repliesText: {
+ color: TAGG_LIGHT_BLUE,
+ fontWeight: '500',
+ fontSize: normalize(12),
+ marginRight: '1%',
+ },
+ repliesBody: {
+ width: SCREEN_WIDTH,
+ },
+ repliesDownArrow: {
+ transform: [{rotate: '270deg'}],
+ marginTop: '1%',
+ },
+ repliesUpArrow: {
+ transform: [{rotate: '90deg'}],
+ marginTop: '1%',
+ },
+ actionText: {
+ color: 'white',
+ fontSize: normalize(12),
+ fontWeight: '500',
+ backgroundColor: 'transparent',
+ paddingHorizontal: '5%',
+ marginTop: '5%',
+ },
+ rightAction: {
+ alignItems: 'center',
+ flex: 1,
+ justifyContent: 'center',
+ flexDirection: 'column',
+ },
+ swipableContainer: {
+ backgroundColor: 'white',
},
});
diff --git a/src/components/comments/CommentsContainer.tsx b/src/components/comments/CommentsContainer.tsx
new file mode 100644
index 00000000..c72da2b7
--- /dev/null
+++ b/src/components/comments/CommentsContainer.tsx
@@ -0,0 +1,171 @@
+import React, {useEffect, useRef, useState} from 'react';
+import {StyleSheet} from 'react-native';
+import {FlatList} from 'react-native-gesture-handler';
+import {useDispatch, useSelector} from 'react-redux';
+import {CommentTile} from '.';
+import {getComments} from '../../services';
+import {updateReplyPosted} from '../../store/actions';
+import {RootState} from '../../store/rootReducer';
+import {CommentType, ScreenType, TypeOfComment} from '../../types';
+import {SCREEN_HEIGHT} from '../../utils';
+export type CommentsContainerProps = {
+ screenType: ScreenType;
+ //objectId can be either moment_id or comment_id
+ objectId: string;
+ commentId?: string;
+ setCommentsLength?: (count: number) => void;
+ newCommentsAvailable: boolean;
+ setNewCommentsAvailable: (value: boolean) => void;
+ typeOfComment: TypeOfComment;
+ setCommentObjectInFocus?: (comment: CommentType | undefined) => void;
+ commentObjectInFocus?: CommentType;
+};
+
+/**
+ * Comments Container to be used for both comments and replies
+ */
+
+const CommentsContainer: React.FC<CommentsContainerProps> = ({
+ screenType,
+ objectId,
+ setCommentsLength,
+ newCommentsAvailable,
+ setNewCommentsAvailable,
+ typeOfComment,
+ setCommentObjectInFocus,
+ commentObjectInFocus,
+ commentId,
+}) => {
+ const {username: loggedInUsername} = useSelector(
+ (state: RootState) => state.user.user,
+ );
+ const [commentsList, setCommentsList] = useState<CommentType[]>([]);
+ const dispatch = useDispatch();
+ const ref = useRef<FlatList<CommentType>>(null);
+
+ useEffect(() => {
+ const loadComments = async () => {
+ await getComments(objectId, typeOfComment === 'Thread').then(
+ (comments) => {
+ if (comments && subscribedToLoadComments) {
+ setCommentsList(comments);
+ if (setCommentsLength) {
+ setCommentsLength(comments.length);
+ }
+ setNewCommentsAvailable(false);
+ }
+ },
+ );
+ };
+ let subscribedToLoadComments = true;
+ if (newCommentsAvailable) {
+ loadComments();
+ }
+ return () => {
+ subscribedToLoadComments = false;
+ };
+ }, [
+ dispatch,
+ objectId,
+ newCommentsAvailable,
+ setNewCommentsAvailable,
+ setCommentsLength,
+ typeOfComment,
+ ]);
+
+ // eslint-disable-next-line no-shadow
+ const swapCommentTo = (commentId: string, toIndex: number) => {
+ const index = commentsList.findIndex(
+ (item) => item.comment_id === commentId,
+ );
+ if (index > 0) {
+ let comments = [...commentsList];
+ const temp = comments[index];
+ comments[index] = comments[toIndex];
+ comments[toIndex] = temp;
+ setCommentsList(comments);
+ }
+ };
+
+ useEffect(() => {
+ //Scroll only if a new comment and not a reply was posted
+ const shouldScroll = () =>
+ typeOfComment === 'Comment' && !commentObjectInFocus;
+
+ const performAction = () => {
+ if (commentId) {
+ swapCommentTo(commentId, 0);
+ } else if (shouldScroll()) {
+ setTimeout(() => {
+ ref.current?.scrollToEnd({animated: true});
+ }, 500);
+ }
+ };
+ if (commentsList) {
+ //Bring the relevant comment to top if a comment id is present else scroll if necessary
+ performAction();
+ }
+
+ //Clean up the reply id present in store
+ return () => {
+ if (commentId && typeOfComment === 'Thread') {
+ setTimeout(() => {
+ dispatch(updateReplyPosted(undefined));
+ }, 200);
+ }
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [commentsList, commentId]);
+
+ //WIP : TODO : Bring the comment in focus above the keyboard
+ // useEffect(() => {
+ // if (commentObjectInFocus && commentsList.length >= 3) {
+ // swapCommentTo(commentObjectInFocus.comment_id, 2);
+ // }
+ // // eslint-disable-next-line react-hooks/exhaustive-deps
+ // }, [commentObjectInFocus]);
+
+ const ITEM_HEIGHT = SCREEN_HEIGHT / 7.0;
+
+ const renderComment = ({item}: {item: CommentType}) => (
+ <CommentTile
+ key={item.comment_id}
+ comment_object={item}
+ screenType={screenType}
+ typeOfComment={typeOfComment}
+ setCommentObjectInFocus={setCommentObjectInFocus}
+ newCommentsAvailable={newCommentsAvailable}
+ setNewCommentsAvailable={setNewCommentsAvailable}
+ canDelete={item.commenter.username === loggedInUsername}
+ />
+ );
+
+ return (
+ <FlatList
+ data={commentsList}
+ ref={ref}
+ keyExtractor={(item, index) => index.toString()}
+ decelerationRate={'fast'}
+ snapToAlignment={'start'}
+ snapToInterval={ITEM_HEIGHT}
+ renderItem={renderComment}
+ showsVerticalScrollIndicator={false}
+ contentContainerStyle={styles.scrollViewContent}
+ getItemLayout={(data, index) => ({
+ length: ITEM_HEIGHT,
+ offset: ITEM_HEIGHT * index,
+ index,
+ })}
+ pagingEnabled
+ />
+ );
+};
+
+const styles = StyleSheet.create({
+ scrollView: {},
+ scrollViewContent: {
+ justifyContent: 'center',
+ },
+});
+
+export default CommentsContainer;
diff --git a/src/components/common/TaggDatePicker.tsx b/src/components/common/TaggDatePicker.tsx
index 059bf620..f929b41d 100644
--- a/src/components/common/TaggDatePicker.tsx
+++ b/src/components/common/TaggDatePicker.tsx
@@ -12,7 +12,7 @@ interface TaggDatePickerProps {
const TaggDatePicker: React.FC<TaggDatePickerProps> = (props) => {
const [date, setDate] = useState(
props.date
- ? new Date(moment(props.date).add(1, 'day').format('YYYY-MM-DD'))
+ ? new Date(moment(props.date).add(1, 'day').format('MM-DD-YYYY'))
: undefined,
);
return (
diff --git a/src/components/common/TaggSquareButton.tsx b/src/components/common/TaggSquareButton.tsx
new file mode 100644
index 00000000..4fe61b95
--- /dev/null
+++ b/src/components/common/TaggSquareButton.tsx
@@ -0,0 +1,79 @@
+import React from 'react';
+import {
+ GestureResponderEvent,
+ StyleSheet,
+ Text,
+ TouchableOpacity,
+ ViewProps,
+ ViewStyle,
+} from 'react-native';
+import {normalize, SCREEN_WIDTH} from '../../utils';
+
+interface TaggSquareButtonProps extends ViewProps {
+ onPress: (event: GestureResponderEvent) => void;
+ title: string;
+ mode: 'normal' | 'large';
+ color: 'purple' | 'white';
+ style?: ViewStyle;
+}
+
+const TaggSquareButton: React.FC<TaggSquareButtonProps> = (props) => {
+ const buttonStyles = (() => {
+ switch (props.color) {
+ case 'purple':
+ return {backgroundColor: '#8F01FF'};
+ case 'white':
+ default:
+ return {backgroundColor: 'white'};
+ }
+ })();
+ switch (props.mode) {
+ case 'large':
+ return (
+ <TouchableOpacity
+ onPress={props.onPress}
+ style={[styles.largeButton, buttonStyles, props.style]}>
+ <Text style={styles.largeLabel}>{props.title}</Text>
+ </TouchableOpacity>
+ );
+ case 'normal':
+ default:
+ return (
+ <TouchableOpacity
+ onPress={props.onPress}
+ style={[styles.normalButton, buttonStyles, props.style]}>
+ <Text style={styles.normalLabel}>{props.title}</Text>
+ </TouchableOpacity>
+ );
+ }
+};
+
+const styles = StyleSheet.create({
+ largeButton: {
+ justifyContent: 'center',
+ alignItems: 'center',
+ width: '70%',
+ height: '10%',
+ borderRadius: 5,
+ },
+ largeLabel: {
+ fontSize: normalize(26),
+ fontWeight: '500',
+ color: '#eee',
+ },
+ normalButton: {
+ justifyContent: 'center',
+ alignItems: 'center',
+ width: SCREEN_WIDTH * 0.45,
+ aspectRatio: 3.7,
+ borderRadius: 5,
+ marginBottom: '5%',
+ },
+ normalLabel: {
+ fontSize: normalize(20),
+ fontWeight: '500',
+ color: '#78A0EF',
+ },
+});
+
+export default TaggSquareButton;
diff --git a/src/components/common/index.ts b/src/components/common/index.ts
index 61c7fa26..a5718c1e 100644
--- a/src/components/common/index.ts
+++ b/src/components/common/index.ts
@@ -20,3 +20,4 @@ export {default as GenericMoreInfoDrawer} from './GenericMoreInfoDrawer';
export {default as TaggPopUp} from './TaggPopup';
export {default as TaggPrompt} from './TaggPrompt';
export {default as AcceptDeclineButtons} from './AcceptDeclineButtons';
+export {default as TaggSquareButton} from './TaggSquareButton';
diff --git a/src/components/moments/MomentPostContent.tsx b/src/components/moments/MomentPostContent.tsx
index 508b6d9f..d68ceaa3 100644
--- a/src/components/moments/MomentPostContent.tsx
+++ b/src/components/moments/MomentPostContent.tsx
@@ -1,6 +1,6 @@
import React, {useEffect} from 'react';
import {Image, StyleSheet, Text, View, ViewProps} from 'react-native';
-import {getMomentCommentsCount} from '../../services';
+import {getCommentsCount} from '../../services';
import {ScreenType} from '../../types';
import {getTimePosted, SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils';
import {CommentsCount} from '../comments';
@@ -24,8 +24,12 @@ const MomentPostContent: React.FC<MomentPostContentProps> = ({
const [comments_count, setCommentsCount] = React.useState('');
useEffect(() => {
+ const fetchCommentsCount = async () => {
+ const count = await getCommentsCount(momentId, false);
+ setCommentsCount(count);
+ };
setElapsedTime(getTimePosted(dateTime));
- getMomentCommentsCount(momentId, setCommentsCount);
+ fetchCommentsCount();
}, [dateTime, momentId]);
return (
diff --git a/src/components/moments/MomentTile.tsx b/src/components/moments/MomentTile.tsx
index 16e91c82..69701192 100644
--- a/src/components/moments/MomentTile.tsx
+++ b/src/components/moments/MomentTile.tsx
@@ -15,7 +15,6 @@ const MomentTile: React.FC<MomentTileProps> = ({
}) => {
const navigation = useNavigation();
- const {path_hash} = moment;
return (
<TouchableOpacity
onPress={() => {
@@ -26,7 +25,7 @@ const MomentTile: React.FC<MomentTileProps> = ({
});
}}>
<View style={styles.image}>
- <Image style={styles.image} source={{uri: path_hash}} />
+ <Image style={styles.image} source={{uri: moment.thumbnail_url}} />
</View>
</TouchableOpacity>
);
diff --git a/src/components/notifications/Notification.tsx b/src/components/notifications/Notification.tsx
index e6d16f82..951a5bf6 100644
--- a/src/components/notifications/Notification.tsx
+++ b/src/components/notifications/Notification.tsx
@@ -1,25 +1,32 @@
import {useNavigation} from '@react-navigation/native';
import React, {useEffect, useState} from 'react';
-import {Image, StyleSheet, Text, View} from 'react-native';
-import {Button} from 'react-native-elements';
+import {Alert, Image, StyleSheet, Text, View} from 'react-native';
import {TouchableWithoutFeedback} from 'react-native-gesture-handler';
+import LinearGradient from 'react-native-linear-gradient';
import {useDispatch, useStore} from 'react-redux';
+import {BACKGROUND_GRADIENT_MAP} from '../../constants';
+import {ERROR_DELETED_OBJECT} from '../../constants/strings';
import {
+ loadImageFromURL,
+ loadMoments,
+ loadMomentThumbnail,
+} from '../../services';
+import {
+ acceptFriendRequest,
declineFriendRequest,
loadUserNotifications,
+ updateReplyPosted,
updateUserXFriends,
} from '../../store/actions';
-import {acceptFriendRequest} from '../../store/actions';
-import {NotificationType, ProfilePreviewType, ScreenType, MomentType} from '../../types';
+import {RootState} from '../../store/rootReducer';
+import {MomentType, NotificationType, ScreenType} from '../../types';
import {
fetchUserX,
+ getTokenOrLogout,
SCREEN_HEIGHT,
- SCREEN_WIDTH,
userXInStore,
} from '../../utils';
import AcceptDeclineButtons from '../common/AcceptDeclineButtons';
-import {loadAvatar, loadMomentThumbnail} from '../../services';
-
interface NotificationProps {
item: NotificationType;
@@ -30,7 +37,7 @@ interface NotificationProps {
const Notification: React.FC<NotificationProps> = (props) => {
const {
item: {
- actor: {id, username, first_name, last_name},
+ actor: {id, username, first_name, last_name, thumbnail_url},
verbage,
notification_type,
notification_object,
@@ -44,22 +51,30 @@ const Notification: React.FC<NotificationProps> = (props) => {
const state: RootState = useStore().getState();
const dispatch = useDispatch();
- const [avatarURI, setAvatarURI] = useState<string | undefined>(undefined);
+ const [avatar, setAvatar] = useState<string | undefined>(undefined);
const [momentURI, setMomentURI] = useState<string | undefined>(undefined);
- const backgroundColor = unread ? '#DCF1F1' : 'rgba(0,0,0,0)';
+ const [onTapLoadProfile, setOnTapLoadProfile] = useState<boolean>(false);
+
useEffect(() => {
- let mounted = true;
- const loadAvatarImage = async (user_id: string) => {
- const response = await loadAvatar(user_id, true);
- if (mounted) {
- setAvatarURI(response);
+ (async () => {
+ const response = await loadImageFromURL(thumbnail_url);
+ if (response) {
+ setAvatar(response);
+ } else {
+ setAvatar(undefined);
}
- };
- loadAvatarImage(id);
+ })();
+ }, []);
+
+ useEffect(() => {
+ if (onTapLoadProfile) {
+ fetchUserX(dispatch, {userId: id, username: username}, screenType);
+ }
return () => {
- mounted = false;
+ setOnTapLoadProfile(false);
};
- }, [id]);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [onTapLoadProfile]);
useEffect(() => {
let mounted = true;
@@ -67,10 +82,22 @@ const Notification: React.FC<NotificationProps> = (props) => {
const response = await loadMomentThumbnail(moment_id);
if (mounted && response) {
setMomentURI(response);
+ } else {
+ // if not set to empty, it will re-use the previous notification's state
+ setMomentURI(undefined);
}
};
- if (notification_type === 'CMT' && notification_object) {
- loadMomentImage(notification_object.moment_id);
+ if (
+ (notification_type === 'CMT' ||
+ notification_type === 'MOM_3+' ||
+ notification_type === 'MOM_FRIEND') &&
+ notification_object
+ ) {
+ loadMomentImage(
+ notification_object.moment_id
+ ? notification_object.moment_id
+ : notification_object.parent_comment.moment_id,
+ );
return () => {
mounted = false;
};
@@ -94,31 +121,85 @@ const Notification: React.FC<NotificationProps> = (props) => {
});
break;
case 'CMT':
- // find the moment we need to display
- const moment = loggedInUserMoments?.find(
- (m) => m.moment_id === notification_object?.moment_id,
+ //Notification object is set to null if the comment / comment_thread / moment gets deleted
+ if (!notification_object) {
+ Alert.alert(ERROR_DELETED_OBJECT);
+ break;
+ }
+ let {moment_id} = notification_object;
+ let {comment_id} = notification_object;
+
+ //If this is a thread, get comment_id and moment_id from parent_comment
+ if (!notification_object?.moment_id) {
+ moment_id = notification_object?.parent_comment?.moment_id;
+ comment_id = notification_object?.parent_comment?.comment_id;
+ }
+
+ // Now find the moment we need to display
+ let moment: MomentType | undefined = loggedInUserMoments?.find(
+ (m) => m.moment_id === moment_id,
);
+ let userXId;
+
+ // If moment does not belong to the logged in user, then the comment was probably a reply to logged in user's comment
+ // on userX's moment
+ // Load moments for userX
+ if (!moment) {
+ let moments: MomentType[] = [];
+ try {
+ //Populate local state in the mean time
+ setOnTapLoadProfile(true);
+ const token = await getTokenOrLogout(dispatch);
+ moments = await loadMoments(id, token);
+ } catch (err) {
+ console.log(err);
+ }
+ moment = moments?.find((m) => m.moment_id === moment_id);
+ userXId = id;
+ }
+
+ //Now if moment was found, navigate to the respective moment
if (moment) {
+ if (notification_object?.parent_comment) {
+ dispatch(updateReplyPosted(notification_object));
+ }
navigation.push('IndividualMoment', {
moment,
- userXId: undefined, // we're only viewing our own moment here
+ userXId: userXId, // we're only viewing our own moment here
screenType,
});
setTimeout(() => {
navigation.push('MomentCommentsScreen', {
- moment_id: moment.moment_id,
+ moment_id: moment_id,
screenType,
+ comment_id: comment_id,
});
}, 500);
}
break;
+ case 'MOM_3+':
+ case 'MOM_FRIEND':
+ const object = notification_object as MomentType;
+ await fetchUserX(
+ dispatch,
+ {userId: id, username: username},
+ screenType,
+ );
+ navigation.push('IndividualMoment', {
+ moment: object,
+ userXId: id,
+ screenType,
+ });
+ break;
default:
break;
}
};
const handleAcceptRequest = async () => {
- await dispatch(acceptFriendRequest({id, username, first_name, last_name}));
+ await dispatch(
+ acceptFriendRequest({id, username, first_name, last_name, thumbnail_url}),
+ );
await dispatch(updateUserXFriends(id, state));
dispatch(loadUserNotifications());
};
@@ -128,48 +209,57 @@ const Notification: React.FC<NotificationProps> = (props) => {
dispatch(loadUserNotifications());
};
- return (
- <>
- <TouchableWithoutFeedback
- style={[styles.container, {backgroundColor}]}
- onPress={onNotificationTap}>
- <View style={styles.avatarContainer}>
- <Image
- style={styles.avatar}
- source={
- avatarURI
- ? {uri: avatarURI, cache: 'only-if-cached'}
- : require('../../assets/images/avatar-placeholder.png')
- }
+ const renderContent = () => (
+ <TouchableWithoutFeedback
+ style={styles.container}
+ onPress={onNotificationTap}>
+ <View style={styles.avatarContainer}>
+ <Image
+ style={styles.avatar}
+ source={
+ avatar
+ ? {uri: avatar}
+ : require('../../assets/images/avatar-placeholder.png')
+ }
+ />
+ </View>
+ <View style={styles.contentContainer}>
+ <Text style={styles.actorName}>
+ {first_name} {last_name}
+ </Text>
+ <Text>{verbage}</Text>
+ </View>
+ {notification_type === 'FRD_REQ' && (
+ <View style={styles.buttonsContainer}>
+ <AcceptDeclineButtons
+ requester={{id, username, first_name, last_name}}
+ onAccept={handleAcceptRequest}
+ onReject={handleDeclineFriendRequest}
/>
</View>
- <View style={styles.contentContainer}>
- <Text style={styles.actorName}>
- {first_name} {last_name}
- </Text>
- <Text>{verbage}</Text>
- </View>
- {notification_type === 'FRD_REQ' && (
- <View style={styles.buttonsContainer}>
- <AcceptDeclineButtons
- requester={{id, username, first_name, last_name}}
- onAccept={handleAcceptRequest}
- onReject={handleDeclineFriendRequest}
- />
- </View>
- )}
- {notification_type === 'CMT' && notification_object && (
- <Image style={styles.moment} source={{uri: momentURI}} />
)}
- </TouchableWithoutFeedback>
- </>
+ {(notification_type === 'CMT' ||
+ notification_type === 'MOM_3+' ||
+ notification_type === 'MOM_FRIEND') &&
+ notification_object && (
+ <Image style={styles.moment} source={{uri: momentURI}} />
+ )}
+ </TouchableWithoutFeedback>
+ );
+
+ return unread ? (
+ <LinearGradient colors={BACKGROUND_GRADIENT_MAP[2]} useAngle angle={90}>
+ {renderContent()}
+ </LinearGradient>
+ ) : (
+ renderContent()
);
};
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
- height: SCREEN_HEIGHT / 10,
+ height: Math.round(SCREEN_HEIGHT / 10),
flex: 1,
alignItems: 'center',
},
diff --git a/src/components/onboarding/BirthDatePicker.tsx b/src/components/onboarding/BirthDatePicker.tsx
index 0fc597c3..6bef5798 100644
--- a/src/components/onboarding/BirthDatePicker.tsx
+++ b/src/components/onboarding/BirthDatePicker.tsx
@@ -45,7 +45,7 @@ const BirthDatePicker = React.forwardRef(
ref={ref}
{...props}>
{(updated || props.showPresetdate) && date
- ? moment(date).format('YYYY-MM-DD')
+ ? moment(date).format('MM-DD-YYYY')
: 'Date of Birth'}
</Text>
</TouchableOpacity>
diff --git a/src/components/profile/FriendsCount.tsx b/src/components/profile/FriendsCount.tsx
index 9647710e..851dbc3b 100644
--- a/src/components/profile/FriendsCount.tsx
+++ b/src/components/profile/FriendsCount.tsx
@@ -17,10 +17,10 @@ const FriendsCount: React.FC<FriendsCountProps> = ({
userXId,
screenType,
}) => {
- const count = (userXId
+ const {friends} = userXId
? useSelector((state: RootState) => state.userX[screenType][userXId])
- : useSelector((state: RootState) => state.friends)
- )?.friends.length;
+ : useSelector((state: RootState) => state.friends);
+ const count = friends ? friends.length : 0;
const displayedCount: string =
count < 5e3
diff --git a/src/components/profile/ProfileBody.tsx b/src/components/profile/ProfileBody.tsx
index d10e2e15..f2d75519 100644
--- a/src/components/profile/ProfileBody.tsx
+++ b/src/components/profile/ProfileBody.tsx
@@ -105,8 +105,8 @@ const ProfileBody: React.FC<ProfileBodyProps> = ({
{friendship_status === 'friends' && (
<Button
title={'Unfriend'}
- buttonStyle={styles.button}
- titleStyle={styles.buttonTitle}
+ buttonStyle={styles.requestedButton}
+ titleStyle={styles.requestedButtonTitle}
onPress={handleFriendUnfriend} // unfriend, no record status
/>
)}
@@ -176,10 +176,10 @@ const styles = StyleSheet.create({
justifyContent: 'center',
alignItems: 'center',
width: SCREEN_WIDTH * 0.4,
- height: SCREEN_WIDTH * 0.09,
+ height: SCREEN_WIDTH * 0.075,
borderColor: TAGG_LIGHT_BLUE,
- borderWidth: 3,
- borderRadius: 5,
+ borderWidth: 2,
+ borderRadius: 3,
marginRight: '2%',
padding: 0,
backgroundColor: 'transparent',
@@ -200,9 +200,11 @@ const styles = StyleSheet.create({
justifyContent: 'center',
alignItems: 'center',
width: SCREEN_WIDTH * 0.4,
- height: SCREEN_WIDTH * 0.09,
+ height: SCREEN_WIDTH * 0.075,
padding: 0,
- borderRadius: 5,
+ borderWidth: 2,
+ borderColor: TAGG_LIGHT_BLUE,
+ borderRadius: 3,
marginRight: '2%',
backgroundColor: TAGG_LIGHT_BLUE,
},
diff --git a/src/components/profile/ProfilePreview.tsx b/src/components/profile/ProfilePreview.tsx
index 389ca367..38defb8d 100644
--- a/src/components/profile/ProfilePreview.tsx
+++ b/src/components/profile/ProfilePreview.tsx
@@ -12,21 +12,11 @@ import {
} from 'react-native';
import {useDispatch, useSelector, useStore} from 'react-redux';
import {ERROR_UNABLE_TO_VIEW_PROFILE} from '../../constants/strings';
-import {loadAvatar} from '../../services';
+import {loadImageFromURL} from '../../services';
import {RootState} from '../../store/rootreducer';
-import {
- PreviewType,
- ProfilePreviewType,
- ScreenType,
- UserType,
-} from '../../types';
+import {PreviewType, ProfilePreviewType, ScreenType} from '../../types';
import {checkIfUserIsBlocked, fetchUserX, userXInStore} from '../../utils';
-const NO_USER: UserType = {
- userId: '',
- username: '',
-};
-
/**
* This component returns user's profile picture friended by username as a touchable component.
* What happens when someone clicks on this component is partly decided by the prop isComment.
@@ -43,28 +33,23 @@ interface ProfilePreviewProps extends ViewProps {
}
const ProfilePreview: React.FC<ProfilePreviewProps> = ({
- profilePreview: {username, first_name, last_name, id},
+ profilePreview: {username, first_name, last_name, id, thumbnail_url},
previewType,
screenType,
}) => {
const navigation = useNavigation();
const {user: loggedInUser} = useSelector((state: RootState) => state.user);
- const [avatarURI, setAvatarURI] = useState<string | null>(null);
- const [user, setUser] = useState<UserType>(NO_USER);
+ const [avatar, setAvatar] = useState<string | null>(null);
const dispatch = useDispatch();
+
useEffect(() => {
- let mounted = true;
- const loadAvatarImage = async () => {
- const response = await loadAvatar(id, true);
- if (mounted) {
- setAvatarURI(response);
+ (async () => {
+ const response = await loadImageFromURL(thumbnail_url);
+ if (response) {
+ setAvatar(response);
}
- };
- loadAvatarImage();
- return () => {
- mounted = false;
- };
- }, [id]);
+ })();
+ }, []);
/**
* Adds a searched user to the recently searched cache if they're tapped on.
@@ -80,6 +65,7 @@ const ProfilePreview: React.FC<ProfilePreviewProps> = ({
username,
first_name,
last_name,
+ thumbnail_url,
};
try {
@@ -211,8 +197,8 @@ const ProfilePreview: React.FC<ProfilePreviewProps> = ({
<Image
style={avatarStyle}
source={
- avatarURI
- ? {uri: avatarURI}
+ avatar
+ ? {uri: avatar}
: require('../../assets/images/avatar-placeholder.png')
}
/>
diff --git a/src/components/search/Explore.tsx b/src/components/search/Explore.tsx
index 4a71249b..2a3bc749 100644
--- a/src/components/search/Explore.tsx
+++ b/src/components/search/Explore.tsx
@@ -12,9 +12,10 @@ const Explore: React.FC = () => {
return (
<View style={styles.container}>
<Text style={styles.header}>Search Profiles</Text>
- {EXPLORE_SECTION_TITLES.map((title: ExploreSectionType) => (
- <ExploreSection key={title} title={title} users={explores[title]} />
- ))}
+ {explores &&
+ EXPLORE_SECTION_TITLES.map((title: ExploreSectionType) => (
+ <ExploreSection key={title} title={title} users={explores[title]} />
+ ))}
</View>
);
};
diff --git a/src/components/search/ExploreSection.tsx b/src/components/search/ExploreSection.tsx
index 17079e77..025c8c3c 100644
--- a/src/components/search/ExploreSection.tsx
+++ b/src/components/search/ExploreSection.tsx
@@ -1,5 +1,5 @@
import React, {Fragment} from 'react';
-import {ScrollView, StyleSheet, Text, View} from 'react-native';
+import {FlatList, StyleSheet, Text, View} from 'react-native';
import {ProfilePreviewType} from '../../types';
import {normalize} from '../../utils';
import ExploreSectionUser from './ExploreSectionUser';
@@ -17,12 +17,15 @@ const ExploreSection: React.FC<ExploreSectionProps> = ({title, users}) => {
return users.length !== 0 ? (
<View style={styles.container}>
<Text style={styles.header}>{title}</Text>
- <ScrollView horizontal showsHorizontalScrollIndicator={false}>
- <View style={styles.padding} />
- {users.map((user) => (
+ <FlatList
+ data={users}
+ ListHeaderComponent={<View style={styles.padding} />}
+ renderItem={({item: user}: {item: ProfilePreviewType}) => (
<ExploreSectionUser key={user.id} user={user} style={styles.user} />
- ))}
- </ScrollView>
+ )}
+ showsHorizontalScrollIndicator={false}
+ horizontal
+ />
</View>
) : (
<Fragment />
diff --git a/src/components/search/ExploreSectionUser.tsx b/src/components/search/ExploreSectionUser.tsx
index 68e077e3..b0cfe5c6 100644
--- a/src/components/search/ExploreSectionUser.tsx
+++ b/src/components/search/ExploreSectionUser.tsx
@@ -9,7 +9,7 @@ import {
} from 'react-native';
import LinearGradient from 'react-native-linear-gradient';
import {useDispatch, useSelector, useStore} from 'react-redux';
-import {loadAvatar} from '../../services';
+import {loadImageFromURL} from '../../services';
import {RootState} from '../../store/rootReducer';
import {ProfilePreviewType, ScreenType} from '../../types';
import {fetchUserX, normalize, userXInStore} from '../../utils';
@@ -36,18 +36,13 @@ const ExploreSectionUser: React.FC<ExploreSectionUserProps> = ({
const dispatch = useDispatch();
useEffect(() => {
- let mounted = true;
- const loadAvatarImage = async () => {
- const response = await loadAvatar(id, true);
- if (mounted) {
+ (async () => {
+ const response = await loadImageFromURL(user.thumbnail_url);
+ if (response) {
setAvatar(response);
}
- };
- loadAvatarImage();
- return () => {
- mounted = false;
- };
- }, [user]);
+ })();
+ }, []);
const handlePress = async () => {
if (!userXInStore(state, screenType, user.id)) {
@@ -63,7 +58,6 @@ const ExploreSectionUser: React.FC<ExploreSectionUserProps> = ({
screenType,
});
};
-
return (
<TouchableOpacity style={[styles.container, style]} onPress={handlePress}>
<LinearGradient
diff --git a/src/constants/api.ts b/src/constants/api.ts
index 701070eb..32631be0 100644
--- a/src/constants/api.ts
+++ b/src/constants/api.ts
@@ -30,6 +30,7 @@ export const MOMENT_CATEGORY_ENDPOINT: string = API_URL + 'moment-category/';
export const NOTIFICATIONS_ENDPOINT: string = API_URL + 'notifications/';
export const DISCOVER_ENDPOINT: string = API_URL + 'discover/';
export const WAITLIST_USER_ENDPOINT: string = API_URL + 'waitlist-user/';
+export const COMMENT_THREAD_ENDPOINT: string = API_URL + 'reply/';
// Register as FCM device
export const FCM_ENDPOINT: string = API_URL + 'fcm/';
diff --git a/src/constants/constants.ts b/src/constants/constants.ts
index ad43c337..7fcc457f 100644
--- a/src/constants/constants.ts
+++ b/src/constants/constants.ts
@@ -130,6 +130,10 @@ export const BACKGROUND_GRADIENT_MAP: Record<
> = {
[BackgroundGradientType.Light]: ['#9F00FF', '#27EAE9'],
[BackgroundGradientType.Dark]: ['#421566', '#385D5E'],
+ [BackgroundGradientType.Notification]: [
+ 'rgba(143, 1, 255, 0.5)',
+ 'rgba(110, 231, 231, 0.5)',
+ ],
};
export const CLASS_YEAR_LIST: Array<string> = [
diff --git a/src/constants/strings.ts b/src/constants/strings.ts
index a1793658..9680320a 100644
--- a/src/constants/strings.ts
+++ b/src/constants/strings.ts
@@ -1,18 +1,22 @@
+
/* eslint-disable */
// Below is the regex to convert this into a csv for the Google Sheet
// export const (.*) = .*?(['|"|`])(.*)\2;
// replace with: $1\t$3
+export const ADD_COMMENT_TEXT = (username?: string) => username ? `Reply to ${username}` : 'Add a comment...'
export const COMING_SOON_MSG = 'Creating more fun things for you, surprises coming soon 😉';
export const ERROR_AUTHENTICATION = 'An error occurred during authentication. Please login again!';
export const ERROR_CATEGORY_CREATION = 'There was a problem creating your categories. Please refresh and try again.';
export const ERROR_CATEGORY_UPDATE = 'There was a problem updating your categories. Please refresh and try again';
export const ERROR_DELETE_CATEGORY = 'There was a problem while deleting category. Please try again';
export const ERROR_DELETE_MOMENT = 'Unable to delete moment, please try again later!';
+export const ERROR_DELETED_OBJECT = 'Oh sad! Looks like the comment / moment was deleted by the user';
export const ERROR_DOUBLE_CHECK_CONNECTION = 'Please double-check your network connection and retry';
export const ERROR_DUP_OLD_PWD = 'You may not use a previously used password';
export const ERROR_EMAIL_IN_USE = 'Email already in use, please try another one';
export const ERROR_FAILED_LOGIN_INFO = 'Login failed, please try re-entering your login information';
export const ERROR_FAILED_TO_COMMENT = 'Unable to post comment, refresh and try again!';
+export const ERROR_FAILED_TO_DELETE_COMMENT = 'Unable to delete comment, refresh and try again!';
export const ERROR_INVALID_INVITATION_CODE = 'Invitation code invalid, try again or talk to the friend that sent it 😬';
export const ERROR_INVALID_LOGIN = 'Invalid login, Please login again';
export const ERROR_INVALID_PWD_CODE = 'Looks like you have entered the wrong code, please try again';
@@ -49,4 +53,4 @@ export const UPLOAD_MOMENT_PROMPT_ONE_MESSAGE = 'Post your first moment to\n con
export const UPLOAD_MOMENT_PROMPT_THREE_HEADER = 'Continue to build your profile';
export const UPLOAD_MOMENT_PROMPT_THREE_MESSAGE = 'Continue to personalize your own digital space in\nthis community by filling your profile with\ncategories and moments!';
export const UPLOAD_MOMENT_PROMPT_TWO_HEADER = 'Create a new category';
-export const UPLOAD_MOMENT_PROMPT_TWO_MESSAGE = 'You can now create new categories \nand continue to fill your profile with moments!';
+export const UPLOAD_MOMENT_PROMPT_TWO_MESSAGE = 'You can now create new categories \nand continue to fill your profile with moments!'; \ No newline at end of file
diff --git a/src/routes/Routes.tsx b/src/routes/Routes.tsx
index a14f1576..a5383a47 100644
--- a/src/routes/Routes.tsx
+++ b/src/routes/Routes.tsx
@@ -7,6 +7,7 @@ import {userLogin} from '../utils';
import SplashScreen from 'react-native-splash-screen';
import messaging from '@react-native-firebase/messaging';
import {updateNewNotificationReceived} from '../store/actions';
+import {fcmService} from '../services';
const Routes: React.FC = () => {
const {
@@ -39,6 +40,13 @@ const Routes: React.FC = () => {
}
}, [dispatch, userId]);
+ useEffect(() => {
+ if (userId) {
+ fcmService.setUpPushNotifications();
+ fcmService.sendFcmTokenToServer();
+ }
+ });
+
return userId ? <NavigationBar /> : <Onboarding />;
};
diff --git a/src/routes/main/MainStackNavigator.tsx b/src/routes/main/MainStackNavigator.tsx
index e038d2c2..74993af9 100644
--- a/src/routes/main/MainStackNavigator.tsx
+++ b/src/routes/main/MainStackNavigator.tsx
@@ -40,6 +40,7 @@ export type MainStackParams = {
moment_id: string;
userXId: string | undefined;
screenType: ScreenType;
+ comment_id?: string;
};
FriendsListScreen: {
userXId: string | undefined;
diff --git a/src/screens/main/NotificationsScreen.tsx b/src/screens/main/NotificationsScreen.tsx
index 4bdee942..d9952aa8 100644
--- a/src/screens/main/NotificationsScreen.tsx
+++ b/src/screens/main/NotificationsScreen.tsx
@@ -70,7 +70,7 @@ const NotificationsScreen: React.FC = () => {
//Called when user leaves the screen
return () => resetNewNotificationFlag();
- }, [newNotificationReceived]),
+ }, [newNotificationReceived, dispatch, refreshNotifications]),
);
// handles storing and fetching the "previously viewed" information
diff --git a/src/screens/onboarding/CategorySelection.tsx b/src/screens/onboarding/CategorySelection.tsx
index a3acbbb7..94dd44b2 100644
--- a/src/screens/onboarding/CategorySelection.tsx
+++ b/src/screens/onboarding/CategorySelection.tsx
@@ -17,7 +17,7 @@ import {Background, MomentCategory} from '../../components';
import {MOMENT_CATEGORIES} from '../../constants';
import {ERROR_SOMETHING_WENT_WRONG} from '../../constants/strings';
import {OnboardingStackParams} from '../../routes';
-import {fcmService, postMomentCategories} from '../../services';
+import {postMomentCategories} from '../../services';
import {
updateIsOnboardedUser,
updateMomentCategories,
@@ -169,7 +169,6 @@ const CategorySelection: React.FC<CategorySelectionProps> = ({
const token = await getTokenOrLogout(dispatch);
await postMomentCategories(selectedCategories, token);
userLogin(dispatch, {userId: userId, username: username});
- fcmService.sendFcmTokenToServer();
} else {
dispatch(
updateMomentCategories(
diff --git a/src/screens/onboarding/Login.tsx b/src/screens/onboarding/Login.tsx
index 8974e000..2db039c1 100644
--- a/src/screens/onboarding/Login.tsx
+++ b/src/screens/onboarding/Login.tsx
@@ -11,16 +11,11 @@ import {
StyleSheet,
Text,
TouchableOpacity,
- View,
} from 'react-native';
import SplashScreen from 'react-native-splash-screen';
import {useDispatch} from 'react-redux';
-import {Background, SubmitButton, TaggInput} from '../../components';
-import {
- LOGIN_ENDPOINT,
- TAGG_LIGHT_PURPLE,
- usernameRegex,
-} from '../../constants';
+import {Background, TaggInput, TaggSquareButton} from '../../components';
+import {LOGIN_ENDPOINT, usernameRegex} from '../../constants';
import {
ERROR_DOUBLE_CHECK_CONNECTION,
ERROR_FAILED_LOGIN_INFO,
@@ -31,7 +26,7 @@ import {
import {OnboardingStackParams} from '../../routes/onboarding';
import {fcmService} from '../../services';
import {BackgroundGradientType, UserType} from '../../types';
-import {userLogin} from '../../utils';
+import {normalize, userLogin} from '../../utils';
type VerificationScreenRouteProp = RouteProp<OnboardingStackParams, 'Login'>;
type VerificationScreenNavigationProp = StackNavigationProp<
@@ -215,45 +210,6 @@ const Login: React.FC<LoginProps> = ({navigation}: LoginProps) => {
</TouchableOpacity>
);
- /**
- * Login screen login button.
- */
- const LoginButton = () => (
- <SubmitButton
- text="Let's Start!"
- color="#fff"
- style={styles.button}
- accessibilityLabel="Let's Start!"
- accessibilityHint="Select this after entering your tagg username and password"
- onPress={handleLogin}
- />
- );
-
- /**
- * Login screen registration prompt.
- */
- const RegistrationPrompt = () => (
- <View style={styles.newUserContainer}>
- <Text
- accessible={true}
- accessibilityLabel="New to tagg?"
- style={styles.newUser}>
- New to tagg?{' '}
- </Text>
- <TouchableOpacity
- accessibilityLabel="Get started."
- accessibilityHint="Select this if you do not have a tagg account">
- <Text
- accessible={true}
- accessibilityLabel="Get started"
- style={styles.getStarted}
- onPress={startRegistrationProcess}>
- Get started!
- </Text>
- </TouchableOpacity>
- </View>
- );
-
return (
<Background
centered
@@ -300,9 +256,19 @@ const Login: React.FC<LoginProps> = ({navigation}: LoginProps) => {
ref={inputRef}
/>
<ForgotPassword />
- <LoginButton />
+ <TaggSquareButton
+ onPress={handleLogin}
+ title={'Login'}
+ mode={'normal'}
+ color={'white'}
+ />
+ <TaggSquareButton
+ onPress={startRegistrationProcess}
+ title={'Sign up'}
+ mode={'normal'}
+ color={'purple'}
+ />
</KeyboardAvoidingView>
- <RegistrationPrompt />
</Background>
);
};
@@ -322,46 +288,17 @@ const styles = StyleSheet.create({
marginBottom: '10%',
},
forgotPassword: {
- marginTop: 10,
- marginBottom: 15,
+ alignSelf: 'flex-start',
+ marginVertical: '1%',
+ borderBottomWidth: 1,
+ paddingBottom: '1%',
+ left: '3%',
+ borderBottomColor: 'white',
+ marginBottom: '8%',
},
forgotPasswordText: {
- fontSize: 14,
+ fontSize: normalize(14),
color: '#fff',
- textDecorationLine: 'underline',
- },
- start: {
- width: 144,
- height: 36,
- justifyContent: 'center',
- alignItems: 'center',
- backgroundColor: '#fff',
- borderRadius: 18,
- marginBottom: '15%',
- },
- startDisabled: {
- backgroundColor: '#ddd',
- },
- startText: {
- fontSize: 16,
- color: '#78a0ef',
- fontWeight: 'bold',
- },
- newUserContainer: {
- flexDirection: 'row',
- color: '#fff',
- },
- newUser: {
- fontSize: 14,
- color: TAGG_LIGHT_PURPLE,
- },
- getStarted: {
- fontSize: 14,
- color: '#fff',
- textDecorationLine: 'underline',
- },
- button: {
- marginVertical: '10%',
},
});
diff --git a/src/screens/onboarding/WelcomeScreen.tsx b/src/screens/onboarding/WelcomeScreen.tsx
index 96d3f929..bfb1a127 100644
--- a/src/screens/onboarding/WelcomeScreen.tsx
+++ b/src/screens/onboarding/WelcomeScreen.tsx
@@ -1,10 +1,10 @@
+import {StackNavigationProp} from '@react-navigation/stack';
import * as React from 'react';
-import {StyleSheet, View, Text, Image, TouchableOpacity} from 'react-native';
-import {SCREEN_WIDTH} from '../../utils';
-import {Background} from '../../components';
+import {Image, StyleSheet, Text, View} from 'react-native';
+import {Background, TaggSquareButton} from '../../components';
import {OnboardingStackParams} from '../../routes';
-import {StackNavigationProp} from '@react-navigation/stack';
import {BackgroundGradientType} from '../../types';
+import {SCREEN_WIDTH} from '../../utils';
type WelcomeScreenNavigationProps = StackNavigationProp<
OnboardingStackParams,
@@ -36,9 +36,13 @@ const WelcomeScreen: React.FC<WelcomeScreenProps> = ({navigation}) => {
you are, along with all the moments that comprehensively define you!
</Text>
</View>
- <TouchableOpacity onPress={handleNext} style={styles.nextButton}>
- <Text style={styles.nextButtonLabel}>Next</Text>
- </TouchableOpacity>
+ <TaggSquareButton
+ onPress={handleNext}
+ title={'Next'}
+ mode={'large'}
+ color={'purple'}
+ style={styles.nextButton}
+ />
</Background>
);
};
@@ -75,20 +79,7 @@ const styles = StyleSheet.create({
marginHorizontal: '10%',
},
nextButton: {
- backgroundColor: '#8F01FF',
- justifyContent: 'center',
- alignItems: 'center',
- width: '70%',
- height: '10%',
- borderRadius: 5,
- borderWidth: 1,
- borderColor: '#8F01FF',
marginBottom: '15%',
},
- nextButtonLabel: {
- fontSize: 30,
- fontWeight: '500',
- color: '#ddd',
- },
});
export default WelcomeScreen;
diff --git a/src/screens/profile/IndividualMoment.tsx b/src/screens/profile/IndividualMoment.tsx
index 6b82b31c..8c1dc327 100644
--- a/src/screens/profile/IndividualMoment.tsx
+++ b/src/screens/profile/IndividualMoment.tsx
@@ -4,12 +4,12 @@ import {StackNavigationProp} from '@react-navigation/stack';
import React from 'react';
import {FlatList, StyleSheet, View} from 'react-native';
import {useSelector} from 'react-redux';
-import {ProfileStackParams} from 'src/routes/main/ProfileStack';
import {
IndividualMomentTitleBar,
MomentPostContent,
MomentPostHeader,
} from '../../components';
+import {MainStackParams} from '../../routes';
import {RootState} from '../../store/rootreducer';
import {MomentType} from '../../types';
import {SCREEN_HEIGHT, SCREEN_WIDTH, StatusBarHeight} from '../../utils';
@@ -17,12 +17,9 @@ import {SCREEN_HEIGHT, SCREEN_WIDTH, StatusBarHeight} from '../../utils';
/**
* Individual moment view opened when user clicks on a moment tile
*/
-type IndividualMomentRouteProp = RouteProp<
- ProfileStackParams,
- 'IndividualMoment'
->;
+type IndividualMomentRouteProp = RouteProp<MainStackParams, 'IndividualMoment'>;
type IndividualMomentNavigationProp = StackNavigationProp<
- ProfileStackParams,
+ MainStackParams,
'IndividualMoment'
>;
interface IndividualMomentProps {
@@ -70,7 +67,7 @@ const IndividualMoment: React.FC<IndividualMomentProps> = ({
style={styles.postContent}
momentId={item.moment_id}
caption={item.caption}
- pathHash={item.path_hash}
+ pathHash={item.moment_url}
dateTime={item.date_created}
screenType={screenType}
/>
diff --git a/src/screens/profile/MomentCommentsScreen.tsx b/src/screens/profile/MomentCommentsScreen.tsx
index 2bceafc9..5c3b8579 100644
--- a/src/screens/profile/MomentCommentsScreen.tsx
+++ b/src/screens/profile/MomentCommentsScreen.tsx
@@ -1,19 +1,13 @@
import {RouteProp, useNavigation} from '@react-navigation/native';
-import React, {useEffect, useRef} from 'react';
-import {
- ScrollView,
- StyleSheet,
- Text,
- TouchableOpacity,
- View,
-} from 'react-native';
+import React, {useState} from 'react';
+import {StyleSheet, Text, TouchableOpacity, View} from 'react-native';
import {SafeAreaView} from 'react-native-safe-area-context';
-import {useDispatch} from 'react-redux';
-import {getMomentComments} from '../..//services';
import BackIcon from '../../assets/icons/back-arrow.svg';
-import {CommentTile, TabsGradient} from '../../components';
+import {TabsGradient} from '../../components';
import {AddComment} from '../../components/';
-import {ProfileStackParams} from '../../routes/main';
+import CommentsContainer from '../../components/comments/CommentsContainer';
+import {ADD_COMMENT_TEXT} from '../../constants/strings';
+import {MainStackParams} from '../../routes/main';
import {CommentType} from '../../types';
import {SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils';
@@ -24,7 +18,7 @@ import {SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils';
*/
type MomentCommentsScreenRouteProps = RouteProp<
- ProfileStackParams,
+ MainStackParams,
'MomentCommentsScreen'
>;
@@ -34,26 +28,16 @@ interface MomentCommentsScreenProps {
const MomentCommentsScreen: React.FC<MomentCommentsScreenProps> = ({route}) => {
const navigation = useNavigation();
- const {moment_id, screenType} = route.params;
- const [commentsList, setCommentsList] = React.useState([]);
+ const {moment_id, screenType, comment_id} = route.params;
+
+ //Receives comment length from child CommentsContainer
+ const [commentsLength, setCommentsLength] = useState<number>(0);
const [newCommentsAvailable, setNewCommentsAvailable] = React.useState(true);
- const dispatch = useDispatch();
- const ref = useRef<ScrollView>(null);
- useEffect(() => {
- const loadComments = async () => {
- getMomentComments(moment_id, setCommentsList);
- setNewCommentsAvailable(false);
- };
- if (newCommentsAvailable) {
- loadComments();
- setTimeout(() => {
- ref.current?.scrollToEnd({
- animated: true,
- });
- }, 500);
- }
- }, [dispatch, moment_id, newCommentsAvailable]);
+ //Keeps track of the current comments object in focus so that the application knows which comment to post a reply to
+ const [commentObjectInFocus, setCommentObjectInFocus] = useState<
+ CommentType | undefined
+ >(undefined);
return (
<View style={styles.background}>
@@ -66,27 +50,31 @@ const MomentCommentsScreen: React.FC<MomentCommentsScreenProps> = ({route}) => {
}}>
<BackIcon height={'100%'} width={'100%'} color={'white'} />
</TouchableOpacity>
- <Text style={styles.headerText}>
- {commentsList.length + ' Comments'}
- </Text>
+ <Text style={styles.headerText}>{commentsLength + ' Comments'}</Text>
</View>
<View style={styles.body}>
- <ScrollView
- ref={ref}
- style={styles.scrollView}
- contentContainerStyle={styles.scrollViewContent}>
- {commentsList &&
- commentsList.map((comment: CommentType) => (
- <CommentTile
- key={comment.comment_id}
- comment_object={comment}
- screenType={screenType}
- />
- ))}
- </ScrollView>
+ <CommentsContainer
+ objectId={moment_id}
+ commentId={comment_id}
+ screenType={screenType}
+ setCommentsLength={setCommentsLength}
+ newCommentsAvailable={newCommentsAvailable}
+ setNewCommentsAvailable={setNewCommentsAvailable}
+ setCommentObjectInFocus={setCommentObjectInFocus}
+ commentObjectInFocus={commentObjectInFocus}
+ typeOfComment={'Comment'}
+ />
<AddComment
+ placeholderText={
+ commentObjectInFocus
+ ? ADD_COMMENT_TEXT(commentObjectInFocus.commenter.username)
+ : ADD_COMMENT_TEXT()
+ }
setNewCommentsAvailable={setNewCommentsAvailable}
- moment_id={moment_id}
+ objectId={
+ commentObjectInFocus ? commentObjectInFocus.comment_id : moment_id
+ }
+ isCommentInFocus={commentObjectInFocus ? true : false}
/>
</View>
</SafeAreaView>
@@ -120,7 +108,7 @@ const styles = StyleSheet.create({
fontWeight: '400',
},
body: {
- width: SCREEN_WIDTH,
+ width: SCREEN_WIDTH * 0.9,
height: SCREEN_HEIGHT * 0.8,
paddingTop: '3%',
},
diff --git a/src/screens/search/SearchScreen.tsx b/src/screens/search/SearchScreen.tsx
index 059bd968..f0be7c9e 100644
--- a/src/screens/search/SearchScreen.tsx
+++ b/src/screens/search/SearchScreen.tsx
@@ -2,7 +2,6 @@ import AsyncStorage from '@react-native-community/async-storage';
import {useFocusEffect} from '@react-navigation/native';
import React, {useCallback, useEffect, useState} from 'react';
import {
- Dimensions,
Keyboard,
RefreshControl,
ScrollView,
@@ -37,9 +36,7 @@ const NO_USER: UserType = {
*/
const SearchScreen: React.FC = () => {
- const {recentSearches, explores} = useSelector(
- (state: RootState) => state.taggUsers,
- );
+ const {recentSearches} = useSelector((state: RootState) => state.taggUsers);
const [query, setQuery] = useState<string>('');
const [results, setResults] = useState<Array<ProfilePreviewType>>([]);
const [recents, setRecents] = useState<Array<ProfilePreviewType>>(
@@ -47,7 +44,6 @@ const SearchScreen: React.FC = () => {
);
const [searching, setSearching] = useState(false);
const top = Animated.useValue(-SCREEN_HEIGHT);
- const [user, setUser] = useState<UserType>(NO_USER);
const [refreshing, setRefreshing] = useState<boolean>(false);
const dispatch = useDispatch();
@@ -70,10 +66,6 @@ const SearchScreen: React.FC = () => {
const loadResults = async (q: string) => {
try {
const token = await AsyncStorage.getItem('token');
- if (!token) {
- setUser(NO_USER);
- return;
- }
const response = await fetch(`${SEARCH_ENDPOINT}?query=${q}`, {
method: 'GET',
headers: {
diff --git a/src/services/CommentService.ts b/src/services/CommentService.ts
new file mode 100644
index 00000000..2faaa8db
--- /dev/null
+++ b/src/services/CommentService.ts
@@ -0,0 +1,118 @@
+import AsyncStorage from '@react-native-community/async-storage';
+import {Alert} from 'react-native';
+import {COMMENTS_ENDPOINT, COMMENT_THREAD_ENDPOINT} from '../constants';
+import {ERROR_FAILED_TO_COMMENT} from '../constants/strings';
+import {CommentType} from '../types';
+
+export const getComments = async (
+ objectId: string,
+ fetchThreads: boolean,
+): Promise<CommentType[]> => {
+ let comments: CommentType[] = [];
+ try {
+ const token = await AsyncStorage.getItem('token');
+ const endpoint = fetchThreads
+ ? COMMENT_THREAD_ENDPOINT + '?comment_id='
+ : COMMENTS_ENDPOINT + '?moment_id=';
+ const response = await fetch(endpoint + objectId, {
+ method: 'GET',
+ headers: {
+ Authorization: 'Token ' + token,
+ },
+ });
+ const status = response.status;
+ if (status === 200) {
+ comments = await response.json();
+ } else {
+ console.log('Could not load comments');
+ }
+ } catch (error) {
+ console.log('Could not load comments', error);
+ }
+ return comments;
+};
+
+export const postComment = async (
+ comment: string,
+ objectId: string,
+ postThread: boolean,
+) => {
+ try {
+ const token = await AsyncStorage.getItem('token');
+ const request = new FormData();
+ request.append('comment', comment);
+ if (postThread) {
+ request.append('comment_id', objectId);
+ } else {
+ request.append('moment_id', objectId);
+ }
+ const endpoint = postThread ? COMMENT_THREAD_ENDPOINT : COMMENTS_ENDPOINT;
+ const response = await fetch(endpoint, {
+ method: 'POST',
+ headers: {
+ Authorization: 'Token ' + token,
+ },
+ body: request,
+ });
+ if (response.status !== 200) {
+ throw 'server error';
+ }
+ return await response.json();
+ } catch (error) {
+ Alert.alert(ERROR_FAILED_TO_COMMENT);
+ return undefined;
+ }
+};
+
+//Get count of comments for a moment
+export const getCommentsCount = async (
+ objectId: string,
+ fetchThread: boolean,
+): Promise<string> => {
+ let comments_count: string = '';
+ try {
+ const token = await AsyncStorage.getItem('token');
+ const endpoint = fetchThread ? COMMENT_THREAD_ENDPOINT : COMMENTS_ENDPOINT;
+ const response = await fetch(endpoint + `${objectId}/`, {
+ method: 'GET',
+ headers: {
+ Authorization: 'Token ' + token,
+ },
+ });
+ const status = response.status;
+ if (status === 200) {
+ const response_data = await response.json();
+ comments_count = response_data.count;
+ } else {
+ console.log(
+ 'Something went wrong! 😭',
+ 'Not able to retrieve comments count',
+ );
+ }
+ } catch (error) {
+ console.log(
+ 'Something went wrong! 😭',
+ 'Not able to retrieve comments count',
+ error,
+ );
+ }
+ return comments_count;
+};
+
+export const deleteComment = async (id: string, isThread: boolean) => {
+ try {
+ const token = await AsyncStorage.getItem('token');
+ const url = isThread ? COMMENT_THREAD_ENDPOINT : COMMENTS_ENDPOINT;
+ const response = await fetch(url + `${id}/`, {
+ method: 'DELETE',
+ headers: {
+ Authorization: 'Token ' + token,
+ },
+ });
+ return response.status === 200;
+ } catch (error) {
+ console.log('Failed to delete a comment');
+ console.log(error);
+ return false;
+ }
+};
diff --git a/src/services/CommonService.ts b/src/services/CommonService.ts
new file mode 100644
index 00000000..dfbbf70e
--- /dev/null
+++ b/src/services/CommonService.ts
@@ -0,0 +1,22 @@
+import RNFetchBlob from 'rn-fetch-blob';
+
+export const loadImageFromURL = async (url: string) => {
+ try {
+ if (!url) {
+ return undefined;
+ }
+ const response = await RNFetchBlob.config({
+ fileCache: false,
+ appendExt: 'jpg',
+ }).fetch('GET', url);
+ const status = response.info().status;
+ if (status === 200) {
+ return response.path();
+ } else {
+ return undefined;
+ }
+ } catch (error) {
+ console.log(error);
+ return undefined;
+ }
+};
diff --git a/src/services/ExploreServices.ts b/src/services/ExploreService.ts
index ca4f1b69..980258be 100644
--- a/src/services/ExploreServices.ts
+++ b/src/services/ExploreService.ts
@@ -1,5 +1,4 @@
import AsyncStorage from '@react-native-community/async-storage';
-import {getDeviceToken} from 'react-native-device-info';
import {ALL_USERS_ENDPOINT, DISCOVER_ENDPOINT} from '../constants';
import {EMPTY_EXPLORE_SECTIONS} from '../store/initialStates';
import {ExploreSectionType, ProfilePreviewType} from '../types';
diff --git a/src/services/MomentServices.ts b/src/services/MomentService.ts
index 7bad6d4c..2354d18e 100644
--- a/src/services/MomentServices.ts
+++ b/src/services/MomentService.ts
@@ -1,100 +1,9 @@
import AsyncStorage from '@react-native-community/async-storage';
-import {Alert} from 'react-native';
import RNFetchBlob from 'rn-fetch-blob';
-import {
- COMMENTS_ENDPOINT,
- MOMENTS_ENDPOINT,
- MOMENT_THUMBNAIL_ENDPOINT,
-} from '../constants';
-import {ERROR_FAILED_TO_COMMENT} from '../constants/strings';
+import {MOMENTS_ENDPOINT, MOMENT_THUMBNAIL_ENDPOINT} from '../constants';
import {MomentType} from '../types';
import {checkImageUploadStatus} from '../utils';
-//Get all comments for a moment
-export const getMomentComments = async (
- momentId: string,
- callback: Function,
-) => {
- try {
- const token = await AsyncStorage.getItem('token');
- const response = await fetch(COMMENTS_ENDPOINT + '?moment_id=' + momentId, {
- method: 'GET',
- headers: {
- Authorization: 'Token ' + token,
- },
- });
- const status = response.status;
- if (status === 200) {
- const comments = await response.json();
- callback(comments);
- } else {
- console.log('Could not load comments');
- }
- } catch (error) {
- console.log('Could not load comments', error);
- }
-};
-
-export const postMomentComment = async (
- commenter: string,
- comment: string,
- momentId: string,
-) => {
- try {
- const token = await AsyncStorage.getItem('token');
- const request = new FormData();
- request.append('moment_id', momentId);
- request.append('commenter', commenter);
- request.append('comment', comment);
- const response = await fetch(COMMENTS_ENDPOINT, {
- method: 'POST',
- headers: {
- Authorization: 'Token ' + token,
- },
- body: request,
- });
- if (response.status !== 200) {
- throw 'server error';
- }
- return await response.json();
- } catch (error) {
- Alert.alert(ERROR_FAILED_TO_COMMENT);
- return undefined;
- }
-};
-
-//Get count of comments for a moment
-export const getMomentCommentsCount = async (
- momentId: string,
- callback: Function,
-) => {
- try {
- const token = await AsyncStorage.getItem('token');
- const response = await fetch(COMMENTS_ENDPOINT + `${momentId}/`, {
- method: 'GET',
- headers: {
- Authorization: 'Token ' + token,
- },
- });
- const status = response.status;
- if (status === 200) {
- const response_data = await response.json();
- callback(response_data.count);
- } else {
- console.log(
- 'Something went wrong! 😭',
- 'Not able to retrieve comments count',
- );
- }
- } catch (error) {
- console.log(
- 'Something went wrong! 😭',
- 'Not able to retrieve comments count',
- error,
- );
- }
-};
-
export const postMoment: (
fileName: string,
uri: string,
@@ -191,7 +100,7 @@ export const loadMomentThumbnail = async (momentId: string) => {
try {
const token = await AsyncStorage.getItem('token');
const response = await RNFetchBlob.config({
- fileCache: true,
+ fileCache: false,
appendExt: 'jpg',
}).fetch('GET', MOMENT_THUMBNAIL_ENDPOINT + `${momentId}/`, {
Authorization: 'Token ' + token,
diff --git a/src/services/UserFriendsServices.ts b/src/services/UserFriendsService.ts
index f2e15824..f2e15824 100644
--- a/src/services/UserFriendsServices.ts
+++ b/src/services/UserFriendsService.ts
diff --git a/src/services/index.ts b/src/services/index.ts
index 56cefddd..9c168d4f 100644
--- a/src/services/index.ts
+++ b/src/services/index.ts
@@ -1,11 +1,13 @@
export * from './UserProfileService';
export * from './SocialLinkingService';
-export * from './MomentServices';
-export * from './ExploreServices';
-export * from './UserFriendsServices';
+export * from './MomentService';
+export * from './ExploreService';
+export * from './UserFriendsService';
export * from './ReportingService';
export * from './BlockUserService';
export * from './MomentCategoryService';
export * from './NotificationService';
export * from './FCMService';
export * from './WaitlistUserService';
+export * from './CommonService';
+export * from './CommentService';
diff --git a/src/store/actions/user.ts b/src/store/actions/user.ts
index 0b1ea789..5f49a103 100644
--- a/src/store/actions/user.ts
+++ b/src/store/actions/user.ts
@@ -1,3 +1,4 @@
+import { CommentThreadType } from './../../types/types';
import {RootState} from '../rootReducer';
import {UserType} from '../../types/types';
import {loadProfileInfo, loadAvatar, loadCover} from '../../services';
@@ -9,6 +10,7 @@ import {
profileCompletionStageUpdated,
setIsOnboardedUser,
setNewNotificationReceived,
+ setReplyPosted,
} from '../reducers';
import {getTokenOrLogout} from '../../utils';
@@ -111,6 +113,21 @@ export const updateNewNotificationReceived = (
}
};
+export const updateReplyPosted = (
+ replyPosted: CommentThreadType | undefined,
+): ThunkAction<Promise<void>, RootState, unknown, Action<string>> => async (
+ dispatch,
+) => {
+ try {
+ dispatch({
+ type: setReplyPosted.type,
+ payload: {replyPosted},
+ });
+ } catch (error) {
+ console.log(error);
+ }
+};
+
export const logout = (): ThunkAction<
Promise<void>,
RootState,
diff --git a/src/store/initialStates.ts b/src/store/initialStates.ts
index 2a5b76db..8d137a5d 100644
--- a/src/store/initialStates.ts
+++ b/src/store/initialStates.ts
@@ -1,3 +1,4 @@
+import {CommentThreadType} from './../types/types';
import {
ExploreSectionType,
MomentType,
@@ -44,6 +45,7 @@ export const NO_USER_DATA = {
cover: <string | null>'',
isOnboardedUser: false,
newNotificationReceived: false,
+ replyPosted: <CommentThreadType | undefined>undefined,
};
export const NO_FRIENDS_DATA = {
diff --git a/src/store/reducers/userReducer.ts b/src/store/reducers/userReducer.ts
index ce497677..1e575339 100644
--- a/src/store/reducers/userReducer.ts
+++ b/src/store/reducers/userReducer.ts
@@ -53,6 +53,10 @@ const userDataSlice = createSlice({
setNewNotificationReceived: (state, action) => {
state.newNotificationReceived = action.payload.newNotificationReceived;
},
+
+ setReplyPosted: (state, action) => {
+ state.replyPosted = action.payload.replyPosted;
+ },
},
});
@@ -63,5 +67,6 @@ export const {
profileCompletionStageUpdated,
setIsOnboardedUser,
setNewNotificationReceived,
+ setReplyPosted,
} = userDataSlice.actions;
export const userDataReducer = userDataSlice.reducer;
diff --git a/src/types/types.ts b/src/types/types.ts
index d9d0b56b..f1ba12f4 100644
--- a/src/types/types.ts
+++ b/src/types/types.ts
@@ -11,6 +11,7 @@ export interface ProfilePreviewType {
username: string;
first_name: string;
last_name: string;
+ thumbnail_url: string;
}
export type FriendshipStatusType = 'friends' | 'requested' | 'no_record';
@@ -83,17 +84,26 @@ export interface MomentType {
caption: string;
date_created: string;
moment_category: string;
- path_hash: string;
+ moment_url: string;
+ thumbnail_url: string;
}
-export interface CommentType {
+export interface CommentBaseType {
comment_id: string;
comment: string;
date_created: string;
- moment_id: string;
commenter: ProfilePreviewType;
}
+export interface CommentType extends CommentBaseType {
+ moment_id: string;
+ replies_count: number;
+}
+
+export interface CommentThreadType extends CommentBaseType {
+ parent_comment: CommentType;
+}
+
export type PreviewType =
| 'Comment'
| 'Search'
@@ -155,6 +165,7 @@ export enum CategorySelectionScreenType {
export enum BackgroundGradientType {
Light,
Dark,
+ Notification
}
/**
@@ -169,8 +180,25 @@ export type TaggPopupType = {
export type NotificationType = {
actor: ProfilePreviewType;
verbage: string;
- notification_type: 'DFT' | 'FRD_REQ' | 'FRD_ACPT' | 'FRD_DEC' | 'CMT';
- notification_object: CommentType | undefined;
+ notification_type: TypeOfNotification;
+ notification_object: CommentType | CommentThreadType | MomentType | undefined;
timestamp: string;
unread: boolean;
};
+
+export type TypeOfComment = 'Comment' | 'Thread';
+export type TypeOfNotification =
+ // notification_object is undefined
+ | 'DFT'
+ // notification_object is undefined
+ | 'FRD_REQ'
+ // notification_object is undefined
+ | 'FRD_ACPT'
+ // notification_object is undefined
+ | 'FRD_DEC'
+ // notification_object is CommentType || CommentThreadType
+ | 'CMT'
+ // notification_object is MomentType
+ | 'MOM_3+'
+ // notification_object is MomentType
+ | 'MOM_FRIEND';