aboutsummaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/comments/AddComment.tsx98
-rw-r--r--src/components/comments/CommentTile.tsx142
-rw-r--r--src/components/comments/CommentsContainer.tsx135
-rw-r--r--src/components/common/Avatar.tsx18
-rw-r--r--src/components/common/AvatarTitle.tsx14
-rw-r--r--src/components/common/BadgeDetailView.tsx89
-rw-r--r--src/components/common/EmptyContentView.tsx132
-rw-r--r--src/components/common/TaggTypeahead.tsx75
-rw-r--r--src/components/common/TaggUserRowCell.tsx52
-rw-r--r--src/components/common/index.ts3
-rw-r--r--src/components/messages/ChannelPreview.tsx9
-rw-r--r--src/components/messages/ChatHeader.tsx11
-rw-r--r--src/components/messages/ChatInput.tsx12
-rw-r--r--src/components/messages/MessageAvatar.tsx15
-rw-r--r--src/components/moments/MomentPostContent.tsx26
-rw-r--r--src/components/moments/MomentPostHeader.tsx10
-rw-r--r--src/components/notifications/Notification.tsx59
-rw-r--r--src/components/profile/ProfileBody.tsx12
-rw-r--r--src/components/profile/ProfileHeader.tsx22
-rw-r--r--src/components/profile/ProfilePreview.tsx13
-rw-r--r--src/components/profile/TaggAvatar.tsx (renamed from src/components/profile/Avatar.tsx)21
-rw-r--r--src/components/profile/index.ts1
-rw-r--r--src/components/search/ExploreSectionUser.tsx16
-rw-r--r--src/components/search/SearchBar.tsx9
-rw-r--r--src/components/search/SearchResultCell.tsx8
-rw-r--r--src/components/suggestedPeople/MutualFriends.tsx3
-rw-r--r--src/components/taggs/TwitterTaggPost.tsx21
27 files changed, 618 insertions, 408 deletions
diff --git a/src/components/comments/AddComment.tsx b/src/components/comments/AddComment.tsx
index 3b195a2b..9cf10b5e 100644
--- a/src/components/comments/AddComment.tsx
+++ b/src/components/comments/AddComment.tsx
@@ -1,45 +1,50 @@
-import React, {useEffect, useRef} from 'react';
+import React, {useContext, useEffect, useRef, useState} from 'react';
import {
- Image,
Keyboard,
KeyboardAvoidingView,
Platform,
StyleSheet,
+ TextInput,
View,
} from 'react-native';
-import {TextInput, TouchableOpacity} from 'react-native-gesture-handler';
+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';
import {TAGG_LIGHT_BLUE} from '../../constants';
+import {CommentContext} from '../../screens/profile/MomentCommentsScreen';
import {postComment} from '../../services';
import {updateReplyPosted} from '../../store/actions';
import {RootState} from '../../store/rootreducer';
+import {CommentThreadType, CommentType} from '../../types';
import {SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils';
-
-/**
- * This file provides the add comment view for a user.
- * Displays the logged in user's profile picture to the left and then provides space to add a comment.
- * Comment is posted when enter is pressed as requested by product team.
- */
+import {mentionPartTypes} from '../../utils/comments';
+import {Avatar} from '../common';
export interface AddCommentProps {
- setNewCommentsAvailable: Function;
- objectId: string;
+ momentId: string;
placeholderText: string;
- isCommentInFocus: boolean;
}
-const AddComment: React.FC<AddCommentProps> = ({
- setNewCommentsAvailable,
- objectId,
- placeholderText,
- isCommentInFocus,
-}) => {
- const [comment, setComment] = React.useState('');
- const [keyboardVisible, setKeyboardVisible] = React.useState(false);
-
+const AddComment: React.FC<AddCommentProps> = ({momentId, placeholderText}) => {
+ const {setShouldUpdateAllComments, commentTapped} = useContext(
+ CommentContext,
+ );
+ const [inReplyToMention, setInReplyToMention] = useState('');
+ const [comment, setComment] = useState('');
+ const [keyboardVisible, setKeyboardVisible] = useState(false);
const {avatar} = useSelector((state: RootState) => state.user);
const dispatch = useDispatch();
+ const ref = useRef<TextInput>(null);
+ const isReplyingToComment =
+ commentTapped !== undefined && !('parent_comment' in commentTapped);
+ const isReplyingToReply =
+ commentTapped !== undefined && 'parent_comment' in commentTapped;
+ const objectId: string = commentTapped
+ ? 'parent_comment' in commentTapped
+ ? (commentTapped as CommentThreadType).parent_comment.comment_id
+ : (commentTapped as CommentType).comment_id
+ : momentId;
const addComment = async () => {
const trimmed = comment.trim();
@@ -47,18 +52,19 @@ const AddComment: React.FC<AddCommentProps> = ({
return;
}
const postedComment = await postComment(
- trimmed,
+ inReplyToMention + trimmed,
objectId,
- isCommentInFocus,
+ isReplyingToComment || isReplyingToReply,
);
if (postedComment) {
setComment('');
+ setInReplyToMention('');
//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) {
+ if (isReplyingToComment || isReplyingToReply) {
dispatch(
updateReplyPosted({
comment_id: postedComment.comment_id,
@@ -66,7 +72,7 @@ const AddComment: React.FC<AddCommentProps> = ({
}),
);
}
- setNewCommentsAvailable(true);
+ setShouldUpdateAllComments(true);
}
};
@@ -82,14 +88,18 @@ 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) {
+ if (isReplyingToComment || isReplyingToReply) {
+ // bring up keyboard
ref.current?.focus();
}
- }, [isCommentInFocus]);
+ if (commentTapped && isReplyingToReply) {
+ const commenter = (commentTapped as CommentThreadType).commenter;
+ setInReplyToMention(`@[${commenter.username}](${commenter.id}) `);
+ } else {
+ setInReplyToMention('');
+ }
+ }, [isReplyingToComment, isReplyingToReply, commentTapped]);
return (
<KeyboardAvoidingView
@@ -101,22 +111,19 @@ const AddComment: React.FC<AddCommentProps> = ({
keyboardVisible ? styles.whiteBackround : {},
]}>
<View style={styles.textContainer}>
- <Image
- style={styles.avatar}
- source={
- avatar
- ? {uri: avatar}
- : require('../../assets/images/avatar-placeholder.png')
- }
- />
- <TextInput
- style={styles.text}
+ <Avatar style={styles.avatar} uri={avatar} />
+ <MentionInput
+ containerStyle={styles.text}
placeholder={placeholderText}
- placeholderTextColor="grey"
- onChangeText={setComment}
- value={comment}
- multiline={true}
- ref={ref}
+ value={inReplyToMention + comment}
+ onChange={(newText: string) => {
+ // skipping the `inReplyToMention` text
+ setComment(
+ newText.substring(inReplyToMention.length, newText.length),
+ );
+ }}
+ inputRef={ref}
+ partTypes={mentionPartTypes('blue')}
/>
<View style={styles.submitButton}>
<TouchableOpacity style={styles.submitButton} onPress={addComment}>
@@ -148,6 +155,7 @@ const styles = StyleSheet.create({
flex: 1,
padding: '1%',
marginHorizontal: '1%',
+ maxHeight: 100,
},
avatar: {
height: 35,
diff --git a/src/components/comments/CommentTile.tsx b/src/components/comments/CommentTile.tsx
index 34eef418..ecdb4c30 100644
--- a/src/components/comments/CommentTile.tsx
+++ b/src/components/comments/CommentTile.tsx
@@ -1,119 +1,114 @@
-/* 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, TypeOfComment} from '../../types';
+import {useNavigation} from '@react-navigation/native';
+import React, {Fragment, useContext, useEffect, useRef, useState} from 'react';
import {Alert, Animated, StyleSheet} from 'react-native';
-import ClockIcon from '../../assets/icons/clock-icon-01.svg';
-import {TAGG_LIGHT_BLUE} from '../../constants';
+import {Text, View} from 'react-native-animatable';
import {RectButton, TouchableOpacity} from 'react-native-gesture-handler';
-import {getTimePosted, normalize, SCREEN_WIDTH} from '../../utils';
+import Swipeable from 'react-native-gesture-handler/Swipeable';
+import {useDispatch, useSelector, useStore} from 'react-redux';
import Arrow from '../../assets/icons/back-arrow-colored.svg';
+import ClockIcon from '../../assets/icons/clock-icon-01.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 {TAGG_LIGHT_BLUE} from '../../constants';
import {ERROR_FAILED_TO_DELETE_COMMENT} from '../../constants/strings';
-import {useSelector} from 'react-redux';
+import {CommentContext} from '../../screens/profile/MomentCommentsScreen';
+import {deleteComment, getCommentsCount} from '../../services';
import {RootState} from '../../store/rootReducer';
+import {
+ CommentThreadType,
+ CommentType,
+ ScreenType,
+ UserType,
+} from '../../types';
+import {
+ getTimePosted,
+ navigateToProfile,
+ normalize,
+ SCREEN_WIDTH,
+} from '../../utils';
+import {mentionPartTypes, renderTextWithMentions} from '../../utils/comments';
+import {ProfilePreview} from '../profile';
+import CommentsContainer from './CommentsContainer';
/**
* Displays users's profile picture, comment posted by them and the time difference between now and when a comment was posted.
*/
interface CommentTileProps {
- comment_object: CommentType;
+ commentObject: CommentType | CommentThreadType;
screenType: ScreenType;
- typeOfComment: TypeOfComment;
- setCommentObjectInFocus?: (comment: CommentType | undefined) => void;
- newCommentsAvailable: boolean;
- setNewCommentsAvailable: (available: boolean) => void;
+ isThread: boolean;
+ shouldUpdateParent: boolean;
+ setShouldUpdateParent: (update: boolean) => void;
canDelete: boolean;
}
const CommentTile: React.FC<CommentTileProps> = ({
- comment_object,
+ commentObject,
screenType,
- typeOfComment,
- setCommentObjectInFocus,
- newCommentsAvailable,
- setNewCommentsAvailable,
+ setShouldUpdateParent,
+ shouldUpdateParent,
canDelete,
+ isThread,
}) => {
- const timePosted = getTimePosted(comment_object.date_created);
+ const {setCommentTapped} = useContext(CommentContext);
+ const timePosted = getTimePosted(commentObject.date_created);
const [showReplies, setShowReplies] = useState<boolean>(false);
const [showKeyboard, setShowKeyboard] = useState<boolean>(false);
- const [newThreadAvailable, setNewThreadAvailable] = useState(true);
+ const [shouldUpdateChild, setShouldUpdateChild] = useState(true);
const swipeRef = useRef<Swipeable>(null);
- const isThread = typeOfComment === 'Thread';
-
const {replyPosted} = useSelector((state: RootState) => state.user);
+ const state: RootState = useStore().getState();
+ const navigation = useNavigation();
+ const dispatch = useDispatch();
- /**
- * Bubbling up, for handling a new comment in a thread.
- */
useEffect(() => {
- if (newCommentsAvailable) {
- setNewThreadAvailable(true);
+ if (shouldUpdateParent) {
+ setShouldUpdateChild(true);
}
- }, [newCommentsAvailable]);
+ }, [shouldUpdateParent]);
useEffect(() => {
- if (replyPosted && typeOfComment === 'Comment') {
- if (replyPosted.parent_comment.comment_id === comment_object.comment_id) {
+ if (replyPosted && !isThread) {
+ if (replyPosted.parent_comment.comment_id === commentObject.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);
- }
+ setCommentTapped(commentObject);
+ setShowKeyboard(!showKeyboard);
};
const toggleReplies = async () => {
- if (showReplies) {
+ if (showReplies && isThread) {
+ const comment = (commentObject as CommentThreadType).parent_comment;
//To update count of replies in case we deleted a reply
- comment_object.replies_count = parseInt(
- await getCommentsCount(comment_object.comment_id, true),
+ comment.replies_count = parseInt(
+ await getCommentsCount(comment.comment_id, true),
+ 10,
);
}
- setNewThreadAvailable(true);
+ setShouldUpdateChild(true);
setShowReplies(!showReplies);
};
/**
* Method to compute text to be shown for replies button
*/
- const getRepliesText = () =>
+ const getRepliesText = (comment: CommentType) =>
showReplies
? 'Hide'
- : comment_object.replies_count > 0
- ? `Replies (${comment_object.replies_count})`
+ : comment.replies_count > 0
+ ? `Replies (${comment.replies_count})`
: 'Replies';
const renderRightAction = (text: string, color: string) => {
const pressHandler = async () => {
swipeRef.current?.close();
- const success = await deleteComment(comment_object.comment_id, isThread);
+ const success = await deleteComment(commentObject.comment_id, isThread);
if (success) {
- setNewCommentsAvailable(true);
+ setShouldUpdateParent(true);
} else {
Alert.alert(ERROR_FAILED_TO_DELETE_COMMENT);
}
@@ -149,12 +144,18 @@ const CommentTile: React.FC<CommentTileProps> = ({
<View
style={[styles.container, isThread ? styles.moreMarginWithThread : {}]}>
<ProfilePreview
- profilePreview={comment_object.commenter}
+ profilePreview={commentObject.commenter}
previewType={'Comment'}
screenType={screenType}
/>
<TouchableOpacity style={styles.body} onPress={toggleAddComment}>
- <Text style={styles.comment}>{comment_object.comment}</Text>
+ {renderTextWithMentions({
+ value: commentObject.comment,
+ styles: styles.comment,
+ partTypes: mentionPartTypes('blue'),
+ onPress: (user: UserType) =>
+ navigateToProfile(state, dispatch, navigation, screenType, user),
+ })}
<View style={styles.clockIconAndTime}>
<ClockIcon style={styles.clockIcon} />
<Text style={styles.date_time}>{' ' + timePosted}</Text>
@@ -162,11 +163,13 @@ const CommentTile: React.FC<CommentTileProps> = ({
</View>
</TouchableOpacity>
{/*** Show replies text only if there are some replies present */}
- {typeOfComment === 'Comment' && comment_object.replies_count > 0 && (
+ {!isThread && (commentObject as CommentType).replies_count > 0 && (
<TouchableOpacity
style={styles.repliesTextAndIconContainer}
onPress={toggleReplies}>
- <Text style={styles.repliesText}>{getRepliesText()}</Text>
+ <Text style={styles.repliesText}>
+ {getRepliesText(commentObject as CommentType)}
+ </Text>
<Arrow
width={12}
height={11}
@@ -183,12 +186,11 @@ const CommentTile: React.FC<CommentTileProps> = ({
{showReplies && (
<View>
<CommentsContainer
- objectId={comment_object.comment_id}
+ objectId={commentObject.comment_id}
screenType={screenType}
- setNewCommentsAvailable={setNewThreadAvailable}
- newCommentsAvailable={newThreadAvailable}
- typeOfComment={'Thread'}
- commentId={replyPosted?.comment_id}
+ shouldUpdate={shouldUpdateChild}
+ setShouldUpdate={setShouldUpdateChild}
+ isThread={true}
/>
</View>
)}
diff --git a/src/components/comments/CommentsContainer.tsx b/src/components/comments/CommentsContainer.tsx
index 3dc8a71c..cd9ecb02 100644
--- a/src/components/comments/CommentsContainer.tsx
+++ b/src/components/comments/CommentsContainer.tsx
@@ -1,24 +1,23 @@
-import React, {useEffect, useRef, useState} from 'react';
+import moment from 'moment';
+import React, {useContext, 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 './CommentTile';
+import {CommentContext} from '../../screens/profile/MomentCommentsScreen';
import {getComments} from '../../services';
import {updateReplyPosted} from '../../store/actions';
import {RootState} from '../../store/rootReducer';
-import {CommentType, ScreenType, TypeOfComment} from '../../types';
+import {CommentThreadType, CommentType, ScreenType} from '../../types';
import {SCREEN_HEIGHT} from '../../utils';
+import CommentTile from './CommentTile';
+
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;
+ shouldUpdate: boolean;
+ setShouldUpdate: (update: boolean) => void;
+ isThread: boolean;
};
/**
@@ -28,121 +27,85 @@ export type CommentsContainerProps = {
const CommentsContainer: React.FC<CommentsContainerProps> = ({
screenType,
objectId,
- setCommentsLength,
- newCommentsAvailable,
- setNewCommentsAvailable,
- typeOfComment,
- setCommentObjectInFocus,
- commentObjectInFocus,
+ isThread,
+ shouldUpdate,
+ setShouldUpdate,
commentId,
}) => {
+ const {setCommentsLength, commentTapped} = useContext(CommentContext);
const {username: loggedInUsername} = useSelector(
(state: RootState) => state.user.user,
);
const [commentsList, setCommentsList] = useState<CommentType[]>([]);
const dispatch = useDispatch();
const ref = useRef<FlatList<CommentType>>(null);
+ const ITEM_HEIGHT = SCREEN_HEIGHT / 7.0;
useEffect(() => {
const loadComments = async () => {
- await getComments(objectId, typeOfComment === 'Thread').then(
- (comments) => {
- if (comments && subscribedToLoadComments) {
- setCommentsList(comments);
- if (setCommentsLength) {
- setCommentsLength(comments.length);
- }
- setNewCommentsAvailable(false);
+ await getComments(objectId, isThread).then((comments) => {
+ if (comments && subscribedToLoadComments) {
+ setCommentsList(comments);
+ if (setCommentsLength) {
+ setCommentsLength(comments.length);
}
- },
- );
+ setShouldUpdate(false);
+ }
+ });
};
let subscribedToLoadComments = true;
- if (newCommentsAvailable) {
+ if (shouldUpdate) {
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);
- }
- };
+ }, [shouldUpdate]);
+ // scrolls to the comment
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 (commentId) {
+ const index = commentsList.findIndex(
+ (item) => item.comment_id === commentId,
+ );
+ if (index > 0) {
+ let comments = [...commentsList];
+ const temp = comments[index];
+ comments[index] = comments[0];
+ comments[0] = temp;
+ setCommentsList(comments);
}
- };
- if (commentsList) {
- //Bring the relevant comment to top if a comment id is present else scroll if necessary
- performAction();
+ } else if (!isThread && !commentTapped) {
+ setTimeout(() => {
+ ref.current?.scrollToEnd({animated: true});
+ }, 500);
}
-
- //Clean up the reply id present in store
return () => {
- if (commentId && typeOfComment === 'Thread') {
+ if (commentId && isThread) {
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;
+ }, [commentId]);
- const renderComment = ({item}: {item: CommentType}) => (
+ const renderComment = ({item}: {item: CommentType | CommentThreadType}) => (
<CommentTile
key={item.comment_id}
- comment_object={item}
+ commentObject={item}
screenType={screenType}
- typeOfComment={typeOfComment}
- setCommentObjectInFocus={setCommentObjectInFocus}
- newCommentsAvailable={newCommentsAvailable}
- setNewCommentsAvailable={setNewCommentsAvailable}
+ isThread={isThread}
+ shouldUpdateParent={shouldUpdate}
+ setShouldUpdateParent={setShouldUpdate}
canDelete={item.commenter.username === loggedInUsername}
/>
);
return (
<FlatList
- data={commentsList}
+ data={commentsList.sort(
+ (a, b) => moment(a.date_created).unix() - moment(b.date_created).unix(),
+ )}
ref={ref}
keyExtractor={(item, index) => index.toString()}
decelerationRate={'fast'}
diff --git a/src/components/common/Avatar.tsx b/src/components/common/Avatar.tsx
new file mode 100644
index 00000000..831cf906
--- /dev/null
+++ b/src/components/common/Avatar.tsx
@@ -0,0 +1,18 @@
+import React, {FC} from 'react';
+import {Image, ImageStyle, StyleProp} from 'react-native';
+
+type AvatarProps = {
+ style: StyleProp<ImageStyle>;
+ uri: string | undefined;
+};
+const Avatar: FC<AvatarProps> = ({style, uri}) => {
+ return (
+ <Image
+ style={style}
+ defaultSource={require('../../assets/images/avatar-placeholder.png')}
+ source={{uri, cache: 'reload'}}
+ />
+ );
+};
+
+export default Avatar;
diff --git a/src/components/common/AvatarTitle.tsx b/src/components/common/AvatarTitle.tsx
index 81351327..a2a7c0aa 100644
--- a/src/components/common/AvatarTitle.tsx
+++ b/src/components/common/AvatarTitle.tsx
@@ -1,10 +1,11 @@
import React from 'react';
-import {Image, StyleSheet, View} from 'react-native';
+import {StyleSheet, View} from 'react-native';
import LinearGradient from 'react-native-linear-gradient';
import {TAGGS_GRADIENT} from '../../constants';
+import Avatar from './Avatar';
type AvatarTitleProps = {
- avatar: string | null;
+ avatar: string | undefined;
};
const AvatarTitle: React.FC<AvatarTitleProps> = ({avatar}) => {
return (
@@ -16,14 +17,7 @@ const AvatarTitle: React.FC<AvatarTitleProps> = ({avatar}) => {
angleCenter={{x: 0.5, y: 0.5}}
style={styles.gradient}
/>
- <Image
- style={styles.avatar}
- source={
- avatar
- ? {uri: avatar}
- : require('../../assets/images/avatar-placeholder.png')
- }
- />
+ <Avatar style={styles.avatar} uri={avatar} />
</View>
);
};
diff --git a/src/components/common/BadgeDetailView.tsx b/src/components/common/BadgeDetailView.tsx
index bc4384e8..6504300c 100644
--- a/src/components/common/BadgeDetailView.tsx
+++ b/src/components/common/BadgeDetailView.tsx
@@ -1,24 +1,15 @@
import {useNavigation} from '@react-navigation/core';
import React, {useEffect, useState} from 'react';
-import {
- ActivityIndicator,
- FlatList,
- Image,
- Modal,
- StyleSheet,
- Text,
- View,
-} from 'react-native';
+import {FlatList, Image, Modal, StyleSheet, Text, View} from 'react-native';
import {TouchableOpacity} from 'react-native-gesture-handler';
import LinearGradient from 'react-native-linear-gradient';
-import {useSelector} from 'react-redux';
+import {useDispatch, useSelector} from 'react-redux';
import CloseIcon from '../../assets/ionicons/close-outline.svg';
import {BADGE_GRADIENT_FIRST} from '../../constants';
import {BADGE_DATA} from '../../constants/badges';
-import {getSuggestedPeopleProfile, removeBadgesService} from '../../services';
import {RootState} from '../../store/rootreducer';
-import {ScreenType, UniversityBadge} from '../../types';
-import {getUniversityBadge, normalize} from '../../utils';
+import {ScreenType} from '../../types';
+import {getUniversityBadge, normalize, removeUserBadge} from '../../utils';
interface BadgeDetailModalProps {
userXId: string | undefined;
@@ -35,38 +26,21 @@ const BadgeDetailView: React.FC<BadgeDetailModalProps> = ({
isEditable = true,
setBadgeViewVisible,
}) => {
- const {user, profile} = useSelector((state: RootState) =>
+ const dispatch = useDispatch();
+ const {
+ user,
+ profile: {university, badges},
+ } = useSelector((state: RootState) =>
userXId ? state.userX[screenType][userXId] : state.user,
);
const navigation = useNavigation();
- const [selectedBadges, setSelectedBadges] = useState<UniversityBadge[]>([]);
- const [isLoading, setIsLoading] = useState(true);
const [selectedBadgesWithImage, setSelectedBadgesWithImage] = useState<any[]>(
[],
);
- const fetchBadges = async () => {
- if (user.userId) {
- const response = await getSuggestedPeopleProfile(user.userId);
- if (response) {
- const data = response.badges;
- let extractedBadgeNames: UniversityBadge[] = [];
- data.forEach((badge) => {
- extractedBadgeNames.push(badge);
- });
- setSelectedBadges(extractedBadgeNames);
- }
- }
- };
-
- useEffect(() => {
- setIsLoading(true);
- fetchBadges();
- }, []);
-
useEffect(() => {
let badgesWithImage = [];
- selectedBadges.forEach((e) => {
+ badges.forEach((e) => {
const uniData = BADGE_DATA[e.university];
const categoryData = uniData.filter((u) => {
return u.title === e.category;
@@ -81,14 +55,11 @@ const BadgeDetailView: React.FC<BadgeDetailModalProps> = ({
});
setTimeout(() => {
setSelectedBadgesWithImage(badgesWithImage);
- setIsLoading(false);
}, 250);
- }, [selectedBadges]);
+ }, [badges]);
- const removeBadgeCell = async (badge: string) => {
- setIsLoading(true);
- await removeBadgesService([badge], user.userId);
- fetchBadges();
+ const removeBadgeCell = async (badgeName: string) => {
+ await removeUserBadge(badges, badgeName, user.userId, dispatch);
};
const badgeEditCell = ({item: {id, name, badgeImage}}) => {
@@ -175,38 +146,27 @@ const BadgeDetailView: React.FC<BadgeDetailModalProps> = ({
<View style={styles.modalImageContainerStyles}>
<Image
style={styles.modalImageStyles}
- source={getUniversityBadge(profile.university, 'Crest')}
+ source={getUniversityBadge(university, 'Crest')}
/>
</View>
</View>
{modalHeader()}
- {!isLoading && (
- <View>
- <FlatList
- contentContainerStyle={styles.modalListStyles}
- scrollEnabled={false}
- data={selectedBadgesWithImage}
- numColumns={3}
- renderItem={badgeEditCell}
- keyExtractor={(item) => item.id.toString()}
- />
- </View>
- )}
- {isLoading && _loaderView()}
+ <View>
+ <FlatList
+ contentContainerStyle={styles.modalListStyles}
+ scrollEnabled={false}
+ data={selectedBadgesWithImage}
+ numColumns={3}
+ renderItem={badgeEditCell}
+ keyExtractor={(item) => item.id.toString()}
+ />
+ </View>
{isEditable && addButton()}
</View>
</View>
);
};
- const _loaderView = () => {
- return (
- <View style={styles.loaderStyles}>
- <ActivityIndicator animating={isLoading} size="large" color="black" />
- </View>
- );
- };
-
return (
<Modal
animationType="fade"
@@ -269,7 +229,6 @@ const styles = StyleSheet.create({
lineHeight: normalize(20.29),
textAlign: 'center',
},
- loaderStyles: {justifyContent: 'center', marginVertical: 20},
modalSubheadingStyles: {
fontWeight: '600',
fontSize: normalize(11),
diff --git a/src/components/common/EmptyContentView.tsx b/src/components/common/EmptyContentView.tsx
new file mode 100644
index 00000000..14ad4af1
--- /dev/null
+++ b/src/components/common/EmptyContentView.tsx
@@ -0,0 +1,132 @@
+import React from 'react';
+import {Image, Text, StyleSheet, View} from 'react-native';
+import LinearGradient from 'react-native-linear-gradient';
+import {
+ UP_TO_DATE,
+ NO_NEW_NOTIFICATIONS,
+ FIRST_MESSAGE,
+ START_CHATTING,
+} from '../../constants/strings';
+import {NOTIFICATION_GRADIENT} from '../../constants/constants';
+import {SCREEN_HEIGHT, normalize, SCREEN_WIDTH} from '../../utils';
+import {EmptyViewProps} from '../../types/index';
+
+const EmptyContentView: React.FC<EmptyViewProps> = ({viewType}) => {
+ const _getNotificationImage = () => {
+ return (
+ <LinearGradient
+ style={styles.backgroundLinearView}
+ useAngle={true}
+ angle={180}
+ colors={NOTIFICATION_GRADIENT}>
+ <Image
+ source={require('../../assets/images/empty_notifications.png')}
+ />
+ </LinearGradient>
+ );
+ };
+
+ const _getChatImage = () => {
+ return (
+ <LinearGradient
+ style={styles.backgroundLinearView}
+ useAngle={true}
+ angle={180}
+ colors={NOTIFICATION_GRADIENT}>
+ <Image
+ style={styles.imageStyles}
+ source={require('../../assets/images/no_chats.png')}
+ />
+ </LinearGradient>
+ );
+ };
+
+ const _getImageForType = () => {
+ switch (viewType) {
+ case 'Notification':
+ return _getNotificationImage();
+ case 'ChatList':
+ return _getChatImage();
+ }
+ };
+
+ const _getTextForNotification = () => {
+ return (
+ <>
+ <View style={styles.topMargin}>
+ <Text style={styles.upperTextStyle}>{UP_TO_DATE}</Text>
+ </View>
+ <View>
+ <Text style={styles.bottomTextStyle}>{NO_NEW_NOTIFICATIONS}</Text>
+ </View>
+ </>
+ );
+ };
+
+ const _getTextForChat = () => {
+ return (
+ <View style={styles.chatTextStyles}>
+ <View style={styles.topMargin}>
+ <Text style={styles.upperTextStyle}>{START_CHATTING}</Text>
+ </View>
+ <View>
+ <Text style={styles.bottomTextStyle}>{FIRST_MESSAGE}</Text>
+ </View>
+ </View>
+ );
+ };
+
+ const _getTextForType = () => {
+ switch (viewType) {
+ case 'Notification':
+ return _getTextForNotification();
+ case 'ChatList':
+ return _getTextForChat();
+ }
+ };
+
+ return (
+ <View style={styles.container}>
+ {_getImageForType()}
+ {_getTextForType()}
+ </View>
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ flexDirection: 'column',
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ topMargin: {
+ marginTop: SCREEN_HEIGHT * 0.025,
+ paddingBottom: '5%',
+ },
+ upperTextStyle: {
+ textAlign: 'center',
+ fontWeight: '700',
+ fontSize: normalize(23),
+ lineHeight: normalize(40),
+ },
+ chatTextStyles: {
+ width: '85%',
+ },
+ bottomTextStyle: {
+ textAlign: 'center',
+ color: '#808080',
+ fontWeight: '600',
+ fontSize: normalize(20),
+ lineHeight: normalize(30),
+ },
+ imageStyles: {
+ width: SCREEN_WIDTH * 0.72,
+ height: SCREEN_WIDTH * 0.72,
+ },
+ backgroundLinearView: {
+ borderRadius: (SCREEN_WIDTH * 0.72) / 2,
+ },
+});
+
+export default EmptyContentView;
diff --git a/src/components/common/TaggTypeahead.tsx b/src/components/common/TaggTypeahead.tsx
new file mode 100644
index 00000000..7cd99278
--- /dev/null
+++ b/src/components/common/TaggTypeahead.tsx
@@ -0,0 +1,75 @@
+import React, {Fragment, useEffect, useState} from 'react';
+import {ScrollView, StyleSheet} from 'react-native';
+import {MentionSuggestionsProps} from 'react-native-controlled-mentions';
+import {SEARCH_ENDPOINT_MESSAGES} from '../../constants';
+import {loadSearchResults} from '../../services';
+import {ProfilePreviewType} from '../../types';
+import {SCREEN_WIDTH} from '../../utils';
+import TaggUserRowCell from './TaggUserRowCell';
+
+const TaggTypeahead: React.FC<MentionSuggestionsProps> = ({
+ keyword,
+ onSuggestionPress,
+}) => {
+ const [results, setResults] = useState<ProfilePreviewType[]>([]);
+ const [height, setHeight] = useState(0);
+
+ useEffect(() => {
+ getQuerySuggested();
+ }, [keyword]);
+
+ const getQuerySuggested = async () => {
+ if (!keyword || keyword.length < 3) {
+ setResults([]);
+ return;
+ }
+ const searchResults = await loadSearchResults(
+ `${SEARCH_ENDPOINT_MESSAGES}?query=${keyword}`,
+ );
+ if (searchResults && searchResults.users) {
+ setResults(searchResults.users);
+ }
+ };
+
+ if (results.length === 0) {
+ return <Fragment />;
+ }
+
+ return (
+ <ScrollView
+ style={[styles.container, {top: -(height + 30)}]}
+ showsVerticalScrollIndicator={false}
+ onLayout={(event) => {
+ setHeight(event.nativeEvent.layout.height);
+ }}>
+ {results.map((user) => (
+ <TaggUserRowCell
+ onPress={() => {
+ onSuggestionPress({
+ id: user.id,
+ name: user.username,
+ });
+ setResults([]);
+ }}
+ user={user}
+ />
+ ))}
+ </ScrollView>
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ marginLeft: SCREEN_WIDTH * 0.05,
+ width: SCREEN_WIDTH * 0.9,
+ maxHeight: 264,
+ borderRadius: 10,
+ backgroundColor: 'white',
+ position: 'absolute',
+ alignSelf: 'center',
+ zIndex: 1,
+ borderWidth: 1,
+ },
+});
+
+export default TaggTypeahead;
diff --git a/src/components/common/TaggUserRowCell.tsx b/src/components/common/TaggUserRowCell.tsx
new file mode 100644
index 00000000..446dedc9
--- /dev/null
+++ b/src/components/common/TaggUserRowCell.tsx
@@ -0,0 +1,52 @@
+import React from 'react';
+import {StyleSheet, Text, TouchableOpacity, View} from 'react-native';
+import {ProfilePreviewType} from '../../types';
+import {normalize} from '../../utils';
+import Avatar from './Avatar';
+
+type TaggUserRowCellProps = {
+ onPress: () => void;
+ user: ProfilePreviewType;
+};
+const TaggUserRowCell: React.FC<TaggUserRowCellProps> = ({onPress, user}) => {
+ return (
+ <TouchableOpacity onPress={onPress} style={styles.container}>
+ <Avatar style={styles.image} uri={user.thumbnail_url} />
+ <View style={styles.textContent}>
+ <Text style={styles.username}>{`@${user.username}`}</Text>
+ <Text style={styles.name}>
+ {user.first_name} {user.last_name}
+ </Text>
+ </View>
+ </TouchableOpacity>
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flexDirection: 'row',
+ paddingHorizontal: 25,
+ paddingVertical: 15,
+ width: '100%',
+ },
+ image: {
+ width: normalize(30),
+ height: normalize(30),
+ borderRadius: 30,
+ },
+ textContent: {
+ flexDirection: 'column',
+ justifyContent: 'space-between',
+ marginLeft: 20,
+ },
+ username: {
+ fontWeight: '500',
+ fontSize: normalize(14),
+ },
+ name: {
+ fontWeight: '500',
+ fontSize: normalize(12),
+ color: '#828282',
+ },
+});
+export default TaggUserRowCell;
diff --git a/src/components/common/index.ts b/src/components/common/index.ts
index 5a601f1d..b38056c6 100644
--- a/src/components/common/index.ts
+++ b/src/components/common/index.ts
@@ -23,3 +23,6 @@ export {default as FriendsButton} from './FriendsButton';
export {default as TaggSquareButton} from './TaggSquareButton';
export {default as GradientBorderButton} from './GradientBorderButton';
export {default as BasicButton} from './BasicButton';
+export {default as Avatar} from './Avatar';
+export {default as TaggTypeahead} from './TaggTypeahead';
+export {default as TaggUserRowCell} from './TaggUserRowCell';
diff --git a/src/components/messages/ChannelPreview.tsx b/src/components/messages/ChannelPreview.tsx
index 3d31d42a..878e5a6b 100644
--- a/src/components/messages/ChannelPreview.tsx
+++ b/src/components/messages/ChannelPreview.tsx
@@ -22,6 +22,7 @@ import {
} from '../../types';
import {normalize, SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils';
import {getMember, isOnline} from '../../utils/messages';
+import {Avatar} from '../common';
const ChannelPreview: React.FC<
ChannelPreviewMessengerProps<
@@ -87,13 +88,9 @@ const ChannelPreview: React.FC<
navigation.navigate('Chat');
}}>
<View>
- <Image
+ <Avatar
style={styles.avatar}
- source={
- member
- ? {uri: member.user?.thumbnail_url}
- : require('../../assets/images/avatar-placeholder.png')
- }
+ uri={member?.user?.thumbnail_url as string}
/>
{online && <View style={styles.online} />}
</View>
diff --git a/src/components/messages/ChatHeader.tsx b/src/components/messages/ChatHeader.tsx
index 7ddaa7e6..37dab0fd 100644
--- a/src/components/messages/ChatHeader.tsx
+++ b/src/components/messages/ChatHeader.tsx
@@ -1,6 +1,6 @@
import {useNavigation} from '@react-navigation/native';
import React, {useContext} from 'react';
-import {Image, StyleSheet, View} from 'react-native';
+import {StyleSheet, View} from 'react-native';
import {Text} from 'react-native-animatable';
import {TouchableOpacity} from 'react-native-gesture-handler';
import {useDispatch, useStore} from 'react-redux';
@@ -15,6 +15,7 @@ import {
userXInStore,
} from '../../utils';
import {formatLastSeenText, getMember, isOnline} from '../../utils/messages';
+import {Avatar} from '../common';
type ChatHeaderProps = {
screenType: ScreenType;
@@ -50,13 +51,9 @@ const ChatHeader: React.FC<ChatHeaderProps> = (props) => {
<View style={styles.container}>
<TouchableOpacity style={styles.tappables} onPress={toProfile}>
<View>
- <Image
+ <Avatar
style={styles.avatar}
- source={
- member
- ? {uri: member.user?.thumbnail_url}
- : require('../../assets/images/avatar-placeholder.png')
- }
+ uri={member?.user?.thumbnail_url as string}
/>
{online && <View style={styles.online} />}
</View>
diff --git a/src/components/messages/ChatInput.tsx b/src/components/messages/ChatInput.tsx
index a6e5c458..5585d246 100644
--- a/src/components/messages/ChatInput.tsx
+++ b/src/components/messages/ChatInput.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import {Image, StyleSheet, View} from 'react-native';
+import {StyleSheet, View} from 'react-native';
import {useStore} from 'react-redux';
import {
AutoCompleteInput,
@@ -17,6 +17,7 @@ import {
LocalUserType,
} from '../../types';
import {normalize} from '../../utils';
+import {Avatar} from '../common';
import {ChatInputSubmit} from '../messages';
const ChatInput: React.FC<
@@ -65,14 +66,7 @@ const ChatInput: React.FC<
return (
<View style={styles.container}>
<View style={styles.textContainer}>
- <Image
- style={styles.avatar}
- source={
- avatar
- ? {uri: avatar}
- : require('../../assets/images/avatar-placeholder.png')
- }
- />
+ <Avatar style={styles.avatar} uri={avatar} />
<AutoCompleteInput />
<View style={styles.actionButtons}>
{/* TODO: Not working, toggled off for now */}
diff --git a/src/components/messages/MessageAvatar.tsx b/src/components/messages/MessageAvatar.tsx
index d275eae5..bcd4e7a8 100644
--- a/src/components/messages/MessageAvatar.tsx
+++ b/src/components/messages/MessageAvatar.tsx
@@ -1,9 +1,10 @@
import React, {useContext} from 'react';
-import {Image, StyleSheet, View} from 'react-native';
-import {getMember, normalize} from '../../utils';
-import {useMessageContext} from 'stream-chat-react-native-core';
+import {StyleSheet, View} from 'react-native';
import {useStore} from 'react-redux';
+import {useMessageContext} from 'stream-chat-react-native-core';
import {ChatContext} from '../../App';
+import {getMember, normalize} from '../../utils';
+import {Avatar} from '../common';
const MessageAvatar: React.FC = () => {
const {channel} = useContext(ChatContext);
@@ -14,13 +15,9 @@ const MessageAvatar: React.FC = () => {
return (
<View style={styles.messageAvatarContainer}>
{message.lastGroupMessage === true && (
- <Image
+ <Avatar
style={styles.messageAvatar}
- source={
- member
- ? {uri: member.user?.thumbnail_url}
- : require('../../assets/images/avatar-placeholder.png')
- }
+ uri={member?.user?.thumbnail_url as string}
/>
)}
</View>
diff --git a/src/components/moments/MomentPostContent.tsx b/src/components/moments/MomentPostContent.tsx
index d68ceaa3..45186ba1 100644
--- a/src/components/moments/MomentPostContent.tsx
+++ b/src/components/moments/MomentPostContent.tsx
@@ -1,8 +1,17 @@
+import {useNavigation} from '@react-navigation/native';
import React, {useEffect} from 'react';
import {Image, StyleSheet, Text, View, ViewProps} from 'react-native';
+import {useDispatch, useStore} from 'react-redux';
import {getCommentsCount} from '../../services';
-import {ScreenType} from '../../types';
-import {getTimePosted, SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils';
+import {RootState} from '../../store/rootReducer';
+import {ScreenType, UserType} from '../../types';
+import {
+ getTimePosted,
+ navigateToProfile,
+ SCREEN_HEIGHT,
+ SCREEN_WIDTH,
+} from '../../utils';
+import {mentionPartTypes, renderTextWithMentions} from '../../utils/comments';
import {CommentsCount} from '../comments';
interface MomentPostContentProps extends ViewProps {
@@ -22,6 +31,9 @@ const MomentPostContent: React.FC<MomentPostContentProps> = ({
}) => {
const [elapsedTime, setElapsedTime] = React.useState<string>();
const [comments_count, setCommentsCount] = React.useState('');
+ const state: RootState = useStore().getState();
+ const navigation = useNavigation();
+ const dispatch = useDispatch();
useEffect(() => {
const fetchCommentsCount = async () => {
@@ -47,7 +59,13 @@ const MomentPostContent: React.FC<MomentPostContentProps> = ({
/>
<Text style={styles.text}>{elapsedTime}</Text>
</View>
- <Text style={styles.captionText}>{caption}</Text>
+ {renderTextWithMentions({
+ value: caption,
+ styles: styles.captionText,
+ partTypes: mentionPartTypes('white'),
+ onPress: (user: UserType) =>
+ navigateToProfile(state, dispatch, navigation, screenType, user),
+ })}
</View>
);
};
@@ -84,7 +102,7 @@ const styles = StyleSheet.create({
marginLeft: '5%',
marginRight: '5%',
color: '#ffffff',
- fontWeight: 'bold',
+ fontWeight: '500',
},
});
export default MomentPostContent;
diff --git a/src/components/moments/MomentPostHeader.tsx b/src/components/moments/MomentPostHeader.tsx
index 8cf509ab..d2e9fc49 100644
--- a/src/components/moments/MomentPostHeader.tsx
+++ b/src/components/moments/MomentPostHeader.tsx
@@ -1,4 +1,4 @@
-import React, {useState} from 'react';
+import React, {useEffect, useState} from 'react';
import {
StyleSheet,
Text,
@@ -10,7 +10,7 @@ import {MomentMoreInfoDrawer} from '../profile';
import {loadUserMoments} from '../../store/actions';
import {useDispatch, useSelector, useStore} from 'react-redux';
import {ScreenType} from '../../types';
-import Avatar from '../profile/Avatar';
+import TaggAvatar from '../profile/TaggAvatar';
import {useNavigation} from '@react-navigation/native';
import {RootState} from '../../store/rootReducer';
import {fetchUserX, userXInStore} from '../../utils';
@@ -51,10 +51,14 @@ const MomentPostHeader: React.FC<MomentPostHeaderProps> = ({
});
};
+ useEffect(() => {
+ setDrawerVisible(drawerVisible);
+ }, [drawerVisible]);
+
return (
<View style={[styles.container, style]}>
<TouchableOpacity onPress={navigateToProfile} style={styles.header}>
- <Avatar
+ <TaggAvatar
style={styles.avatar}
userXId={userXId}
screenType={screenType}
diff --git a/src/components/notifications/Notification.tsx b/src/components/notifications/Notification.tsx
index 3cc1c7f1..ae884b42 100644
--- a/src/components/notifications/Notification.tsx
+++ b/src/components/notifications/Notification.tsx
@@ -26,10 +26,13 @@ import {
} from '../../types';
import {
fetchUserX,
+ getTimeInShorthand,
+ normalize,
SCREEN_HEIGHT,
SCREEN_WIDTH,
userXInStore,
} from '../../utils';
+import {Avatar} from '../common';
import AcceptDeclineButtons from '../common/AcceptDeclineButtons';
interface NotificationProps {
@@ -46,6 +49,7 @@ const Notification: React.FC<NotificationProps> = (props) => {
notification_type,
notification_object,
unread,
+ timestamp,
},
screenType,
loggedInUser,
@@ -225,19 +229,17 @@ const Notification: React.FC<NotificationProps> = (props) => {
<TouchableWithoutFeedback
onPress={navigateToProfile}
style={styles.avatarContainer}>
- <Image
- style={styles.avatar}
- source={
- avatar
- ? {uri: avatar, cache: 'reload'}
- : require('../../assets/images/avatar-placeholder.png')
- }
- />
+ <Avatar style={styles.avatar} uri={avatar} />
</TouchableWithoutFeedback>
{notification_type === 'SYSTEM_MSG' ? (
// Only verbage
<View style={styles.contentContainer}>
- <Text style={styles.actorName}>{verbage}</Text>
+ <View style={styles.textContainerStyles}>
+ <Text style={styles.actorName}>{verbage}</Text>
+ <Text style={styles.timeStampStyles}>
+ {getTimeInShorthand(timestamp)}
+ </Text>
+ </View>
</View>
) : (
<>
@@ -248,8 +250,17 @@ const Notification: React.FC<NotificationProps> = (props) => {
{first_name} {last_name}
</Text>
</TouchableWithoutFeedback>
- <TouchableWithoutFeedback onPress={onNotificationTap}>
- <Text>{verbage}</Text>
+ <TouchableWithoutFeedback
+ style={styles.textContainerStyles}
+ onPress={onNotificationTap}>
+ <Text style={styles.verbageStyles}>
+ {verbage}
+ <Text style={styles.timeStampStyles}>
+ {' '}
+ {getTimeInShorthand(timestamp)}
+ </Text>
+ </Text>
+ {/* <Text style={styles.verbageStyles}>{verbage}</Text> */}
</TouchableWithoutFeedback>
</View>
{/* Friend request accept/decline button */}
@@ -310,22 +321,40 @@ const styles = StyleSheet.create({
contentContainer: {
flex: 5,
marginLeft: '5%',
+ marginRight: '3%',
height: '80%',
flexDirection: 'column',
justifyContent: 'space-around',
},
actorName: {
- fontSize: 15,
+ fontSize: normalize(12),
fontWeight: '700',
+ lineHeight: normalize(14.32),
},
moment: {
- height: 42,
- width: 42,
- right: '5%',
+ height: normalize(42),
+ width: normalize(42),
},
buttonsContainer: {
height: '80%',
},
+ textContainerStyles: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ },
+ verbageStyles: {
+ fontWeight: '500',
+ fontSize: normalize(11),
+ lineHeight: normalize(13.13),
+ },
+ timeStampStyles: {
+ fontWeight: '700',
+ fontSize: normalize(12),
+ lineHeight: normalize(14.32),
+ marginHorizontal: 2,
+ color: '#828282',
+ textAlignVertical: 'center',
+ },
imageFlex: {
flex: 1,
},
diff --git a/src/components/profile/ProfileBody.tsx b/src/components/profile/ProfileBody.tsx
index ea1e5166..400a31e7 100644
--- a/src/components/profile/ProfileBody.tsx
+++ b/src/components/profile/ProfileBody.tsx
@@ -1,3 +1,4 @@
+import {useNavigation} from '@react-navigation/core';
import React, {useContext} from 'react';
import {
Alert,
@@ -7,30 +8,29 @@ import {
Text,
View,
} from 'react-native';
-import {normalize} from 'react-native-elements';
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,
updateUserXFriends,
updateUserXProfileAllScreens,
} from '../../store/actions';
-import {canViewProfile} from '../../utils/users';
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 {FriendsButton, BasicButton} from '../common';
+import {canViewProfile} from '../../utils/users';
+import {BasicButton, FriendsButton} from '../common';
import ToggleButton from './ToggleButton';
-import {ChatContext} from '../../App';
-import {useNavigation} from '@react-navigation/core';
-import {ERROR_UNABLE_CONNECT_CHAT} from '../../constants/strings';
interface ProfileBodyProps {
onLayout: (event: LayoutChangeEvent) => void;
diff --git a/src/components/profile/ProfileHeader.tsx b/src/components/profile/ProfileHeader.tsx
index 35ec0ea9..14f7dc71 100644
--- a/src/components/profile/ProfileHeader.tsx
+++ b/src/components/profile/ProfileHeader.tsx
@@ -8,7 +8,7 @@ import {RootState} from '../../store/rootreducer';
import {ScreenType} from '../../types';
import {hasSeenBadgeTutorial, normalize} from '../../utils';
import BadgeDetailView from '../common/BadgeDetailView';
-import Avatar from './Avatar';
+import TaggAvatar from './TaggAvatar';
import BadgeTutorial from './BadgeTutorial';
import FriendsCount from './FriendsCount';
import ProfileMoreInfoDrawer from './ProfileMoreInfoDrawer';
@@ -45,7 +45,6 @@ const ProfileHeader: React.FC<ProfileHeaderProps> = ({
const [drawerVisible, setDrawerVisible] = useState(false);
const [showBadgeView, setBadgeViewVisible] = useState(false);
- const [firstName, lastName] = [...name.split(' ')];
const containerRef = useRef(null);
const childRef = useRef(null);
@@ -86,6 +85,10 @@ const ProfileHeader: React.FC<ProfileHeaderProps> = ({
}
};
+ useEffect(() => {
+ setDrawerVisible(drawerVisible);
+ }, [drawerVisible]);
+
return (
<View ref={containerRef} style={styles.container}>
<ProfileMoreInfoDrawer
@@ -96,6 +99,7 @@ const ProfileHeader: React.FC<ProfileHeaderProps> = ({
userXName={userXName}
setIsOpen={setDrawerVisible}
/>
+
{userId === loggedInUserId && measure && (
<BadgeTutorial
uniIconProps={{
@@ -107,21 +111,15 @@ const ProfileHeader: React.FC<ProfileHeaderProps> = ({
/>
)}
<View style={styles.row}>
- <Avatar
+ <TaggAvatar
style={styles.avatar}
userXId={userXId}
screenType={screenType}
/>
<View style={styles.header}>
- {name.length <= 16 ? (
- <Text style={styles.name}>{name}</Text>
- ) : (
- <View>
- <Text style={styles.name}>{firstName}</Text>
- <Text style={styles.name}>{lastName}</Text>
- </View>
- )}
-
+ <Text style={styles.name} numberOfLines={2}>
+ {name}
+ </Text>
<View style={styles.friendsAndUniversity}>
<FriendsCount screenType={screenType} userXId={userXId} />
<TouchableOpacity
diff --git a/src/components/profile/ProfilePreview.tsx b/src/components/profile/ProfilePreview.tsx
index bea989d9..66d68d8f 100644
--- a/src/components/profile/ProfilePreview.tsx
+++ b/src/components/profile/ProfilePreview.tsx
@@ -2,7 +2,6 @@ import {useNavigation} from '@react-navigation/native';
import React, {useEffect, useState} from 'react';
import {
Alert,
- Image,
StyleSheet,
Text,
TouchableOpacity,
@@ -23,6 +22,7 @@ import {
userXInStore,
} from '../../utils';
import {addUserToRecentlySearched} from '../../utils/search';
+import {Avatar} from '../common';
/**
* This component returns user's profile picture friended by username as a touchable component.
@@ -48,7 +48,7 @@ const ProfilePreview: React.FC<ProfilePreviewProps> = ({
}) => {
const navigation = useNavigation();
const {user: loggedInUser} = useSelector((state: RootState) => state.user);
- const [avatar, setAvatar] = useState<string | null>(null);
+ const [avatar, setAvatar] = useState<string>();
const dispatch = useDispatch();
useEffect(() => {
@@ -187,14 +187,7 @@ const ProfilePreview: React.FC<ProfilePreviewProps> = ({
<TouchableOpacity
onPress={addToRecentlyStoredAndNavigateToProfile}
style={containerStyle}>
- <Image
- style={avatarStyle}
- source={
- avatar
- ? {uri: avatar}
- : require('../../assets/images/avatar-placeholder.png')
- }
- />
+ <Avatar style={avatarStyle} uri={avatar} />
<View style={nameContainerStyle}>
{(previewType === 'Search' || previewType === 'Recent') && (
<>
diff --git a/src/components/profile/Avatar.tsx b/src/components/profile/TaggAvatar.tsx
index e57a56a3..ea0bdb65 100644
--- a/src/components/profile/Avatar.tsx
+++ b/src/components/profile/TaggAvatar.tsx
@@ -1,28 +1,27 @@
import React from 'react';
-import {Image, StyleSheet} from 'react-native';
+import {StyleSheet} from 'react-native';
import {useSelector} from 'react-redux';
import {RootState} from '../../store/rootreducer';
import {ScreenType} from '../../types';
+import {Avatar} from '../common';
const PROFILE_DIM = 100;
-interface AvatarProps {
+interface TaggAvatarProps {
style?: object;
userXId: string | undefined;
screenType: ScreenType;
}
-const Avatar: React.FC<AvatarProps> = ({style, screenType, userXId}) => {
+const TaggAvatar: React.FC<TaggAvatarProps> = ({
+ style,
+ screenType,
+ userXId,
+}) => {
const {avatar} = useSelector((state: RootState) =>
userXId ? state.userX[screenType][userXId] : state.user,
);
- return (
- <Image
- style={[styles.image, style]}
- defaultSource={require('../../assets/images/avatar-placeholder.png')}
- source={{uri: avatar, cache: 'reload'}}
- />
- );
+ return <Avatar style={[styles.image, style]} uri={avatar} />;
};
const styles = StyleSheet.create({
@@ -33,4 +32,4 @@ const styles = StyleSheet.create({
},
});
-export default Avatar;
+export default TaggAvatar;
diff --git a/src/components/profile/index.ts b/src/components/profile/index.ts
index 260f4217..c544c3f2 100644
--- a/src/components/profile/index.ts
+++ b/src/components/profile/index.ts
@@ -8,3 +8,4 @@ export {default as Friends} from './Friends';
export {default as ProfileMoreInfoDrawer} from './ProfileMoreInfoDrawer';
export {default as MomentMoreInfoDrawer} from './MomentMoreInfoDrawer';
export {default as UniversityIcon} from './UniversityIcon';
+export {default as TaggAvatar} from './TaggAvatar';
diff --git a/src/components/search/ExploreSectionUser.tsx b/src/components/search/ExploreSectionUser.tsx
index d8c92be2..c949acd4 100644
--- a/src/components/search/ExploreSectionUser.tsx
+++ b/src/components/search/ExploreSectionUser.tsx
@@ -1,13 +1,13 @@
import {useNavigation} from '@react-navigation/native';
import React, {useEffect, useState} from 'react';
-import {TextStyle, ViewStyle} from 'react-native';
import {
- Image,
StyleProp,
StyleSheet,
Text,
+ TextStyle,
TouchableOpacity,
ViewProps,
+ ViewStyle,
} from 'react-native';
import LinearGradient from 'react-native-linear-gradient';
import {useDispatch, useSelector, useStore} from 'react-redux';
@@ -15,6 +15,7 @@ import {loadImageFromURL} from '../../services';
import {RootState} from '../../store/rootReducer';
import {ProfilePreviewType, ScreenType} from '../../types';
import {fetchUserX, normalize, userXInStore} from '../../utils';
+import {Avatar} from '../common';
/**
* Search Screen for user recommendations and a search
@@ -30,7 +31,7 @@ const ExploreSectionUser: React.FC<ExploreSectionUserProps> = ({
externalStyles,
}) => {
const {id, username, first_name, last_name} = user;
- const [avatar, setAvatar] = useState<string | null>(null);
+ const [avatar, setAvatar] = useState<string>();
const navigation = useNavigation();
const {user: loggedInUser} = useSelector((state: RootState) => state.user);
const state: RootState = useStore().getState();
@@ -71,14 +72,7 @@ const ExploreSectionUser: React.FC<ExploreSectionUserProps> = ({
angle={90}
angleCenter={{x: 0.5, y: 0.5}}
style={styles.gradient}>
- <Image
- source={
- avatar
- ? {uri: avatar}
- : require('../../assets/images/avatar-placeholder.png')
- }
- style={styles.profile}
- />
+ <Avatar style={styles.profile} uri={avatar} />
</LinearGradient>
<Text style={[styles.name, externalStyles?.name]} numberOfLines={2}>
{first_name} {last_name}
diff --git a/src/components/search/SearchBar.tsx b/src/components/search/SearchBar.tsx
index ea36d58b..8a53178b 100644
--- a/src/components/search/SearchBar.tsx
+++ b/src/components/search/SearchBar.tsx
@@ -1,6 +1,7 @@
import React, {useEffect, useState} from 'react';
import {
Keyboard,
+ LayoutChangeEvent,
NativeSyntheticEvent,
StyleSheet,
Text,
@@ -10,14 +11,12 @@ import {
TouchableOpacity,
View,
ViewStyle,
- LayoutChangeEvent,
} from 'react-native';
-import {normalize} from 'react-native-elements';
import Animated, {useAnimatedStyle} from 'react-native-reanimated';
import Icon from 'react-native-vector-icons/Feather';
import {useSelector} from 'react-redux';
import {RootState} from '../../store/rootReducer';
-import {getSearchSuggestions} from '../../utils';
+import {getSearchSuggestions, normalize} from '../../utils';
const AnimatedIcon = Animated.createAnimatedComponent(Icon);
@@ -166,9 +165,9 @@ const styles = StyleSheet.create({
},
input: {
flex: 1,
- fontSize: 16,
+ fontSize: normalize(19),
color: '#000',
- letterSpacing: normalize(0.5),
+ letterSpacing: 0.5,
},
cancelButton: {
height: '100%',
diff --git a/src/components/search/SearchResultCell.tsx b/src/components/search/SearchResultCell.tsx
index 5a6ea110..16e62a53 100644
--- a/src/components/search/SearchResultCell.tsx
+++ b/src/components/search/SearchResultCell.tsx
@@ -22,10 +22,10 @@ import {
} from '../../utils';
import {
checkIfUserIsBlocked,
- defaultUserProfile,
fetchUserX,
userXInStore,
} from '../../utils/users';
+import {Avatar} from '../common';
interface SearchResults {
profileData: ProfilePreviewType;
@@ -129,11 +129,7 @@ const SearchResultsCell: React.FC<SearchResults> = ({
<TouchableOpacity
onPress={addToRecentlyStoredAndNavigateToProfile}
style={styles.cellContainer}>
- <Image
- defaultSource={defaultUserProfile()}
- source={{uri: avatar}}
- style={styles.imageContainer}
- />
+ <Avatar style={styles.imageContainer} uri={avatar} />
<View style={[styles.initialTextContainer, styles.multiText]}>
<Text style={styles.initialTextStyle}>{`@${username}`}</Text>
<Text style={styles.secondaryTextStyle}>
diff --git a/src/components/suggestedPeople/MutualFriends.tsx b/src/components/suggestedPeople/MutualFriends.tsx
index f72104d4..3e2dc851 100644
--- a/src/components/suggestedPeople/MutualFriends.tsx
+++ b/src/components/suggestedPeople/MutualFriends.tsx
@@ -1,10 +1,9 @@
import React, {useState} from 'react';
import {SafeAreaView, StyleSheet, Text, View} from 'react-native';
-import {normalize} from 'react-native-elements';
import {ScrollView, TouchableOpacity} from 'react-native-gesture-handler';
import {BottomDrawer, TabsGradient} from '../../components';
import {ProfilePreviewType, ScreenType} from '../../types';
-import {isIPhoneX, SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils';
+import {isIPhoneX, normalize, SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils';
import {ProfilePreview} from '../profile';
interface MutualFriendsProps {
diff --git a/src/components/taggs/TwitterTaggPost.tsx b/src/components/taggs/TwitterTaggPost.tsx
index 0a6f53d8..db07ba66 100644
--- a/src/components/taggs/TwitterTaggPost.tsx
+++ b/src/components/taggs/TwitterTaggPost.tsx
@@ -11,7 +11,7 @@ import {
} from '../../constants';
import {TwitterPostType} from '../../types';
import {handleOpenSocialUrlOnBrowser, SCREEN_WIDTH} from '../../utils';
-import {DateLabel, PostCarousel} from '../common';
+import {Avatar, DateLabel, PostCarousel} from '../common';
interface TwitterTaggPostProps {
ownerHandle: string;
@@ -31,14 +31,7 @@ const TwitterTaggPost: React.FC<TwitterTaggPostProps> = ({
)}
{/* Post header (avatar and handle) */}
<View style={styles.header}>
- <Image
- style={styles.avatar}
- source={
- post.profile_pic
- ? {uri: post.profile_pic}
- : require('../../assets/images/avatar-placeholder.png')
- }
- />
+ <Avatar style={styles.avatar} uri={post.profile_pic} />
<Text
style={styles.headerText}
onPress={() => handleOpenSocialUrlOnBrowser(post.handle, 'Twitter')}>
@@ -84,13 +77,9 @@ const TwitterTaggPost: React.FC<TwitterTaggPostProps> = ({
<View style={styles.replyHeader}>
{post.in_reply_to.text !== 'This tweet is unavailable' && (
<>
- <Image
- style={styles.replyAvatar}
- source={
- post.in_reply_to.profile_pic
- ? {uri: post.in_reply_to.profile_pic}
- : require('../../assets/images/avatar-placeholder.png')
- }
+ <Avatar
+ style={styles.avatar}
+ uri={post.in_reply_to.profile_pic}
/>
<Text
style={styles.replyHandleText}