diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/assets/icons/grey-purple-plus.svg | 5 | ||||
-rw-r--r-- | src/assets/icons/purple-plus.svg | 15 | ||||
-rw-r--r-- | src/components/comments/CommentsContainer.tsx | 2 | ||||
-rw-r--r-- | src/components/comments/MentionInputControlled.tsx | 97 | ||||
-rw-r--r-- | src/components/common/Avatar.tsx | 23 | ||||
-rw-r--r-- | src/components/common/BottomDrawer.tsx | 11 | ||||
-rw-r--r-- | src/components/moments/MomentPostHeader.tsx | 16 | ||||
-rw-r--r-- | src/components/profile/Cover.tsx | 114 | ||||
-rw-r--r-- | src/components/profile/ProfileHeader.tsx | 5 | ||||
-rw-r--r-- | src/components/profile/TaggAvatar.tsx | 95 | ||||
-rw-r--r-- | src/constants/api.ts | 3 | ||||
-rw-r--r-- | src/services/UserProfileService.ts | 2 | ||||
-rw-r--r-- | src/utils/common.ts | 17 | ||||
-rw-r--r-- | src/utils/users.ts | 86 |
14 files changed, 402 insertions, 89 deletions
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/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, }, }); diff --git a/src/constants/api.ts b/src/constants/api.ts index 3c7e669e..ec67b6f9 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -5,7 +5,8 @@ const BASE_URL: string = 'http://127.0.0.1:8000/'; export const STREAM_CHAT_API = 'g2hvnyqx9cmv'; // Prod -// const BASE_URL: string = 'http://app-prod.tagg.id/'; +// const BASE_URL: string = 'https://app-prod2.tagg.id/'; +// const BASE_URL: string = 'https://app-prod3.tagg.id/'; // export const STREAM_CHAT_API = 'ur3kg5qz8x5v' const API_URL: string = BASE_URL + 'api/'; diff --git a/src/services/UserProfileService.ts b/src/services/UserProfileService.ts index 624f0e2a..19d353aa 100644 --- a/src/services/UserProfileService.ts +++ b/src/services/UserProfileService.ts @@ -453,7 +453,7 @@ export const verifyExistingInformation = async ( }, body: form, }); - return response.status===200; + return response.status === 200; } catch (error) { console.log(error); return false; 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..8505cde2 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,86 @@ export const navigateToProfile = async ( screenType, }); }; + +/* Function to open imagepicker and + * select images, which are sent to + * the database to update the profile + */ +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; + }); +}; |