aboutsummaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/comments/CommentsContainer.tsx2
-rw-r--r--src/components/comments/MentionInputControlled.tsx97
-rw-r--r--src/components/common/Avatar.tsx23
-rw-r--r--src/components/common/BottomDrawer.tsx11
-rw-r--r--src/components/moments/MomentPostHeader.tsx16
-rw-r--r--src/components/profile/Cover.tsx114
-rw-r--r--src/components/profile/ProfileHeader.tsx5
-rw-r--r--src/components/profile/TaggAvatar.tsx95
8 files changed, 276 insertions, 87 deletions
diff --git a/src/components/comments/CommentsContainer.tsx b/src/components/comments/CommentsContainer.tsx
index 595ec743..d839ef38 100644
--- a/src/components/comments/CommentsContainer.tsx
+++ b/src/components/comments/CommentsContainer.tsx
@@ -49,7 +49,7 @@ const CommentsContainer: React.FC<CommentsContainerProps> = ({
count += 1 + comments[i].replies_count;
}
return count;
- }
+ };
useEffect(() => {
const loadComments = async () => {
diff --git a/src/components/comments/MentionInputControlled.tsx b/src/components/comments/MentionInputControlled.tsx
index 6abcb566..2fd2b41d 100644
--- a/src/components/comments/MentionInputControlled.tsx
+++ b/src/components/comments/MentionInputControlled.tsx
@@ -6,7 +6,6 @@ import {
TextInputSelectionChangeEventData,
View,
} from 'react-native';
-
import {
MentionInputProps,
MentionPartType,
@@ -43,16 +42,16 @@ const MentionInputControlled: FC<MentionInputProps> = ({
const validRegex = () => {
if (partTypes.length === 0) {
- return /.*\@[^ ]*$/;
+ return /.*@[^ ]*$/;
} else {
- return new RegExp(`.*\@${keywordByTrigger[partTypes[0].trigger]}.*$`);
+ return new RegExp(`.*@${keywordByTrigger[partTypes[0].trigger]}.*$`);
}
};
- const {plainText, parts} = useMemo(() => parseValue(value, partTypes), [
- value,
- partTypes,
- ]);
+ const {plainText, parts} = useMemo(
+ () => parseValue(value, partTypes),
+ [value, partTypes],
+ );
const handleSelectionChange = (
event: NativeSyntheticEvent<TextInputSelectionChangeEventData>,
@@ -91,36 +90,35 @@ const MentionInputControlled: FC<MentionInputProps> = ({
* - 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;
- }
+ const onSuggestionPress =
+ (mentionType: MentionPartType) => (suggestion: Suggestion) => {
+ const newValue = generateValueWithAddedSuggestion(
+ parts,
+ mentionType,
+ plainText,
+ selection,
+ suggestion,
+ );
+
+ if (!newValue) {
+ return;
+ }
- onChange(newValue);
+ 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;
+ /**
+ * 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}});
- };
+ // textInput.current?.setNativeProps({selection: {start: newCursorPosition, end: newCursorPosition}});
+ };
const handleTextInputRef = (ref: TextInput) => {
textInput.current = ref as TextInput;
@@ -129,7 +127,8 @@ const MentionInputControlled: FC<MentionInputProps> = ({
if (typeof propInputRef === 'function') {
propInputRef(ref);
} else {
- (propInputRef as MutableRefObject<TextInput>).current = ref as TextInput;
+ (propInputRef as MutableRefObject<TextInput>).current =
+ ref as TextInput;
}
}
};
@@ -151,12 +150,14 @@ const MentionInputControlled: FC<MentionInputProps> = ({
return (
<View style={containerStyle}>
{validateInput(keyboardText)
- ? (partTypes.filter(
- (one) =>
- isMentionPartType(one) &&
- one.renderSuggestions != null &&
- !one.isBottomMentionSuggestionsRender,
- ) as MentionPartType[]).map(renderMentionSuggestions)
+ ? (
+ partTypes.filter(
+ (one) =>
+ isMentionPartType(one) &&
+ one.renderSuggestions != null &&
+ !one.isBottomMentionSuggestionsRender,
+ ) as MentionPartType[]
+ ).map(renderMentionSuggestions)
: null}
<TextInput
@@ -181,12 +182,14 @@ const MentionInputControlled: FC<MentionInputProps> = ({
</TextInput>
{validateInput(keyboardText)
- ? (partTypes.filter(
- (one) =>
- isMentionPartType(one) &&
- one.renderSuggestions != null &&
- one.isBottomMentionSuggestionsRender,
- ) as MentionPartType[]).map(renderMentionSuggestions)
+ ? (
+ partTypes.filter(
+ (one) =>
+ isMentionPartType(one) &&
+ one.renderSuggestions != null &&
+ one.isBottomMentionSuggestionsRender,
+ ) as MentionPartType[]
+ ).map(renderMentionSuggestions)
: null}
</View>
);
diff --git a/src/components/common/Avatar.tsx b/src/components/common/Avatar.tsx
index 831cf906..86ebedf3 100644
--- a/src/components/common/Avatar.tsx
+++ b/src/components/common/Avatar.tsx
@@ -1,17 +1,30 @@
import React, {FC} from 'react';
-import {Image, ImageStyle, StyleProp} from 'react-native';
+import {Image, ImageStyle, StyleProp, ImageBackground} from 'react-native';
type AvatarProps = {
style: StyleProp<ImageStyle>;
uri: string | undefined;
+ loading: boolean;
+ loadingStyle: StyleProp<ImageStyle> | undefined;
};
-const Avatar: FC<AvatarProps> = ({style, uri}) => {
+const Avatar: FC<AvatarProps> = ({
+ style,
+ uri,
+ loading = false,
+ loadingStyle,
+}) => {
return (
- <Image
+ <ImageBackground
style={style}
defaultSource={require('../../assets/images/avatar-placeholder.png')}
- source={{uri, cache: 'reload'}}
- />
+ source={{uri, cache: 'reload'}}>
+ {loading && (
+ <Image
+ source={require('../../assets/gifs/loading-animation.gif')}
+ style={loadingStyle}
+ />
+ )}
+ </ImageBackground>
);
};
diff --git a/src/components/common/BottomDrawer.tsx b/src/components/common/BottomDrawer.tsx
index 3d9c0471..16e98690 100644
--- a/src/components/common/BottomDrawer.tsx
+++ b/src/components/common/BottomDrawer.tsx
@@ -71,15 +71,14 @@ const BottomDrawer: React.FC<BottomDrawerProps> = (props) => {
enabledContentGestureInteraction={false}
callbackNode={bgAlpha}
onCloseEnd={() => {
- setModalVisible(false);
- setIsOpen(false);
+ if (!isOpen) {
+ setModalVisible(false);
+ setIsOpen(false);
+ }
}}
/>
- <TouchableWithoutFeedback
- onPress={() => {
- setIsOpen(false);
- }}>
+ <TouchableWithoutFeedback onPress={() => setIsOpen(false)}>
<Animated.View style={[styles.backgroundView, {backgroundColor}]} />
</TouchableWithoutFeedback>
</Modal>
diff --git a/src/components/moments/MomentPostHeader.tsx b/src/components/moments/MomentPostHeader.tsx
index d2e9fc49..3c3ee4c3 100644
--- a/src/components/moments/MomentPostHeader.tsx
+++ b/src/components/moments/MomentPostHeader.tsx
@@ -1,4 +1,5 @@
-import React, {useEffect, useState} from 'react';
+import {useNavigation} from '@react-navigation/native';
+import React, {useState} from 'react';
import {
StyleSheet,
Text,
@@ -6,14 +7,13 @@ import {
View,
ViewProps,
} from 'react-native';
-import {MomentMoreInfoDrawer} from '../profile';
-import {loadUserMoments} from '../../store/actions';
import {useDispatch, useSelector, useStore} from 'react-redux';
-import {ScreenType} from '../../types';
-import TaggAvatar from '../profile/TaggAvatar';
-import {useNavigation} from '@react-navigation/native';
+import {loadUserMoments} from '../../store/actions';
import {RootState} from '../../store/rootReducer';
+import {ScreenType} from '../../types';
import {fetchUserX, userXInStore} from '../../utils';
+import {MomentMoreInfoDrawer} from '../profile';
+import TaggAvatar from '../profile/TaggAvatar';
interface MomentPostHeaderProps extends ViewProps {
userXId?: string;
@@ -51,10 +51,6 @@ const MomentPostHeader: React.FC<MomentPostHeaderProps> = ({
});
};
- useEffect(() => {
- setDrawerVisible(drawerVisible);
- }, [drawerVisible]);
-
return (
<View style={[styles.container, style]}>
<TouchableOpacity onPress={navigateToProfile} style={styles.header}>
diff --git a/src/components/profile/Cover.tsx b/src/components/profile/Cover.tsx
index 27777b64..2b6268a6 100644
--- a/src/components/profile/Cover.tsx
+++ b/src/components/profile/Cover.tsx
@@ -1,26 +1,99 @@
-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';
+import {useIsFocused} from '@react-navigation/native';
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,
);
+ const [needsUpdate, setNeedsUpdate] = useState(false);
+ const [updating, setUpdating] = useState(false);
+ const [loading, setLoading] = useState(true);
+ const [validImage, setValidImage] = useState<boolean>(true);
+ const isFocused = useIsFocused();
+
+ useEffect(() => {
+ checkCover(cover);
+ setLoading(false);
+ }, []);
+
+ useEffect(() => {
+ checkCover(cover);
+ }, [cover, isFocused]);
+
+ useEffect(() => {
+ checkCover(cover);
+ 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) {
+ setUpdating(true);
+ setNeedsUpdate(true);
+ setLoading(false);
+ } else {
+ setLoading(false);
+ }
+ };
+
+ const checkCover = async (url: string | undefined) => {
+ const valid = await validateImageLink(url);
+ if (valid !== validImage) {
+ setValidImage(valid);
+ }
+ setLoading(false);
+ };
+
return (
- <View style={[styles.container]}>
- <Image
+ <View style={styles.container}>
+ <ImageBackground
style={styles.image}
defaultSource={require('../../assets/images/cover-placeholder.png')}
- source={{uri: cover, cache: 'reload'}}
- />
+ source={{uri: cover, cache: 'reload'}}>
+ {loading && (
+ <Image
+ source={require('../../assets/gifs/loading-animation.gif')}
+ style={styles.loadingLarge}
+ />
+ )}
+ {!validImage && userXId === undefined && !loading && !updating && (
+ <TouchableOpacity
+ accessible={true}
+ accessibilityLabel="ADD HEADER PICTURE"
+ onPress={() => handleNewImage()}>
+ <GreyPurplePlus style={styles.plus} />
+ <Text style={styles.text}>Add Picture</Text>
+ </TouchableOpacity>
+ )}
+ </ImageBackground>
</View>
);
};
@@ -33,5 +106,28 @@ 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,
+ },
+ loadingLarge: {
+ alignSelf: 'center',
+ justifyContent: 'center',
+ height: COVER_HEIGHT * 0.2,
+ width: IMAGE_WIDTH * 0.2,
+ aspectRatio: 1,
+ top: 100,
+ },
});
export default Cover;
diff --git a/src/components/profile/ProfileHeader.tsx b/src/components/profile/ProfileHeader.tsx
index 14f7dc71..2241899d 100644
--- a/src/components/profile/ProfileHeader.tsx
+++ b/src/components/profile/ProfileHeader.tsx
@@ -85,10 +85,6 @@ const ProfileHeader: React.FC<ProfileHeaderProps> = ({
}
};
- useEffect(() => {
- setDrawerVisible(drawerVisible);
- }, [drawerVisible]);
-
return (
<View ref={containerRef} style={styles.container}>
<ProfileMoreInfoDrawer
@@ -115,6 +111,7 @@ const ProfileHeader: React.FC<ProfileHeaderProps> = ({
style={styles.avatar}
userXId={userXId}
screenType={screenType}
+ editable={true}
/>
<View style={styles.header}>
<Text style={styles.name} numberOfLines={2}>
diff --git a/src/components/profile/TaggAvatar.tsx b/src/components/profile/TaggAvatar.tsx
index ea0bdb65..8ccae2ef 100644
--- a/src/components/profile/TaggAvatar.tsx
+++ b/src/components/profile/TaggAvatar.tsx
@@ -1,9 +1,13 @@
-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';
+import {useIsFocused} from '@react-navigation/native';
const PROFILE_DIM = 100;
@@ -11,17 +15,85 @@ interface TaggAvatarProps {
style?: object;
userXId: string | undefined;
screenType: ScreenType;
+ editable: boolean;
}
const TaggAvatar: React.FC<TaggAvatarProps> = ({
style,
screenType,
userXId,
+ editable = false,
}) => {
- const {avatar} = useSelector((state: RootState) =>
+ const {avatar, user} = useSelector((state: RootState) =>
userXId ? state.userX[screenType][userXId] : state.user,
);
+ const dispatch = useDispatch();
+ const [needsUpdate, setNeedsUpdate] = useState(false);
+ const [updating, setUpdating] = useState(false);
+ const [loading, setLoading] = useState(true);
+ const [validImage, setValidImage] = useState<boolean>(true);
+ const isFocused = useIsFocused();
- return <Avatar style={[styles.image, style]} uri={avatar} />;
+ useEffect(() => {
+ checkAvatar(avatar);
+ setLoading(false);
+ }, []);
+
+ useEffect(() => {
+ checkAvatar(avatar);
+ }, [avatar, isFocused]);
+
+ useEffect(() => {
+ checkAvatar(avatar);
+ 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('profile', user.userId);
+ setLoading(true);
+ if (result) {
+ setUpdating(true);
+ setNeedsUpdate(true);
+ setLoading(false);
+ } else {
+ setLoading(false);
+ }
+ };
+
+ const checkAvatar = async (url: string | undefined) => {
+ const valid = await validateImageLink(url);
+ if (valid !== validImage) {
+ setValidImage(valid);
+ }
+ };
+
+ return (
+ <>
+ <Avatar
+ style={[styles.image, style]}
+ uri={avatar}
+ loading={loading}
+ loadingStyle={styles.loadingLarge}
+ />
+ {editable &&
+ !validImage &&
+ userXId === undefined &&
+ !loading &&
+ !updating && (
+ <TouchableOpacity
+ accessible={true}
+ accessibilityLabel="ADD PROFILE PICTURE"
+ onPress={() => handleNewImage()}>
+ <PurplePlus style={styles.plus} />
+ </TouchableOpacity>
+ )}
+ </>
+ );
};
const styles = StyleSheet.create({
@@ -29,6 +101,19 @@ const styles = StyleSheet.create({
height: PROFILE_DIM,
width: PROFILE_DIM,
borderRadius: PROFILE_DIM / 2,
+ overflow: 'hidden',
+ },
+ plus: {
+ position: 'absolute',
+ bottom: 35,
+ right: 0,
+ },
+ loadingLarge: {
+ height: PROFILE_DIM * 0.8,
+ width: PROFILE_DIM * 0.8,
+ alignSelf: 'center',
+ justifyContent: 'center',
+ aspectRatio: 2,
},
});