aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/assets/badges/aap.pngbin0 -> 11184 bytes
-rw-r--r--src/assets/badges/cals.pngbin0 -> 11952 bytes
-rw-r--r--src/assets/badges/college_of_arts_and_sciences.pngbin0 -> 13632 bytes
-rw-r--r--src/assets/badges/college_of_engineering.pngbin0 -> 10165 bytes
-rw-r--r--src/assets/badges/college_of_human_ecology.pngbin0 -> 13524 bytes
-rw-r--r--src/assets/badges/college_of_veterinary_medicine.pngbin0 -> 13496 bytes
-rw-r--r--src/assets/badges/cornell_law_school.pngbin0 -> 12782 bytes
-rw-r--r--src/assets/badges/cornell_tech.pngbin0 -> 8537 bytes
-rw-r--r--src/assets/badges/dyson_school.pngbin0 -> 14758 bytes
-rw-r--r--src/assets/badges/entrepreneurship_at_cornell.pngbin0 -> 12587 bytes
-rw-r--r--src/assets/badges/graduate_school.pngbin0 -> 14748 bytes
-rw-r--r--src/assets/badges/hotel_administration.pngbin0 -> 10646 bytes
-rw-r--r--src/assets/badges/ilr.pngbin0 -> 17000 bytes
-rw-r--r--src/assets/badges/sc_johnson_school_of_management.pngbin0 -> 11346 bytes
-rw-r--r--src/assets/badges/student_agencies.pngbin0 -> 18373 bytes
-rw-r--r--src/assets/badges/weill_cornell_medical_sciences.pngbin0 -> 17096 bytes
-rw-r--r--src/assets/badges/weill_cornell_medicine.pngbin0 -> 16821 bytes
-rw-r--r--src/assets/icons/grey-purple-plus.svg5
-rw-r--r--src/assets/icons/purple-plus.svg15
-rw-r--r--src/assets/images/heart-filled.pngbin0 -> 1208 bytes
-rw-r--r--src/assets/images/heart-outlined.pngbin0 -> 1055 bytes
-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
-rw-r--r--src/constants/api.ts2
-rw-r--r--src/constants/badges.ts110
-rw-r--r--src/routes/main/MainStackNavigator.tsx11
-rw-r--r--src/routes/main/MainStackScreen.tsx8
-rw-r--r--src/screens/badge/BadgeItem.tsx33
-rw-r--r--src/screens/profile/CommentReactionScreen.tsx69
-rw-r--r--src/screens/profile/FriendsListScreen.tsx4
-rw-r--r--src/screens/profile/MomentCommentsScreen.tsx2
-rw-r--r--src/screens/profile/index.ts1
-rw-r--r--src/services/CommentService.ts95
-rw-r--r--src/types/types.ts11
-rw-r--r--src/utils/comments.tsx20
-rw-r--r--src/utils/common.ts17
-rw-r--r--src/utils/users.ts82
51 files changed, 1082 insertions, 250 deletions
diff --git a/src/assets/badges/aap.png b/src/assets/badges/aap.png
new file mode 100644
index 00000000..dd150b8f
--- /dev/null
+++ b/src/assets/badges/aap.png
Binary files differ
diff --git a/src/assets/badges/cals.png b/src/assets/badges/cals.png
new file mode 100644
index 00000000..fbf0717b
--- /dev/null
+++ b/src/assets/badges/cals.png
Binary files differ
diff --git a/src/assets/badges/college_of_arts_and_sciences.png b/src/assets/badges/college_of_arts_and_sciences.png
new file mode 100644
index 00000000..146a06ed
--- /dev/null
+++ b/src/assets/badges/college_of_arts_and_sciences.png
Binary files differ
diff --git a/src/assets/badges/college_of_engineering.png b/src/assets/badges/college_of_engineering.png
new file mode 100644
index 00000000..c3f9e889
--- /dev/null
+++ b/src/assets/badges/college_of_engineering.png
Binary files differ
diff --git a/src/assets/badges/college_of_human_ecology.png b/src/assets/badges/college_of_human_ecology.png
new file mode 100644
index 00000000..14a9fd80
--- /dev/null
+++ b/src/assets/badges/college_of_human_ecology.png
Binary files differ
diff --git a/src/assets/badges/college_of_veterinary_medicine.png b/src/assets/badges/college_of_veterinary_medicine.png
new file mode 100644
index 00000000..1814aa97
--- /dev/null
+++ b/src/assets/badges/college_of_veterinary_medicine.png
Binary files differ
diff --git a/src/assets/badges/cornell_law_school.png b/src/assets/badges/cornell_law_school.png
new file mode 100644
index 00000000..43104e41
--- /dev/null
+++ b/src/assets/badges/cornell_law_school.png
Binary files differ
diff --git a/src/assets/badges/cornell_tech.png b/src/assets/badges/cornell_tech.png
new file mode 100644
index 00000000..c40d3889
--- /dev/null
+++ b/src/assets/badges/cornell_tech.png
Binary files differ
diff --git a/src/assets/badges/dyson_school.png b/src/assets/badges/dyson_school.png
new file mode 100644
index 00000000..d17663b7
--- /dev/null
+++ b/src/assets/badges/dyson_school.png
Binary files differ
diff --git a/src/assets/badges/entrepreneurship_at_cornell.png b/src/assets/badges/entrepreneurship_at_cornell.png
new file mode 100644
index 00000000..6b86abd4
--- /dev/null
+++ b/src/assets/badges/entrepreneurship_at_cornell.png
Binary files differ
diff --git a/src/assets/badges/graduate_school.png b/src/assets/badges/graduate_school.png
new file mode 100644
index 00000000..f1c4006e
--- /dev/null
+++ b/src/assets/badges/graduate_school.png
Binary files differ
diff --git a/src/assets/badges/hotel_administration.png b/src/assets/badges/hotel_administration.png
new file mode 100644
index 00000000..a92cfa3d
--- /dev/null
+++ b/src/assets/badges/hotel_administration.png
Binary files differ
diff --git a/src/assets/badges/ilr.png b/src/assets/badges/ilr.png
new file mode 100644
index 00000000..549d6955
--- /dev/null
+++ b/src/assets/badges/ilr.png
Binary files differ
diff --git a/src/assets/badges/sc_johnson_school_of_management.png b/src/assets/badges/sc_johnson_school_of_management.png
new file mode 100644
index 00000000..3fc24aa4
--- /dev/null
+++ b/src/assets/badges/sc_johnson_school_of_management.png
Binary files differ
diff --git a/src/assets/badges/student_agencies.png b/src/assets/badges/student_agencies.png
new file mode 100644
index 00000000..ac31a1ee
--- /dev/null
+++ b/src/assets/badges/student_agencies.png
Binary files differ
diff --git a/src/assets/badges/weill_cornell_medical_sciences.png b/src/assets/badges/weill_cornell_medical_sciences.png
new file mode 100644
index 00000000..2167df79
--- /dev/null
+++ b/src/assets/badges/weill_cornell_medical_sciences.png
Binary files differ
diff --git a/src/assets/badges/weill_cornell_medicine.png b/src/assets/badges/weill_cornell_medicine.png
new file mode 100644
index 00000000..3e8a60b3
--- /dev/null
+++ b/src/assets/badges/weill_cornell_medicine.png
Binary files differ
diff --git a/src/assets/icons/grey-purple-plus.svg b/src/assets/icons/grey-purple-plus.svg
new file mode 100644
index 00000000..2053d4a7
--- /dev/null
+++ b/src/assets/icons/grey-purple-plus.svg
@@ -0,0 +1,5 @@
+<svg width="31" height="31" viewBox="0 0 31 31" fill="none" xmlns="http://www.w3.org/2000/svg">
+<circle cx="15.5" cy="15.5" r="15.5" fill="white"/>
+<rect width="2.38462" height="16.6923" rx="1.19231" transform="matrix(-1 0 0 1 16.6934 7.15381)" fill="#8F00FF"/>
+<rect width="2.38462" height="16.6923" rx="1.19231" transform="matrix(0.00550217 0.999985 0.999985 -0.00550217 7.1543 14.4004)" fill="#8F00FF"/>
+</svg>
diff --git a/src/assets/icons/purple-plus.svg b/src/assets/icons/purple-plus.svg
new file mode 100644
index 00000000..20949b6d
--- /dev/null
+++ b/src/assets/icons/purple-plus.svg
@@ -0,0 +1,15 @@
+<svg width="23" height="23" viewBox="0 0 23 23" fill="none" xmlns="http://www.w3.org/2000/svg">
+<circle cx="11.5" cy="11.5" r="11.25" fill="url(#paint0_linear)" stroke="url(#paint1_linear)" stroke-width="0.5"/>
+<rect width="1.76923" height="12.3846" rx="0.884615" transform="matrix(-1 0 0 1 12.3848 5.30762)" fill="white"/>
+<rect width="1.76923" height="12.3846" rx="0.884615" transform="matrix(0.00550217 0.999985 0.999985 -0.00550217 5.30859 10.6841)" fill="white"/>
+<defs>
+<linearGradient id="paint0_linear" x1="11.5" y1="0" x2="10.9524" y2="30.119" gradientUnits="userSpaceOnUse">
+<stop stop-color="#8F01FF"/>
+<stop offset="1" stop-color="#6EE7E7"/>
+</linearGradient>
+<linearGradient id="paint1_linear" x1="11.5" y1="0" x2="10.9524" y2="32.8571" gradientUnits="userSpaceOnUse">
+<stop stop-color="#8F01FF"/>
+<stop offset="1" stop-color="#6EE7E7"/>
+</linearGradient>
+</defs>
+</svg>
diff --git a/src/assets/images/heart-filled.png b/src/assets/images/heart-filled.png
new file mode 100644
index 00000000..59bf0ab1
--- /dev/null
+++ b/src/assets/images/heart-filled.png
Binary files differ
diff --git a/src/assets/images/heart-outlined.png b/src/assets/images/heart-outlined.png
new file mode 100644
index 00000000..aeb87a99
--- /dev/null
+++ b/src/assets/images/heart-outlined.png
Binary files differ
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 ? (
diff --git a/src/constants/api.ts b/src/constants/api.ts
index 41c54d6f..3c7e669e 100644
--- a/src/constants/api.ts
+++ b/src/constants/api.ts
@@ -33,6 +33,8 @@ export const MOMENTS_ENDPOINT: string = API_URL + 'moments/';
export const MOMENT_THUMBNAIL_ENDPOINT: string = API_URL + 'moment-thumbnail/';
export const VERIFY_INVITATION_CODE_ENDPOUNT: string = API_URL + 'verify-code/';
export const COMMENTS_ENDPOINT: string = API_URL + 'comments/';
+export const COMMENT_REACTIONS_ENDPOINT: string = API_URL + 'reaction-comment/';
+export const COMMENT_REACTIONS_REPLY_ENDPOINT: string = API_URL + 'reaction-reply/';
export const FRIENDS_ENDPOINT: string = API_URL + 'friends/';
export const ALL_USERS_ENDPOINT: string = API_URL + 'users/';
export const REPORT_ISSUE_ENDPOINT: string = API_URL + 'report/';
diff --git a/src/constants/badges.ts b/src/constants/badges.ts
index aca53f26..54979ecd 100644
--- a/src/constants/badges.ts
+++ b/src/constants/badges.ts
@@ -1,7 +1,7 @@
import {BadgeDataType} from '../types';
export const _badgeImages = {
- iff: require('../assets/badges/iff.png'),
+ aap: require('../assets/badges/aap.png'),
acacia: require('../assets/badges/acacia.png'),
acapella: require('../assets/badges/acapella.png'),
alpha_chi_omega: require('../assets/badges/alpha_chi_omega.png'),
@@ -26,8 +26,15 @@ export const _badgeImages = {
brown_womens_collective: require('../assets/badges/brown_womens_collective.png'),
bsu: require('../assets/badges/bsu.png'),
buxton_international: require('../assets/badges/buxton_international.png'),
+ cals: require('../assets/badges/cals.png'),
chi_phi: require('../assets/badges/chi_phi.png'),
chi_psi: require('../assets/badges/chi_psi.png'),
+ college_of_arts_and_sciences: require('../assets/badges/college_of_arts_and_sciences.png'),
+ college_of_engineering: require('../assets/badges/college_of_engineering.png'),
+ college_of_human_ecology: require('../assets/badges/college_of_human_ecology.png'),
+ college_of_veterinary_medicine: require('../assets/badges/college_of_veterinary_medicine.png'),
+ cornell_law_school: require('../assets/badges/cornell_law_school.png'),
+ cornell_tech: require('../assets/badges/cornell_tech.png'),
delta_chi: require('../assets/badges/delta_chi.png'),
delta_delta_delta: require('../assets/badges/delta_delta_delta.png'),
delta_gamma: require('../assets/badges/delta_gamma.png'),
@@ -36,13 +43,19 @@ export const _badgeImages = {
delta_tau: require('../assets/badges/delta_tau.png'),
delta_tau_delta: require('../assets/badges/delta_tau_delta.png'),
delta_upsilon: require('../assets/badges/delta_upsilon.png'),
+ dyson_school: require('../assets/badges/dyson_school.png'),
+ entrepreneurship_at_cornell: require('../assets/badges/entrepreneurship_at_cornell.png'),
fashion_at_brown: require('../assets/badges/fashion_at_brown.png'),
fencing: require('../assets/badges/fencing.png'),
field_hockey: require('../assets/badges/field_hockey.png'),
football: require('../assets/badges/football.png'),
golf: require('../assets/badges/golf.png'),
+ graduate_school: require('../assets/badges/graduate_school.png'),
gymnastics: require('../assets/badges/gymnastics.png'),
hockey: require('../assets/badges/hockey.png'),
+ hotel_administration: require('../assets/badges/hotel_administration.png'),
+ iff: require('../assets/badges/iff.png'),
+ ilr: require('../assets/badges/ilr.png'),
impulse_and_mezcla: require('../assets/badges/impulse_and_mezcla.png'),
kappa_alpha_theta: require('../assets/badges/kappa_alpha_theta.png'),
kappa_delta: require('../assets/badges/kappa_delta.png'),
@@ -69,6 +82,7 @@ export const _badgeImages = {
polo: require('../assets/badges/polo.png'),
rowing: require('../assets/badges/rowing.png'),
sailing: require('../assets/badges/sailing.png'),
+ sc_johnson_school_of_management: require('../assets/badges/sc_johnson_school_of_management.png'),
sigma_alpha_mu: require('../assets/badges/sigma_alpha_mu.png'),
sigma_chi: require('../assets/badges/sigma_chi.png'),
sigma_delta_tau: require('../assets/badges/sigma_delta_tau.png'),
@@ -77,6 +91,7 @@ export const _badgeImages = {
soccer: require('../assets/badges/soccer.png'),
softball: require('../assets/badges/softball.png'),
squash: require('../assets/badges/squash.png'),
+ student_agencies: require('../assets/badges/student_agencies.png'),
swimming_and_diving: require('../assets/badges/swimming_and_diving.png'),
tap: require('../assets/badges/tap.png'),
tennis: require('../assets/badges/tennis.png'),
@@ -86,6 +101,8 @@ export const _badgeImages = {
track_and_field: require('../assets/badges/track_and_field.png'),
ucs: require('../assets/badges/ucs.png'),
volleyball: require('../assets/badges/volleyball.png'),
+ weill_cornell_medical_sciences: require('../assets/badges/weill_cornell_medical_sciences.png'),
+ weill_cornell_medicine: require('../assets/badges/weill_cornell_medicine.png'),
women_in_business: require('../assets/badges/women_in_business.png'),
wrestling: require('../assets/badges/wrestling.png'),
zeta_beta_tau: require('../assets/badges/zeta_beta_tau.png'),
@@ -244,13 +261,13 @@ const _brownUniversityBadges = [
badgeImage: _badgeImages.fashion_at_brown,
},
{
- badgeName: 'Impulse',
- badgeImage: _badgeImages.impulse_and_mezcla,
- },
- {
badgeName: 'Ivy Film Festival ',
badgeImage: _badgeImages.iff,
},
+ {
+ badgeName: 'Impulse',
+ badgeImage: _badgeImages.impulse_and_mezcla,
+ },
],
},
{
@@ -629,6 +646,89 @@ const _cornellUniversityBadges = [
},
],
},
+ {
+ title: 'School',
+ data: [
+ {
+ badgeName: 'College of Agriculture and Life Sciences or (CALS)',
+ badgeImage: _badgeImages.cals,
+ },
+ {
+ badgeName: 'College of Architecture, Art and Planning or (AAP)',
+ badgeImage: _badgeImages.aap,
+ },
+ {
+ badgeName: 'College of Arts and Sciences ',
+ badgeImage: _badgeImages.college_of_arts_and_sciences,
+ },
+ {
+ badgeName: 'Hotel Administration ',
+ badgeImage: _badgeImages.hotel_administration,
+ },
+ {
+ badgeName: 'Dyson School',
+ badgeImage: _badgeImages.dyson_school,
+ },
+ {
+ badgeName: 'College of Engineering ',
+ badgeImage: _badgeImages.college_of_engineering,
+ },
+ {
+ badgeName: 'College of Human Ecology',
+ badgeImage: _badgeImages.college_of_human_ecology,
+ },
+ {
+ badgeName: 'School of Industrial and Labor Relations or ILR',
+ badgeImage: _badgeImages.ilr,
+ },
+ ],
+ },
+ {
+ title: 'Graduate Program',
+ data: [
+ {
+ badgeName: 'SC Johnson School of Management',
+ badgeImage: _badgeImages.sc_johnson_school_of_management,
+ },
+ {
+ badgeName: 'Conrell Tech (NYC)',
+ badgeImage: _badgeImages.cornell_tech,
+ },
+ {
+ badgeName: 'Cornell Law School',
+ badgeImage: _badgeImages.cornell_law_school,
+ },
+ {
+ badgeName: 'College of Veterinary Medicine',
+ badgeImage: _badgeImages.college_of_veterinary_medicine,
+ },
+ {
+ badgeName: 'Graduate School',
+ badgeImage: _badgeImages.graduate_school,
+ },
+ {
+ badgeName: 'Weill Cornell Medicine (NYC)',
+ badgeImage: _badgeImages.weill_cornell_medicine,
+ },
+ {
+ badgeName: 'Weill Cornell Medical Sciences (NYC)',
+ badgeImage: _badgeImages.weill_cornell_medical_sciences,
+ },
+ ],
+ },
+ {
+ title: 'Network',
+ data: [
+ {
+ badgeName: 'Entrepreneurship @ Cornell ',
+ badgeImage: _badgeImages.entrepreneurship_at_cornell,
+ },
+ {
+ badgeName: 'Student Agencies, INC',
+ badgeImage: _badgeImages.student_agencies,
+ },
+ ],
+ },
];
export const BADGE_DATA: BadgeDataType = {
diff --git a/src/routes/main/MainStackNavigator.tsx b/src/routes/main/MainStackNavigator.tsx
index 1f173569..3b183cc0 100644
--- a/src/routes/main/MainStackNavigator.tsx
+++ b/src/routes/main/MainStackNavigator.tsx
@@ -3,7 +3,12 @@
*/
import {createStackNavigator} from '@react-navigation/stack';
import {Image} from 'react-native-image-crop-picker';
-import {MomentType, ScreenType, SearchCategoryType} from '../../types';
+import {
+ CommentBaseType,
+ MomentType,
+ ScreenType,
+ SearchCategoryType,
+} from '../../types';
export type MainStackParams = {
SuggestedPeople: {
@@ -46,6 +51,10 @@ export type MainStackParams = {
screenType: ScreenType;
comment_id?: string;
};
+ CommentReactionScreen: {
+ comment: CommentBaseType;
+ screenType: ScreenType;
+ };
FriendsListScreen: {
userXId: string | undefined;
screenType: ScreenType;
diff --git a/src/routes/main/MainStackScreen.tsx b/src/routes/main/MainStackScreen.tsx
index f5100e58..d76f9137 100644
--- a/src/routes/main/MainStackScreen.tsx
+++ b/src/routes/main/MainStackScreen.tsx
@@ -12,6 +12,7 @@ import {
CategorySelection,
ChatListScreen,
ChatScreen,
+ CommentReactionScreen,
CreateCustomCategory,
DiscoverUsers,
EditProfile,
@@ -217,6 +218,13 @@ const MainStackScreen: React.FC<MainStackProps> = ({route}) => {
}}
/>
<MainStack.Screen
+ name="CommentReactionScreen"
+ component={CommentReactionScreen}
+ options={{
+ ...headerBarOptions('black', 'Likes'),
+ }}
+ />
+ <MainStack.Screen
name="MomentUploadPrompt"
component={MomentUploadPromptScreen}
initialParams={{screenType}}
diff --git a/src/screens/badge/BadgeItem.tsx b/src/screens/badge/BadgeItem.tsx
index 1051d4a7..e4f1b1da 100644
--- a/src/screens/badge/BadgeItem.tsx
+++ b/src/screens/badge/BadgeItem.tsx
@@ -43,9 +43,15 @@ const BadgeItem: React.FC<BadgeItemProps> = ({
style={styles.item}>
<View style={styles.detailContainer}>
<Image source={resourcePath} style={styles.imageStyles} />
- <View style={styles.textContainer}>
- <Text style={styles.title}>{title}</Text>
- </View>
+ <Text
+ style={[
+ styles.title,
+ title.length > 30
+ ? {fontSize: normalize(12), lineHeight: normalize(16)}
+ : {},
+ ]}>
+ {title}
+ </Text>
</View>
</LinearGradient>
</TouchableOpacity>
@@ -53,33 +59,30 @@ const BadgeItem: React.FC<BadgeItemProps> = ({
);
};
+const ITEM_WIDTH = SCREEN_WIDTH / 3 - 20;
+
const styles = StyleSheet.create({
border: {
- width: SCREEN_WIDTH / 3 - 20 + 6,
- height: 146,
+ width: ITEM_WIDTH + 6,
+ height: 156,
marginLeft: 10,
marginBottom: 12,
borderRadius: 8,
},
item: {
- width: SCREEN_WIDTH / 3 - 20,
- height: 140,
+ width: ITEM_WIDTH,
+ height: 150,
borderRadius: 8,
},
detailContainer: {
flexGrow: 1,
- justifyContent: 'center',
+ justifyContent: 'space-evenly',
alignItems: 'center',
- borderWidth: 3,
- borderRadius: 8,
- borderColor: 'transparent',
},
imageStyles: {
- width: 40,
- height: 40,
- marginTop: '11%',
+ width: normalize(50),
+ height: normalize(50),
},
- textContainer: {marginTop: '16%'},
title: {
fontSize: normalize(15),
fontWeight: '500',
diff --git a/src/screens/profile/CommentReactionScreen.tsx b/src/screens/profile/CommentReactionScreen.tsx
new file mode 100644
index 00000000..0596a184
--- /dev/null
+++ b/src/screens/profile/CommentReactionScreen.tsx
@@ -0,0 +1,69 @@
+import {RouteProp, useNavigation} from '@react-navigation/native';
+import React, {useEffect, useState} from 'react';
+import {Alert, ScrollView, StyleSheet, View} from 'react-native';
+import {SafeAreaView} from 'react-native-safe-area-context';
+import {Friends} from '../../components';
+import {ERROR_SOMETHING_WENT_WRONG} from '../../constants/strings';
+import {MainStackParams} from '../../routes/main';
+import {getUsersReactedToAComment} from '../../services';
+import {ProfilePreviewType} from '../../types';
+import {HeaderHeight, SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils';
+
+type CommentReactionScreenRouteProps = RouteProp<
+ MainStackParams,
+ 'CommentReactionScreen'
+>;
+
+interface CommentReactionScreenProps {
+ route: CommentReactionScreenRouteProps;
+}
+
+const CommentReactionScreen: React.FC<CommentReactionScreenProps> = ({
+ route,
+}) => {
+ const navigation = useNavigation();
+ const {comment, screenType} = route.params;
+ const [users, setUsers] = useState<ProfilePreviewType[]>([]);
+
+ useEffect(() => {
+ const loadUsers = async () => {
+ const response = await getUsersReactedToAComment(comment);
+ if (response.length !== 0) {
+ setUsers(response);
+ } else {
+ Alert.alert(ERROR_SOMETHING_WENT_WRONG);
+ navigation.goBack();
+ }
+ };
+ loadUsers();
+ }, []);
+
+ return (
+ <View style={styles.background}>
+ <SafeAreaView>
+ <ScrollView style={styles.container}>
+ <Friends
+ result={users}
+ screenType={screenType}
+ userId={undefined}
+ hideFriendsFeature
+ />
+ </ScrollView>
+ </SafeAreaView>
+ </View>
+ );
+};
+
+const styles = StyleSheet.create({
+ background: {
+ backgroundColor: 'white',
+ width: SCREEN_WIDTH,
+ height: SCREEN_HEIGHT,
+ },
+ container: {
+ marginTop: HeaderHeight,
+ height: SCREEN_HEIGHT - HeaderHeight,
+ },
+});
+
+export default CommentReactionScreen;
diff --git a/src/screens/profile/FriendsListScreen.tsx b/src/screens/profile/FriendsListScreen.tsx
index 1d10bc86..73364f3b 100644
--- a/src/screens/profile/FriendsListScreen.tsx
+++ b/src/screens/profile/FriendsListScreen.tsx
@@ -36,10 +36,6 @@ const FriendsListScreen: React.FC<FriendsListScreenProps> = ({route}) => {
};
const styles = StyleSheet.create({
- background: {
- backgroundColor: 'white',
- height: '100%',
- },
backButton: {
marginLeft: 10,
},
diff --git a/src/screens/profile/MomentCommentsScreen.tsx b/src/screens/profile/MomentCommentsScreen.tsx
index bf07ae30..4b332b56 100644
--- a/src/screens/profile/MomentCommentsScreen.tsx
+++ b/src/screens/profile/MomentCommentsScreen.tsx
@@ -102,7 +102,7 @@ const styles = StyleSheet.create({
},
body: {
marginTop: HeaderHeight,
- width: SCREEN_WIDTH * 0.9,
+ width: SCREEN_WIDTH * 0.95,
height: SCREEN_HEIGHT * 0.8,
paddingTop: '3%',
},
diff --git a/src/screens/profile/index.ts b/src/screens/profile/index.ts
index d5377494..ea0505a2 100644
--- a/src/screens/profile/index.ts
+++ b/src/screens/profile/index.ts
@@ -12,3 +12,4 @@ export {default as PrivacyScreen} from './PrivacyScreen';
export {default as AccountType} from './AccountType';
export {default as CategorySelection} from './CategorySelection';
export {default as CreateCustomCategory} from './CreateCustomCategory';
+export {default as CommentReactionScreen} from './CommentReactionScreen';
diff --git a/src/services/CommentService.ts b/src/services/CommentService.ts
index 2faaa8db..6d71ce9c 100644
--- a/src/services/CommentService.ts
+++ b/src/services/CommentService.ts
@@ -1,8 +1,18 @@
import AsyncStorage from '@react-native-community/async-storage';
import {Alert} from 'react-native';
-import {COMMENTS_ENDPOINT, COMMENT_THREAD_ENDPOINT} from '../constants';
+import {
+ COMMENTS_ENDPOINT,
+ COMMENT_REACTIONS_ENDPOINT,
+ COMMENT_REACTIONS_REPLY_ENDPOINT,
+ COMMENT_THREAD_ENDPOINT,
+} from '../constants';
import {ERROR_FAILED_TO_COMMENT} from '../constants/strings';
-import {CommentType} from '../types';
+import {
+ CommentThreadType,
+ CommentType,
+ ProfilePreviewType,
+ ReactionOptionsType,
+} from '../types';
export const getComments = async (
objectId: string,
@@ -116,3 +126,84 @@ export const deleteComment = async (id: string, isThread: boolean) => {
return false;
}
};
+
+/**
+ * If `user_reaction` is undefined, we like the comment, if `user_reaction`
+ * is defined, we unlike the comment.
+ *
+ * @param comment the comment object that contains `user_reaction` (or not)
+ * @returns
+ */
+export const handleLikeUnlikeComment = async (
+ comment: CommentType | CommentThreadType,
+ liked: boolean,
+) => {
+ try {
+ const isReply = 'parent_comment' in comment;
+ const token = await AsyncStorage.getItem('token');
+ let url = isReply
+ ? COMMENT_REACTIONS_REPLY_ENDPOINT
+ : COMMENT_REACTIONS_ENDPOINT;
+ if (liked) {
+ // unlike a comment
+ url += `${comment.comment_id}/?reaction_type=LIKE`;
+ const response = await fetch(url, {
+ method: 'DELETE',
+ headers: {
+ Authorization: 'Token ' + token,
+ },
+ });
+ return response.status === 200;
+ } else {
+ // like a comment
+ const form = new FormData();
+ form.append('comment_id', comment.comment_id);
+ form.append('reaction_type', ReactionOptionsType.Like);
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ Authorization: 'Token ' + token,
+ },
+ body: form,
+ });
+ return response.status === 200;
+ }
+ } catch (error) {
+ console.log('Unable to like/unlike a comment');
+ console.error(error);
+ }
+};
+
+export const getUsersReactedToAComment = async (
+ comment: CommentType | CommentThreadType,
+) => {
+ try {
+ const isReply = 'parent_comment' in comment;
+ const token = await AsyncStorage.getItem('token');
+ let url = isReply
+ ? COMMENT_REACTIONS_REPLY_ENDPOINT
+ : COMMENT_REACTIONS_ENDPOINT;
+ url += `?comment_id=${comment.comment_id}`;
+ const response = await fetch(url, {
+ method: 'GET',
+ headers: {
+ Authorization: 'Token ' + token,
+ },
+ });
+ const typedResponse: {
+ reaction: ReactionOptionsType;
+ user_list: ProfilePreviewType[];
+ }[] = await response.json();
+ for (const obj of typedResponse) {
+ if (obj.reaction === ReactionOptionsType.Like) {
+ return obj.user_list;
+ }
+ }
+ return [];
+ } catch (error) {
+ console.log('Unable to fetch list of users whom reacted to a comment');
+ console.error(error);
+ }
+ return [];
+};
diff --git a/src/types/types.ts b/src/types/types.ts
index 00501d49..e9975529 100644
--- a/src/types/types.ts
+++ b/src/types/types.ts
@@ -122,6 +122,8 @@ export interface CommentBaseType {
comment: string;
date_created: string;
commenter: ProfilePreviewType;
+ user_reaction: ReactionType | null;
+ reaction_count: number;
}
export interface CommentType extends CommentBaseType {
@@ -316,3 +318,12 @@ export type ChatContextType = {
>;
chatClient: StreamChat;
};
+
+export enum ReactionOptionsType {
+ Like = 'LIKE',
+}
+
+export type ReactionType = {
+ id: string;
+ type: ReactionOptionsType;
+};
diff --git a/src/utils/comments.tsx b/src/utils/comments.tsx
index 0d551682..5c17cefe 100644
--- a/src/utils/comments.tsx
+++ b/src/utils/comments.tsx
@@ -6,9 +6,11 @@ import {
Part,
PartType,
} from 'react-native-controlled-mentions';
+import {TouchableOpacity} from 'react-native-gesture-handler';
import TaggTypeahead from '../components/common/TaggTypeahead';
import {TAGG_LIGHT_BLUE} from '../constants';
import {UserType} from '../types';
+import {normalize} from './layouts';
/**
* Part renderer
@@ -28,9 +30,8 @@ const renderPart = (
// Mention type part
if (isMentionPartType(part.partType)) {
return (
- <Text
+ <TouchableOpacity
key={`${index}-${part.data?.trigger}`}
- style={part.partType.textStyle}
onPress={() => {
if (part.data) {
handlePress({
@@ -39,8 +40,8 @@ const renderPart = (
});
}
}}>
- {part.text}
- </Text>
+ <Text style={part.partType.textStyle}>{part.text}</Text>
+ </TouchableOpacity>
);
}
@@ -89,8 +90,15 @@ export const mentionPartTypes: (style: 'blue' | 'white') => PartType[] = (
isInsertSpaceAfterMention: true,
textStyle:
style === 'blue'
- ? {color: TAGG_LIGHT_BLUE}
- : {color: 'white', fontWeight: '800'},
+ ? {
+ color: TAGG_LIGHT_BLUE,
+ top: normalize(3),
+ }
+ : {
+ color: 'white',
+ fontWeight: '800',
+ top: normalize(7.5),
+ },
},
];
};
diff --git a/src/utils/common.ts b/src/utils/common.ts
index ce4ab7d1..95e77f64 100644
--- a/src/utils/common.ts
+++ b/src/utils/common.ts
@@ -180,3 +180,20 @@ const _crestIcon = (university: UniversityType) => {
return require('../assets/images/bwbadges.png');
}
};
+
+export const validateImageLink = async (url: string | undefined) => {
+ if (!url) {
+ return false;
+ }
+ return fetch(url)
+ .then((res) => {
+ if (res.status === 200) {
+ return true;
+ } else {
+ return false;
+ }
+ })
+ .catch((_) => {
+ return false;
+ });
+};
diff --git a/src/utils/users.ts b/src/utils/users.ts
index 334cb3c0..430c843f 100644
--- a/src/utils/users.ts
+++ b/src/utils/users.ts
@@ -1,3 +1,4 @@
+import {Alert} from 'react-native';
import AsyncStorage from '@react-native-community/async-storage';
import {INTEGRATED_SOCIAL_LIST} from '../constants';
import {isUserBlocked, loadSocialPosts, removeBadgesService} from '../services';
@@ -24,6 +25,8 @@ import {
UserType,
UniversityBadge,
} from './../types/types';
+import ImagePicker from 'react-native-image-crop-picker';
+import {patchEditProfile} from '../services';
const loadData = async (dispatch: AppDispatch, user: UserType) => {
await Promise.all([
@@ -240,3 +243,82 @@ export const navigateToProfile = async (
screenType,
});
};
+
+export const patchProfile = async (
+ title: 'profile' | 'header',
+ userId: string,
+) => {
+ let imageSettings = {};
+ let screenTitle: string;
+ let requestTitle: string;
+ let fileName: string;
+ switch (title) {
+ case 'header':
+ screenTitle = 'Select Header Picture';
+ requestTitle = 'largeProfilePicture';
+ fileName = 'large_profile_pic.jpg';
+ imageSettings = {
+ smartAlbums: [
+ 'Favorites',
+ 'RecentlyAdded',
+ 'SelfPortraits',
+ 'Screenshots',
+ 'UserLibrary',
+ ],
+ width: 580,
+ height: 580,
+ cropping: true,
+ cropperToolbarTitle: screenTitle,
+ mediaType: 'photo',
+ };
+ break;
+ case 'profile':
+ screenTitle = 'Select Profile Picture';
+ requestTitle = 'smallProfilePicture';
+ fileName = 'small_profile_pic.jpg';
+ imageSettings = {
+ smartAlbums: [
+ 'Favorites',
+ 'RecentlyAdded',
+ 'SelfPortraits',
+ 'Screenshots',
+ 'UserLibrary',
+ ],
+ width: 580,
+ height: 580,
+ cropping: true,
+ cropperToolbarTitle: screenTitle,
+ mediaType: 'photo',
+ cropperCircleOverlay: true,
+ };
+ break;
+ default:
+ screenTitle = '';
+ requestTitle = '';
+ fileName = '';
+ }
+
+ return await ImagePicker.openPicker(imageSettings)
+ .then((picture) => {
+ if ('path' in picture) {
+ const request = new FormData();
+ request.append(requestTitle, {
+ uri: picture.path,
+ name: fileName,
+ type: 'image/jpg',
+ });
+
+ return patchEditProfile(request, userId)
+ .then((_) => {
+ return true;
+ })
+ .catch((error) => {
+ Alert.alert(error);
+ return false;
+ });
+ }
+ })
+ .catch((_) => {
+ return false;
+ });
+};