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/BasicButton.tsx12
-rw-r--r--src/components/common/LikeButton.tsx38
-rw-r--r--src/components/common/index.ts1
-rw-r--r--src/components/messages/MessageButton.tsx73
-rw-r--r--src/components/messages/index.ts1
-rw-r--r--src/components/moments/MomentPostContent.tsx4
-rw-r--r--src/components/notifications/Notification.tsx41
-rw-r--r--src/components/profile/Cover.tsx108
-rw-r--r--src/components/profile/Friends.tsx97
-rw-r--r--src/components/profile/ProfileBody.tsx58
-rw-r--r--src/components/profile/TaggAvatar.tsx67
-rw-r--r--src/components/taggs/TaggsBar.tsx14
16 files changed, 631 insertions, 216 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..a1810b71 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 && !liked}
+ 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/BasicButton.tsx b/src/components/common/BasicButton.tsx
index 1fe29cd9..e2274dbd 100644
--- a/src/components/common/BasicButton.tsx
+++ b/src/components/common/BasicButton.tsx
@@ -1,5 +1,12 @@
import React from 'react';
-import {StyleProp, StyleSheet, Text, View, ViewStyle} from 'react-native';
+import {
+ StyleProp,
+ StyleSheet,
+ Text,
+ TextStyle,
+ View,
+ ViewStyle,
+} from 'react-native';
import {TAGG_LIGHT_BLUE} from '../../constants';
import {TouchableOpacity} from 'react-native-gesture-handler';
import {normalize} from '../../utils';
@@ -8,7 +15,7 @@ interface BasicButtonProps {
title: string;
onPress: () => void;
solid?: boolean;
- externalStyles?: Record<string, StyleProp<ViewStyle>>;
+ externalStyles?: Record<string, StyleProp<ViewStyle | TextStyle>>;
}
const BasicButton: React.FC<BasicButtonProps> = ({
title,
@@ -27,6 +34,7 @@ const BasicButton: React.FC<BasicButtonProps> = ({
<Text
style={[
styles.buttonTitle,
+ externalStyles?.buttonTitle,
solid
? styles.solidButtonTitleColor
: styles.outlineButtonTitleColor,
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/messages/MessageButton.tsx b/src/components/messages/MessageButton.tsx
new file mode 100644
index 00000000..5ac42c4c
--- /dev/null
+++ b/src/components/messages/MessageButton.tsx
@@ -0,0 +1,73 @@
+import React from 'react';
+import {Fragment, useContext} from 'react';
+import {useStore} from 'react-redux';
+import {ChatContext} from '../../App';
+import {RootState} from '../../store/rootReducer';
+import {FriendshipStatusType} from '../../types';
+import {createChannel} from '../../utils';
+import {Alert, StyleProp, TextStyle, ViewStyle} from 'react-native';
+import {BasicButton} from '../common';
+import {useNavigation} from '@react-navigation/native';
+import {ERROR_UNABLE_CONNECT_CHAT} from '../../constants/strings';
+
+interface MessageButtonProps {
+ userXId: string;
+ isBlocked: boolean;
+ friendship_status: FriendshipStatusType;
+ friendship_requester_id?: string;
+ solid?: boolean;
+ externalStyles?: Record<string, StyleProp<ViewStyle | TextStyle>>;
+}
+
+const MessageButton: React.FC<MessageButtonProps> = ({
+ userXId,
+ isBlocked,
+ friendship_status,
+ friendship_requester_id,
+ solid,
+ externalStyles,
+}) => {
+ const navigation = useNavigation();
+ const {chatClient, setChannel} = useContext(ChatContext);
+
+ const state: RootState = useStore().getState();
+ const loggedInUserId = state.user.user.userId;
+
+ const canMessage = () => {
+ if (
+ userXId &&
+ !isBlocked &&
+ (friendship_status === 'no_record' ||
+ friendship_status === 'friends' ||
+ (friendship_status === 'requested' &&
+ friendship_requester_id === loggedInUserId))
+ ) {
+ return true;
+ } else {
+ return false;
+ }
+ };
+
+ const onPressMessage = async () => {
+ if (chatClient.user && userXId) {
+ const channel = await createChannel(loggedInUserId, userXId, chatClient);
+ setChannel(channel);
+ navigation.navigate('Chat');
+ } else {
+ Alert.alert(ERROR_UNABLE_CONNECT_CHAT);
+ }
+ };
+
+ return canMessage() ? (
+ <BasicButton
+ title={'Message'}
+ onPress={onPressMessage}
+ externalStyles={externalStyles}
+ solid={solid ? solid : false}
+ />
+ ) : (
+ <Fragment />
+ );
+};
+
+export default MessageButton;
diff --git a/src/components/messages/index.ts b/src/components/messages/index.ts
index b19067ca..7270e2e2 100644
--- a/src/components/messages/index.ts
+++ b/src/components/messages/index.ts
@@ -7,3 +7,4 @@ export {default as MessageAvatar} from './MessageAvatar';
export {default as TypingIndicator} from './TypingIndicator';
export {default as MessageFooter} from './MessageFooter';
export {default as DateHeader} from './DateHeader';
+export {default as MessageButton} from './MessageButton';
diff --git a/src/components/moments/MomentPostContent.tsx b/src/components/moments/MomentPostContent.tsx
index 45186ba1..193bf40c 100644
--- a/src/components/moments/MomentPostContent.tsx
+++ b/src/components/moments/MomentPostContent.tsx
@@ -10,6 +10,7 @@ import {
navigateToProfile,
SCREEN_HEIGHT,
SCREEN_WIDTH,
+ normalize,
} from '../../utils';
import {mentionPartTypes, renderTextWithMentions} from '../../utils/comments';
import {CommentsCount} from '../comments';
@@ -103,6 +104,9 @@ const styles = StyleSheet.create({
marginRight: '5%',
color: '#ffffff',
fontWeight: '500',
+ fontSize: normalize(13),
+ lineHeight: normalize(15.51),
+ letterSpacing: normalize(0.6),
},
});
export default MomentPostContent;
diff --git a/src/components/notifications/Notification.tsx b/src/components/notifications/Notification.tsx
index ae884b42..cb62047a 100644
--- a/src/components/notifications/Notification.tsx
+++ b/src/components/notifications/Notification.tsx
@@ -34,6 +34,7 @@ import {
} from '../../utils';
import {Avatar} from '../common';
import AcceptDeclineButtons from '../common/AcceptDeclineButtons';
+import {MessageButton} from '../messages';
interface NotificationProps {
item: NotificationType;
@@ -61,6 +62,10 @@ const Notification: React.FC<NotificationProps> = (props) => {
const [avatar, setAvatar] = useState<string | undefined>(undefined);
const [momentURI, setMomentURI] = useState<string | undefined>(undefined);
+ const notification_title =
+ notification_type === 'FRD_ACPT'
+ ? `Say Hi to ${first_name}!`
+ : `${first_name} ${last_name}`;
useEffect(() => {
(async () => {
@@ -246,9 +251,7 @@ const Notification: React.FC<NotificationProps> = (props) => {
{/* Text content: Actor name and verbage*/}
<View style={styles.contentContainer}>
<TouchableWithoutFeedback onPress={navigateToProfile}>
- <Text style={styles.actorName}>
- {first_name} {last_name}
- </Text>
+ <Text style={styles.actorName}>{notification_title}</Text>
</TouchableWithoutFeedback>
<TouchableWithoutFeedback
style={styles.textContainerStyles}
@@ -273,6 +276,30 @@ const Notification: React.FC<NotificationProps> = (props) => {
/>
</View>
)}
+ {notification_type === 'FRD_ACPT' && (
+ <View style={styles.buttonsContainer}>
+ <MessageButton
+ userXId={id}
+ isBlocked={false}
+ friendship_status={'friends'}
+ externalStyles={{
+ container: {
+ width: normalize(63),
+ height: normalize(21),
+ marginTop: '7%',
+ },
+ buttonTitle: {
+ fontSize: normalize(11),
+ lineHeight: normalize(13.13),
+ letterSpacing: normalize(0.5),
+ fontWeight: '700',
+ textAlign: 'center',
+ },
+ }}
+ solid
+ />
+ </View>
+ )}
{/* Moment Image Preview */}
{(notification_type === 'CMT' ||
notification_type === 'MOM_3+' ||
@@ -306,7 +333,7 @@ const styles = StyleSheet.create({
flex: 1,
alignSelf: 'center',
alignItems: 'center',
- paddingHorizontal: '8%',
+ paddingHorizontal: '6.3%',
},
avatarContainer: {
height: 42,
@@ -348,9 +375,9 @@ const styles = StyleSheet.create({
lineHeight: normalize(13.13),
},
timeStampStyles: {
- fontWeight: '700',
- fontSize: normalize(12),
- lineHeight: normalize(14.32),
+ fontWeight: '500',
+ fontSize: normalize(11),
+ lineHeight: normalize(13.13),
marginHorizontal: 2,
color: '#828282',
textAlignVertical: 'center',
diff --git a/src/components/profile/Cover.tsx b/src/components/profile/Cover.tsx
index 27777b64..5d5b4234 100644
--- a/src/components/profile/Cover.tsx
+++ b/src/components/profile/Cover.tsx
@@ -1,28 +1,93 @@
-import React from 'react';
-import {Image, StyleSheet, View} from 'react-native';
-import {useSelector} from 'react-redux';
+import React, {useState, useEffect} from 'react';
+import {
+ Image,
+ StyleSheet,
+ View,
+ TouchableOpacity,
+ Text,
+ ImageBackground,
+} from 'react-native';
import {COVER_HEIGHT, IMAGE_WIDTH} from '../../constants';
-import {RootState} from '../../store/rootreducer';
import {ScreenType} from '../../types';
+import GreyPurplePlus from '../../assets/icons/grey-purple-plus.svg';
+import {useDispatch, useSelector} from 'react-redux';
+import {loadUserData, resetHeaderAndProfileImage} from '../../store/actions';
+import {RootState} from '../../store/rootreducer';
+import {normalize, patchProfile, validateImageLink} from '../../utils';
interface CoverProps {
userXId: string | undefined;
screenType: ScreenType;
}
const Cover: React.FC<CoverProps> = ({userXId, screenType}) => {
- const {cover} = useSelector((state: RootState) =>
+ const dispatch = useDispatch();
+ const {cover, user} = useSelector((state: RootState) =>
userXId ? state.userX[screenType][userXId] : state.user,
);
- return (
- <View style={[styles.container]}>
- <Image
- style={styles.image}
- defaultSource={require('../../assets/images/cover-placeholder.png')}
- source={{uri: cover, cache: 'reload'}}
- />
- </View>
- );
+ const [needsUpdate, setNeedsUpdate] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const [validImage, setValidImage] = useState<boolean>(true);
+
+ useEffect(() => {
+ checkAvatar(cover);
+ }, []);
+
+ useEffect(() => {
+ if (needsUpdate) {
+ const userId = user.userId;
+ const username = user.username;
+ dispatch(resetHeaderAndProfileImage());
+ dispatch(loadUserData({userId, username}));
+ }
+ }, [dispatch, needsUpdate]);
+
+ const handleNewImage = async () => {
+ setLoading(true);
+ const result = await patchProfile('header', user.userId);
+ setLoading(true);
+ if (result) {
+ setNeedsUpdate(true);
+ } else {
+ setLoading(false);
+ }
+ };
+
+ const checkAvatar = async (url: string | undefined) => {
+ const valid = await validateImageLink(url);
+ if (valid !== validImage) {
+ setValidImage(valid);
+ }
+ };
+
+ if (!validImage && userXId === undefined && !loading) {
+ return (
+ <View style={[styles.container]}>
+ <ImageBackground
+ style={styles.image}
+ defaultSource={require('../../assets/images/cover-placeholder.png')}
+ source={{uri: cover, cache: 'reload'}}>
+ <TouchableOpacity
+ accessible={true}
+ accessibilityLabel="ADD HEADER PICTURE"
+ onPress={() => handleNewImage()}>
+ <GreyPurplePlus style={styles.plus} />
+ <Text style={styles.text}>Add Picture</Text>
+ </TouchableOpacity>
+ </ImageBackground>
+ </View>
+ );
+ } else {
+ return (
+ <View style={styles.container}>
+ <Image
+ style={styles.image}
+ defaultSource={require('../../assets/images/cover-placeholder.png')}
+ source={{uri: cover, cache: 'reload'}}
+ />
+ </View>
+ );
+ }
};
const styles = StyleSheet.create({
@@ -33,5 +98,20 @@ const styles = StyleSheet.create({
width: IMAGE_WIDTH,
height: COVER_HEIGHT,
},
+ plus: {
+ position: 'absolute',
+ top: 75,
+ right: 125,
+ },
+ text: {
+ color: 'white',
+ position: 'absolute',
+ fontSize: normalize(16),
+ top: 80,
+ right: 20,
+ },
+ touch: {
+ flex: 1,
+ },
});
export default Cover;
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',
diff --git a/src/components/profile/ProfileBody.tsx b/src/components/profile/ProfileBody.tsx
index 3d654724..7557de00 100644
--- a/src/components/profile/ProfileBody.tsx
+++ b/src/components/profile/ProfileBody.tsx
@@ -1,17 +1,7 @@
-import {useNavigation} from '@react-navigation/core';
-import React, {useContext} from 'react';
-import {
- Alert,
- LayoutChangeEvent,
- Linking,
- StyleSheet,
- Text,
- View,
-} from 'react-native';
+import React from 'react';
+import {LayoutChangeEvent, Linking, StyleSheet, Text, View} from 'react-native';
import {useDispatch, useSelector, useStore} from 'react-redux';
-import {ChatContext} from '../../App';
import {TAGG_DARK_BLUE, TOGGLE_BUTTON_TYPE} from '../../constants';
-import {ERROR_UNABLE_CONNECT_CHAT} from '../../constants/strings';
import {
acceptFriendRequest,
declineFriendRequest,
@@ -22,14 +12,14 @@ import {NO_PROFILE} from '../../store/initialStates';
import {RootState} from '../../store/rootReducer';
import {ScreenType} from '../../types';
import {
- createChannel,
getUserAsProfilePreviewType,
normalize,
SCREEN_HEIGHT,
SCREEN_WIDTH,
} from '../../utils';
import {canViewProfile} from '../../utils/users';
-import {BasicButton, FriendsButton} from '../common';
+import {FriendsButton} from '../common';
+import {MessageButton} from '../messages';
import ToggleButton from './ToggleButton';
interface ProfileBodyProps {
@@ -47,7 +37,6 @@ const ProfileBody: React.FC<ProfileBodyProps> = ({
screenType,
}) => {
const dispatch = useDispatch();
- const navigation = useNavigation();
const {profile = NO_PROFILE, user} = useSelector((state: RootState) =>
userXId ? state.userX[screenType][userXId] : state.user,
@@ -65,10 +54,7 @@ const ProfileBody: React.FC<ProfileBodyProps> = ({
profile,
);
- const {chatClient, setChannel} = useContext(ChatContext);
-
const state: RootState = useStore().getState();
- const loggedInUserId = state.user.user.userId;
const handleAcceptRequest = async () => {
await dispatch(acceptFriendRequest({id, username, first_name, last_name}));
@@ -81,32 +67,6 @@ const ProfileBody: React.FC<ProfileBodyProps> = ({
dispatch(updateUserXProfileAllScreens(id, state));
};
- const canMessage = () => {
- if (
- userXId &&
- !isBlocked &&
- (friendship_status === 'no_record' ||
- friendship_status === 'friends' ||
- (friendship_status === 'requested' &&
- friendship_requester_id === loggedInUserId)) &&
- canViewProfile(state, userXId, screenType)
- ) {
- return true;
- } else {
- return false;
- }
- };
-
- const onPressMessage = async () => {
- if (chatClient.user && userXId) {
- const channel = await createChannel(loggedInUserId, userXId, chatClient);
- setChannel(channel);
- navigation.navigate('Chat');
- } else {
- Alert.alert(ERROR_UNABLE_CONNECT_CHAT);
- }
- };
-
return (
<View onLayout={onLayout} style={styles.container}>
<Text style={styles.username}>{`@${username}`}</Text>
@@ -142,10 +102,12 @@ const ProfileBody: React.FC<ProfileBodyProps> = ({
onAcceptRequest={handleAcceptRequest}
onRejectRequest={handleDeclineFriendRequest}
/>
- {canMessage() && (
- <BasicButton
- title={'Message'}
- onPress={onPressMessage}
+ {canViewProfile(state, userXId, screenType) && (
+ <MessageButton
+ userXId={userXId}
+ isBlocked={isBlocked}
+ friendship_status={friendship_status}
+ friendship_requester_id={friendship_requester_id}
externalStyles={{
container: {
width: SCREEN_WIDTH * 0.42,
diff --git a/src/components/profile/TaggAvatar.tsx b/src/components/profile/TaggAvatar.tsx
index ea0bdb65..304b9e3a 100644
--- a/src/components/profile/TaggAvatar.tsx
+++ b/src/components/profile/TaggAvatar.tsx
@@ -1,9 +1,12 @@
-import React from 'react';
-import {StyleSheet} from 'react-native';
-import {useSelector} from 'react-redux';
+import React, {useState, useEffect} from 'react';
+import {StyleSheet, TouchableOpacity} from 'react-native';
import {RootState} from '../../store/rootreducer';
import {ScreenType} from '../../types';
import {Avatar} from '../common';
+import {useDispatch, useSelector} from 'react-redux';
+import {loadUserData, resetHeaderAndProfileImage} from '../../store/actions';
+import PurplePlus from '../../assets/icons/purple-plus.svg';
+import {patchProfile, validateImageLink} from '../../utils';
const PROFILE_DIM = 100;
@@ -20,8 +23,59 @@ const TaggAvatar: React.FC<TaggAvatarProps> = ({
const {avatar} = useSelector((state: RootState) =>
userXId ? state.userX[screenType][userXId] : state.user,
);
+ const dispatch = useDispatch();
+ const [needsUpdate, setNeedsUpdate] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const [validImage, setValidImage] = useState<boolean>(true);
+ const {user} = useSelector((state: RootState) =>
+ userXId ? state.userX[screenType][userXId] : state.user,
+ );
+
+ useEffect(() => {
+ checkAvatar(avatar);
+ }, []);
+
+ useEffect(() => {
+ if (needsUpdate) {
+ const userId = user.userId;
+ const username = user.username;
+ dispatch(resetHeaderAndProfileImage());
+ dispatch(loadUserData({userId, username}));
+ }
+ }, [dispatch, needsUpdate]);
- return <Avatar style={[styles.image, style]} uri={avatar} />;
+ const handleNewImage = async () => {
+ setLoading(true);
+ const result = await patchProfile('profile', user.userId);
+ if (result) {
+ setNeedsUpdate(true);
+ } else {
+ setLoading(false);
+ }
+ };
+
+ const checkAvatar = async (url: string | undefined) => {
+ const valid = await validateImageLink(url);
+ if (valid !== validImage) {
+ setValidImage(valid);
+ }
+ };
+
+ if (!validImage && userXId === undefined && !loading) {
+ return (
+ <>
+ <Avatar style={[styles.image, style]} uri={avatar} />
+ <TouchableOpacity
+ accessible={true}
+ accessibilityLabel="ADD PROFILE PICTURE"
+ onPress={() => handleNewImage()}>
+ <PurplePlus style={styles.plus} />
+ </TouchableOpacity>
+ </>
+ );
+ } else {
+ return <Avatar style={[styles.image, style]} uri={avatar} />;
+ }
};
const styles = StyleSheet.create({
@@ -30,6 +84,11 @@ const styles = StyleSheet.create({
width: PROFILE_DIM,
borderRadius: PROFILE_DIM / 2,
},
+ plus: {
+ position: 'absolute',
+ bottom: 35,
+ right: 0,
+ },
});
export default TaggAvatar;
diff --git a/src/components/taggs/TaggsBar.tsx b/src/components/taggs/TaggsBar.tsx
index 4d567b25..a7e8fc7a 100644
--- a/src/components/taggs/TaggsBar.tsx
+++ b/src/components/taggs/TaggsBar.tsx
@@ -113,13 +113,11 @@ const TaggsBar: React.FC<TaggsBarProps> = ({
loadData();
}
}, [taggsNeedUpdate, user]);
- const paddingTopStylesProgress = useDerivedValue(() =>
- interpolate(
- y.value,
- [PROFILE_CUTOUT_BOTTOM_Y, PROFILE_CUTOUT_BOTTOM_Y + profileBodyHeight],
- [0, 1],
- Extrapolate.CLAMP,
- ),
+ const paddingTopStylesProgress = interpolate(
+ y.value,
+ [PROFILE_CUTOUT_BOTTOM_Y, PROFILE_CUTOUT_BOTTOM_Y + profileBodyHeight],
+ [0, 1],
+ Extrapolate.CLAMP,
);
const shadowOpacityStylesProgress = useDerivedValue(() =>
interpolate(
@@ -134,7 +132,7 @@ const TaggsBar: React.FC<TaggsBarProps> = ({
);
const animatedStyles = useAnimatedStyle(() => ({
shadowOpacity: shadowOpacityStylesProgress.value / 5,
- paddingTop: paddingTopStylesProgress.value * insetTop,
+ paddingTop: paddingTopStylesProgress + insetTop,
}));
return taggs.length > 0 ? (