aboutsummaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/comments/AddComment.tsx4
-rw-r--r--src/components/comments/CommentTile.tsx133
-rw-r--r--src/components/comments/CommentsContainer.tsx1
-rw-r--r--src/components/comments/MentionInputControlled.tsx195
-rw-r--r--src/components/common/LikeButton.tsx38
-rw-r--r--src/components/common/index.ts1
-rw-r--r--src/components/profile/Friends.tsx97
7 files changed, 336 insertions, 133 deletions
diff --git a/src/components/comments/AddComment.tsx b/src/components/comments/AddComment.tsx
index 9cf10b5e..befaa8fe 100644
--- a/src/components/comments/AddComment.tsx
+++ b/src/components/comments/AddComment.tsx
@@ -7,7 +7,6 @@ import {
TextInput,
View,
} from 'react-native';
-import {MentionInput} from 'react-native-controlled-mentions';
import {TouchableOpacity} from 'react-native-gesture-handler';
import {useDispatch, useSelector} from 'react-redux';
import UpArrowIcon from '../../assets/icons/up_arrow.svg';
@@ -20,6 +19,7 @@ import {CommentThreadType, CommentType} from '../../types';
import {SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils';
import {mentionPartTypes} from '../../utils/comments';
import {Avatar} from '../common';
+import {MentionInputControlled} from './MentionInputControlled';
export interface AddCommentProps {
momentId: string;
@@ -112,7 +112,7 @@ const AddComment: React.FC<AddCommentProps> = ({momentId, placeholderText}) => {
]}>
<View style={styles.textContainer}>
<Avatar style={styles.avatar} uri={avatar} />
- <MentionInput
+ <MentionInputControlled
containerStyle={styles.text}
placeholder={placeholderText}
value={inReplyToMention + comment}
diff --git a/src/components/comments/CommentTile.tsx b/src/components/comments/CommentTile.tsx
index ecdb4c30..ee32f889 100644
--- a/src/components/comments/CommentTile.tsx
+++ b/src/components/comments/CommentTile.tsx
@@ -11,7 +11,11 @@ import Trash from '../../assets/ionicons/trash-outline.svg';
import {TAGG_LIGHT_BLUE} from '../../constants';
import {ERROR_FAILED_TO_DELETE_COMMENT} from '../../constants/strings';
import {CommentContext} from '../../screens/profile/MomentCommentsScreen';
-import {deleteComment, getCommentsCount} from '../../services';
+import {
+ deleteComment,
+ getCommentsCount,
+ handleLikeUnlikeComment,
+} from '../../services';
import {RootState} from '../../store/rootReducer';
import {
CommentThreadType,
@@ -19,13 +23,9 @@ import {
ScreenType,
UserType,
} from '../../types';
-import {
- getTimePosted,
- navigateToProfile,
- normalize,
- SCREEN_WIDTH,
-} from '../../utils';
+import {getTimePosted, navigateToProfile, normalize} from '../../utils';
import {mentionPartTypes, renderTextWithMentions} from '../../utils/comments';
+import {LikeButton} from '../common';
import {ProfilePreview} from '../profile';
import CommentsContainer from './CommentsContainer';
@@ -55,6 +55,7 @@ const CommentTile: React.FC<CommentTileProps> = ({
const [showReplies, setShowReplies] = useState<boolean>(false);
const [showKeyboard, setShowKeyboard] = useState<boolean>(false);
const [shouldUpdateChild, setShouldUpdateChild] = useState(true);
+ const [liked, setLiked] = useState(commentObject.user_reaction !== null);
const swipeRef = useRef<Swipeable>(null);
const {replyPosted} = useSelector((state: RootState) => state.user);
const state: RootState = useStore().getState();
@@ -100,7 +101,7 @@ const CommentTile: React.FC<CommentTileProps> = ({
showReplies
? 'Hide'
: comment.replies_count > 0
- ? `Replies (${comment.replies_count})`
+ ? `Replies (${comment.replies_count}) `
: 'Replies';
const renderRightAction = (text: string, color: string) => {
@@ -143,11 +144,19 @@ const CommentTile: React.FC<CommentTileProps> = ({
containerStyle={styles.swipableContainer}>
<View
style={[styles.container, isThread ? styles.moreMarginWithThread : {}]}>
- <ProfilePreview
- profilePreview={commentObject.commenter}
- previewType={'Comment'}
- screenType={screenType}
- />
+ <View style={styles.commentHeaderContainer}>
+ <ProfilePreview
+ profilePreview={commentObject.commenter}
+ previewType={'Comment'}
+ screenType={screenType}
+ />
+ <LikeButton
+ liked={liked}
+ setLiked={setLiked}
+ onPress={() => handleLikeUnlikeComment(commentObject, liked)}
+ style={styles.likeButton}
+ />
+ </View>
<TouchableOpacity style={styles.body} onPress={toggleAddComment}>
{renderTextWithMentions({
value: commentObject.comment,
@@ -156,33 +165,53 @@ const CommentTile: React.FC<CommentTileProps> = ({
onPress: (user: UserType) =>
navigateToProfile(state, dispatch, navigation, screenType, user),
})}
- <View style={styles.clockIconAndTime}>
- <ClockIcon style={styles.clockIcon} />
- <Text style={styles.date_time}>{' ' + timePosted}</Text>
- <View style={styles.flexer} />
+ <View style={styles.commentInfoContainer}>
+ <View style={styles.row}>
+ <ClockIcon style={styles.clockIcon} />
+ <Text style={styles.date_time}>{' ' + timePosted}</Text>
+ </View>
+ <View style={styles.row}>
+ <TouchableOpacity
+ style={styles.row}
+ disabled={commentObject.reaction_count === 0}
+ onPress={() => {
+ navigation.navigate('CommentReactionScreen', {
+ comment: commentObject,
+ screenType: screenType,
+ });
+ }}>
+ <Text style={[styles.date_time, styles.likeCount]}>
+ {commentObject.user_reaction !== null
+ ? commentObject.reaction_count + (liked ? 0 : -1)
+ : commentObject.reaction_count + (liked ? 1 : 0)}
+ </Text>
+ <Text style={styles.date_time}>Likes</Text>
+ </TouchableOpacity>
+ {/* Show replies text only if there are some replies present */}
+ {!isThread && (commentObject as CommentType).replies_count > 0 && (
+ <TouchableOpacity
+ style={styles.repliesTextAndIconContainer}
+ onPress={toggleReplies}>
+ <Text style={styles.repliesText}>
+ {getRepliesText(commentObject as CommentType)}
+ </Text>
+ <Arrow
+ width={12}
+ height={11}
+ color={TAGG_LIGHT_BLUE}
+ style={
+ !showReplies
+ ? styles.repliesDownArrow
+ : styles.repliesUpArrow
+ }
+ />
+ </TouchableOpacity>
+ )}
+ </View>
</View>
</TouchableOpacity>
- {/*** Show replies text only if there are some replies present */}
- {!isThread && (commentObject as CommentType).replies_count > 0 && (
- <TouchableOpacity
- style={styles.repliesTextAndIconContainer}
- onPress={toggleReplies}>
- <Text style={styles.repliesText}>
- {getRepliesText(commentObject as CommentType)}
- </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 */}
+ {/* Show replies if toggle state is true */}
{showReplies && (
<View>
<CommentsContainer
@@ -206,8 +235,8 @@ const styles = StyleSheet.create({
flexDirection: 'column',
flex: 1,
paddingTop: '3%',
- paddingBottom: '5%',
- marginLeft: '7%',
+ marginLeft: '5%',
+ paddingBottom: '2%',
},
swipeActions: {
flexDirection: 'row',
@@ -215,6 +244,14 @@ const styles = StyleSheet.create({
moreMarginWithThread: {
marginLeft: '14%',
},
+ commentHeaderContainer: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ },
+ likeButton: {
+ marginRight: 10,
+ },
body: {
marginLeft: 56,
},
@@ -231,18 +268,23 @@ const styles = StyleSheet.create({
height: 12,
alignSelf: 'center',
},
- clockIconAndTime: {
+ commentInfoContainer: {
flexDirection: 'row',
marginTop: '3%',
+ justifyContent: 'space-between',
+ alignItems: 'center',
},
- flexer: {
- flex: 1,
+ likeCount: {
+ color: 'black',
+ marginRight: 5,
+ },
+ row: {
+ flexDirection: 'row',
},
repliesTextAndIconContainer: {
flexDirection: 'row',
alignItems: 'center',
- marginTop: '5%',
- marginLeft: 56,
+ paddingLeft: 10,
},
repliesText: {
color: TAGG_LIGHT_BLUE,
@@ -250,9 +292,6 @@ const styles = StyleSheet.create({
fontSize: normalize(12),
marginRight: '1%',
},
- repliesBody: {
- width: SCREEN_WIDTH,
- },
repliesDownArrow: {
transform: [{rotate: '270deg'}],
marginTop: '1%',
diff --git a/src/components/comments/CommentsContainer.tsx b/src/components/comments/CommentsContainer.tsx
index 0bfd5ad6..595ec743 100644
--- a/src/components/comments/CommentsContainer.tsx
+++ b/src/components/comments/CommentsContainer.tsx
@@ -136,7 +136,6 @@ const CommentsContainer: React.FC<CommentsContainerProps> = ({
};
const styles = StyleSheet.create({
- scrollView: {},
scrollViewContent: {
justifyContent: 'center',
},
diff --git a/src/components/comments/MentionInputControlled.tsx b/src/components/comments/MentionInputControlled.tsx
new file mode 100644
index 00000000..6abcb566
--- /dev/null
+++ b/src/components/comments/MentionInputControlled.tsx
@@ -0,0 +1,195 @@
+import React, {FC, MutableRefObject, useMemo, useRef, useState} from 'react';
+import {
+ NativeSyntheticEvent,
+ Text,
+ TextInput,
+ TextInputSelectionChangeEventData,
+ View,
+} from 'react-native';
+
+import {
+ MentionInputProps,
+ MentionPartType,
+ Suggestion,
+} from 'react-native-controlled-mentions/dist/types';
+import {
+ defaultMentionTextStyle,
+ generateValueFromPartsAndChangedText,
+ generateValueWithAddedSuggestion,
+ getMentionPartSuggestionKeywords,
+ isMentionPartType,
+ parseValue,
+} from 'react-native-controlled-mentions/dist/utils';
+
+const MentionInputControlled: FC<MentionInputProps> = ({
+ value,
+ onChange,
+
+ partTypes = [],
+
+ inputRef: propInputRef,
+
+ containerStyle,
+
+ onSelectionChange,
+
+ ...textInputProps
+}) => {
+ const textInput = useRef<TextInput | null>(null);
+
+ const [selection, setSelection] = useState({start: 0, end: 0});
+
+ const [keyboardText, setKeyboardText] = useState<string>('');
+
+ const validRegex = () => {
+ if (partTypes.length === 0) {
+ return /.*\@[^ ]*$/;
+ } else {
+ return new RegExp(`.*\@${keywordByTrigger[partTypes[0].trigger]}.*$`);
+ }
+ };
+
+ const {plainText, parts} = useMemo(() => parseValue(value, partTypes), [
+ value,
+ partTypes,
+ ]);
+
+ const handleSelectionChange = (
+ event: NativeSyntheticEvent<TextInputSelectionChangeEventData>,
+ ) => {
+ setSelection(event.nativeEvent.selection);
+
+ onSelectionChange && onSelectionChange(event);
+ };
+
+ /**
+ * Callback that trigger on TextInput text change
+ *
+ * @param changedText
+ */
+ const onChangeInput = (changedText: string) => {
+ setKeyboardText(changedText);
+ onChange(
+ generateValueFromPartsAndChangedText(parts, plainText, changedText),
+ );
+ };
+
+ /**
+ * We memoize the keyword to know should we show mention suggestions or not
+ */
+ const keywordByTrigger = useMemo(() => {
+ return getMentionPartSuggestionKeywords(
+ parts,
+ plainText,
+ selection,
+ partTypes,
+ );
+ }, [parts, plainText, selection, partTypes]);
+
+ /**
+ * Callback on mention suggestion press. We should:
+ * - Get updated value
+ * - Trigger onChange callback with new value
+ */
+ const onSuggestionPress = (mentionType: MentionPartType) => (
+ suggestion: Suggestion,
+ ) => {
+ const newValue = generateValueWithAddedSuggestion(
+ parts,
+ mentionType,
+ plainText,
+ selection,
+ suggestion,
+ );
+
+ if (!newValue) {
+ return;
+ }
+
+ onChange(newValue);
+
+ /**
+ * Move cursor to the end of just added mention starting from trigger string and including:
+ * - Length of trigger string
+ * - Length of mention name
+ * - Length of space after mention (1)
+ *
+ * Not working now due to the RN bug
+ */
+ // const newCursorPosition = currentPart.position.start + triggerPartIndex + trigger.length +
+ // suggestion.name.length + 1;
+
+ // textInput.current?.setNativeProps({selection: {start: newCursorPosition, end: newCursorPosition}});
+ };
+
+ const handleTextInputRef = (ref: TextInput) => {
+ textInput.current = ref as TextInput;
+
+ if (propInputRef) {
+ if (typeof propInputRef === 'function') {
+ propInputRef(ref);
+ } else {
+ (propInputRef as MutableRefObject<TextInput>).current = ref as TextInput;
+ }
+ }
+ };
+
+ const renderMentionSuggestions = (mentionType: MentionPartType) => (
+ <React.Fragment key={mentionType.trigger}>
+ {mentionType.renderSuggestions &&
+ mentionType.renderSuggestions({
+ keyword: keywordByTrigger[mentionType.trigger],
+ onSuggestionPress: onSuggestionPress(mentionType),
+ })}
+ </React.Fragment>
+ );
+
+ const validateInput = (testString: string) => {
+ return validRegex().test(testString);
+ };
+
+ return (
+ <View style={containerStyle}>
+ {validateInput(keyboardText)
+ ? (partTypes.filter(
+ (one) =>
+ isMentionPartType(one) &&
+ one.renderSuggestions != null &&
+ !one.isBottomMentionSuggestionsRender,
+ ) as MentionPartType[]).map(renderMentionSuggestions)
+ : null}
+
+ <TextInput
+ multiline
+ {...textInputProps}
+ ref={handleTextInputRef}
+ onChangeText={onChangeInput}
+ onSelectionChange={handleSelectionChange}>
+ <Text>
+ {parts.map(({text, partType, data}, index) =>
+ partType ? (
+ <Text
+ key={`${index}-${data?.trigger ?? 'pattern'}`}
+ style={partType.textStyle ?? defaultMentionTextStyle}>
+ {text}
+ </Text>
+ ) : (
+ <Text key={index}>{text}</Text>
+ ),
+ )}
+ </Text>
+ </TextInput>
+
+ {validateInput(keyboardText)
+ ? (partTypes.filter(
+ (one) =>
+ isMentionPartType(one) &&
+ one.renderSuggestions != null &&
+ one.isBottomMentionSuggestionsRender,
+ ) as MentionPartType[]).map(renderMentionSuggestions)
+ : null}
+ </View>
+ );
+};
+
+export {MentionInputControlled};
diff --git a/src/components/common/LikeButton.tsx b/src/components/common/LikeButton.tsx
new file mode 100644
index 00000000..81383eca
--- /dev/null
+++ b/src/components/common/LikeButton.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import {Image, ImageStyle, StyleSheet, TouchableOpacity} from 'react-native';
+import {normalize} from '../../utils';
+
+interface LikeButtonProps {
+ onPress: () => void;
+ style: ImageStyle;
+ liked: boolean;
+ setLiked: (liked: boolean) => void;
+}
+const LikeButton: React.FC<LikeButtonProps> = ({
+ onPress,
+ style,
+ liked,
+ setLiked,
+}) => {
+ const uri = liked
+ ? require('../../assets/images/heart-filled.png')
+ : require('../../assets/images/heart-outlined.png');
+ return (
+ <TouchableOpacity
+ onPress={() => {
+ setLiked(!liked);
+ onPress();
+ }}>
+ <Image style={[styles.image, style]} source={uri} />
+ </TouchableOpacity>
+ );
+};
+
+const styles = StyleSheet.create({
+ image: {
+ width: normalize(18),
+ height: normalize(15),
+ },
+});
+
+export default LikeButton;
diff --git a/src/components/common/index.ts b/src/components/common/index.ts
index b38056c6..48abb8b8 100644
--- a/src/components/common/index.ts
+++ b/src/components/common/index.ts
@@ -26,3 +26,4 @@ export {default as BasicButton} from './BasicButton';
export {default as Avatar} from './Avatar';
export {default as TaggTypeahead} from './TaggTypeahead';
export {default as TaggUserRowCell} from './TaggUserRowCell';
+export {default as LikeButton} from './LikeButton';
diff --git a/src/components/profile/Friends.tsx b/src/components/profile/Friends.tsx
index a7a06567..f800597b 100644
--- a/src/components/profile/Friends.tsx
+++ b/src/components/profile/Friends.tsx
@@ -1,98 +1,39 @@
-import React, {useEffect, useState} from 'react';
+import React from 'react';
import {ScrollView, StyleSheet, Text, View} from 'react-native';
-import {checkPermission} from 'react-native-contacts';
import {TouchableOpacity} from 'react-native-gesture-handler';
import {useDispatch, useStore} from 'react-redux';
import {TAGG_LIGHT_BLUE} from '../../constants';
-import {usersFromContactsService} from '../../services';
import {NO_USER} from '../../store/initialStates';
import {RootState} from '../../store/rootReducer';
import {ProfilePreviewType, ScreenType} from '../../types';
-import {
- extractContacts,
- normalize,
- SCREEN_HEIGHT,
- SCREEN_WIDTH,
-} from '../../utils';
-import {handleAddFriend, handleUnfriend} from '../../utils/friends';
+import {normalize, SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils';
+import {handleUnfriend} from '../../utils/friends';
import {ProfilePreview} from '../profile';
interface FriendsProps {
result: Array<ProfilePreviewType>;
screenType: ScreenType;
userId: string | undefined;
+ hideFriendsFeature?: boolean;
}
-const Friends: React.FC<FriendsProps> = ({result, screenType, userId}) => {
+const Friends: React.FC<FriendsProps> = ({
+ result,
+ screenType,
+ userId,
+ hideFriendsFeature,
+}) => {
const state: RootState = useStore().getState();
const dispatch = useDispatch();
const {user: loggedInUser = NO_USER} = state.user;
- const [usersFromContacts, setUsersFromContacts] = useState<
- ProfilePreviewType[]
- >([]);
-
- useEffect(() => {
- const handleFindFriends = () => {
- extractContacts().then(async (contacts) => {
- const permission = await checkPermission();
- if (permission === 'authorized') {
- let response = await usersFromContactsService(contacts);
- setUsersFromContacts(response.existing_tagg_users);
- } else {
- console.log('Authorize access to contacts');
- }
- });
- };
- handleFindFriends();
- }, []);
-
- const UsersFromContacts = () => (
- <>
- {usersFromContacts?.splice(0, 2).map((profilePreview) => (
- <View key={profilePreview.id} style={styles.container}>
- <View style={styles.friend}>
- <ProfilePreview
- {...{profilePreview}}
- previewType={'Friend'}
- screenType={screenType}
- />
- </View>
- <TouchableOpacity
- style={styles.addFriendButton}
- onPress={() => {
- handleAddFriend(screenType, profilePreview, dispatch, state).then(
- (success) => {
- if (success) {
- let users = usersFromContacts;
- setUsersFromContacts(
- users.filter(
- (user) => user.username !== profilePreview.username,
- ),
- );
- }
- },
- );
- }}>
- <Text style={styles.addFriendButtonTitle}>Add Friend</Text>
- </TouchableOpacity>
- </View>
- ))}
- </>
- );
return (
<>
- {loggedInUser.userId === userId && usersFromContacts.length !== 0 && (
- <View style={styles.subheader}>
- <View style={styles.addFriendHeaderContainer}>
- <Text style={[styles.subheaderText]}>Contacts on Tagg</Text>
- </View>
- <UsersFromContacts />
- </View>
+ {!hideFriendsFeature && (
+ <Text style={[styles.subheaderText, styles.friendsSubheaderText]}>
+ Friends
+ </Text>
)}
- <Text style={[styles.subheaderText, styles.friendsSubheaderText]}>
- Friends
- </Text>
<ScrollView
keyboardShouldPersistTaps={'always'}
style={styles.scrollView}
@@ -129,7 +70,6 @@ const styles = StyleSheet.create({
alignSelf: 'center',
width: SCREEN_WIDTH * 0.85,
},
- firstScrollView: {},
scrollViewContent: {
alignSelf: 'center',
paddingBottom: SCREEN_HEIGHT / 7,
@@ -142,7 +82,6 @@ const styles = StyleSheet.create({
marginBottom: '3%',
marginTop: '2%',
},
- header: {flexDirection: 'row'},
subheader: {
alignSelf: 'center',
width: SCREEN_WIDTH * 0.85,
@@ -154,20 +93,12 @@ const styles = StyleSheet.create({
fontWeight: '600',
lineHeight: normalize(14.32),
},
- findFriendsButton: {flexDirection: 'row'},
friendsSubheaderText: {
alignSelf: 'center',
width: SCREEN_WIDTH * 0.85,
marginVertical: '1%',
marginBottom: '2%',
},
- findFriendsSubheaderText: {
- marginLeft: '5%',
- color: '#08E2E2',
- fontSize: normalize(12),
- fontWeight: '600',
- lineHeight: normalize(14.32),
- },
container: {
alignSelf: 'center',
flexDirection: 'row',