diff --git a/app/lib/methods/helpers/image.test.ts b/app/lib/methods/helpers/image.test.ts deleted file mode 100644 index 852de3cab..000000000 --- a/app/lib/methods/helpers/image.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { isImage } from './image'; - -const imageJPG = 't2mul61342l91.jpg'; -const imagePNG = '205175493-fc1f7fdd-d10a-4099-88c4-146bac69e223.png'; -const imageSVG = 't2mul61342l91.svg'; -const linkToImagePNG = 'https://user-images.githubusercontent.com/47038980/205175493-fc1f7fdd-d10a-4099-88c4-146bac69e223.png'; -const linkToImageWithQueryParams = - 'https://i.ytimg.com/vi/suFuJZCfC7g/hqdefault.jpg?sqp=-oaymwE2CNACELwBSFXyq4qpAygIARUAAIhCGAFwAcABBvABAfgB_gmAAtAFigIMCAAQARhlIGUoZTAP&rs=AOn4CLB_0OCFNuoCRBlaTJEa2PPOOHxkbQ'; - -describe("Evaluate if the string is returning an image's type", () => { - test('return true when the image ends with .jpg', () => { - const result = isImage(imageJPG); - expect(result).toBe(true); - }); - test('return true when the image ends with .png', () => { - const result = isImage(imagePNG); - expect(result).toBe(true); - }); - test('return false when the image ends with .svg', () => { - const result = isImage(imageSVG); - expect(result).toBe(false); - }); - test('return true when the image ends with .jpg and query params', () => { - const result = isImage(linkToImageWithQueryParams); - expect(result).toBe(true); - }); - test('return true when a link ends with .png', () => { - const result = isImage(linkToImagePNG); - expect(result).toBe(true); - }); -}); diff --git a/app/lib/methods/helpers/image.ts b/app/lib/methods/helpers/image.ts index 0935f7615..63ce69c99 100644 --- a/app/lib/methods/helpers/image.ts +++ b/app/lib/methods/helpers/image.ts @@ -1,7 +1,10 @@ -// Fast Image can't render a svg image from a uri yet, because of that we aren't test the svg within the RegEx -export const regExpImageType = new RegExp( - '.(jpg|jpeg|png|webp|avif|gif|tiff)' + // type of the URL - '(\\?[;&a-z\\d%_.~+=-]*)?', - 'i' // query string -); -export const isImage = (url: string) => regExpImageType.test(url); +import { Image } from 'react-native'; + +export const isImageURL = async (url: string) => { + try { + const result = await Image.prefetch(url); + return result; + } catch { + return false; + } +}; diff --git a/app/views/ChangeAvatarView/AvatarSuggestion.tsx b/app/views/ChangeAvatarView/AvatarSuggestion.tsx index 58e795a44..e68c3d2e0 100644 --- a/app/views/ChangeAvatarView/AvatarSuggestion.tsx +++ b/app/views/ChangeAvatarView/AvatarSuggestion.tsx @@ -13,7 +13,7 @@ const AvatarSuggestion = ({ username, resetAvatar }: { - onPress: (value: IAvatar | null) => void; + onPress: (value: IAvatar) => void; username?: string; resetAvatar?: () => void; }) => { diff --git a/app/views/ChangeAvatarView/AvatarUrl.tsx b/app/views/ChangeAvatarView/AvatarUrl.tsx index 64e990ccf..dae1d34a6 100644 --- a/app/views/ChangeAvatarView/AvatarUrl.tsx +++ b/app/views/ChangeAvatarView/AvatarUrl.tsx @@ -1,35 +1,25 @@ -import React, { useEffect } from 'react'; -import { useForm } from 'react-hook-form'; +import React from 'react'; import I18n from '../../i18n'; -import { ControlledFormTextInput } from '../../containers/TextInput'; - -interface ISubmit { - avatarUrl: string; -} +import { FormTextInput } from '../../containers/TextInput'; +import { useDebounce, isImageURL } from '../../lib/methods/helpers'; const AvatarUrl = ({ submit }: { submit: (value: string) => void }) => { - const { - control, - formState: { isDirty }, - getValues - } = useForm({ mode: 'onChange', defaultValues: { avatarUrl: '' } }); - - useEffect(() => { - if (isDirty) { - const { avatarUrl } = getValues(); - submit(avatarUrl); - } else { - submit(''); + const handleChangeText = useDebounce(async (value: string) => { + if (value) { + const result = await isImageURL(value); + if (result) { + return submit(value); + } + return submit(''); } - }, [isDirty]); + }, 500); return ( - diff --git a/app/views/ChangeAvatarView/index.tsx b/app/views/ChangeAvatarView/index.tsx index 139c1dd71..c9240ba11 100644 --- a/app/views/ChangeAvatarView/index.tsx +++ b/app/views/ChangeAvatarView/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; +import React, { useEffect, useLayoutEffect, useReducer, useRef, useState } from 'react'; import { ScrollView, View } from 'react-native'; import { RouteProp, useNavigation, useRoute } from '@react-navigation/native'; import { StackNavigationProp } from '@react-navigation/stack'; @@ -27,11 +27,42 @@ import { Services } from '../../lib/services'; import AvatarSuggestion from './AvatarSuggestion'; import log from '../../lib/methods/helpers/log'; -const RESET_ROOM_AVATAR = 'resetRoomAvatar'; +enum AvatarStateActions { + CHANGE_AVATAR = 'CHANGE_AVATAR', + RESET_USER_AVATAR = 'RESET_USER_AVATAR', + RESET_ROOM_AVATAR = 'RESET_ROOM_AVATAR' +} + +interface IReducerAction { + type: AvatarStateActions; + payload?: Partial; +} + +interface IState extends IAvatar { + resetUserAvatar: string; +} + +const initialState = { + data: '', + url: '', + contentType: '', + service: '', + resetUserAvatar: '' +}; + +function reducer(state: IState, action: IReducerAction) { + const { type, payload } = action; + if (type in AvatarStateActions) { + return { + ...initialState, + ...payload + }; + } + return state; +} const ChangeAvatarView = () => { - const [avatar, setAvatarState] = useState(null); - const [textAvatar, setTextAvatar] = useState(''); + const [state, dispatch] = useReducer(reducer, initialState); const [saving, setSaving] = useState(false); const { colors } = useTheme(); const { userId, username, serverVersion } = useAppSelector( @@ -42,9 +73,7 @@ const ChangeAvatarView = () => { }), shallowEqual ); - - const avatarUrl = useRef(''); - + const isDirty = useRef(false); const navigation = useNavigation>(); const { context, titleHeader, room, t } = useRoute>().params; @@ -56,7 +85,7 @@ const ChangeAvatarView = () => { useEffect(() => { navigation.addListener('beforeRemove', e => { - if (!avatarUrl.current) { + if (!isDirty.current) { return; } @@ -73,85 +102,74 @@ const ChangeAvatarView = () => { }); }, [navigation]); - const setAvatar = (value: IAvatar | null) => { - avatarUrl.current = value?.url || ''; - setAvatarState(value); + const dispatchAvatar = (action: IReducerAction) => { + isDirty.current = true; + dispatch(action); }; const submit = async () => { - let result; - if (context === 'room' && room?.rid) { - // Change Rooms Avatar - result = await changeRoomsAvatar(room.rid); - } else if (avatar?.url) { - // Change User's Avatar - result = await changeUserAvatar(avatar); - } else if (textAvatar) { - // Change User's Avatar - result = await resetUserAvatar(); - } - if (result) { + try { + setSaving(true); + console.log('🚀 ~ file: index.tsx:117 ~ submit ~ state', state); + if (context === 'room' && room?.rid) { + // Change Rooms Avatar + await changeRoomsAvatar(room.rid); + } else if (state?.url) { + // Change User's Avatar + await changeUserAvatar(state); + } else if (state.resetUserAvatar) { + // Change User's Avatar + await resetUserAvatar(); + } + isDirty.current = false; + } catch (e: any) { + log(e); + return showErrorAlert(e.message, I18n.t('Oops')); + } finally { setSaving(false); - avatarUrl.current = ''; - return navigation.goBack(); } + return navigation.goBack(); }; const changeRoomsAvatar = async (rid: string) => { try { - setSaving(true); - await Services.saveRoomSettings(rid, { roomAvatar: avatar?.data }); - return true; + console.log('🚀 ~ file: index.tsx:135 ~ changeRoomsAvatar ~ rid', rid, state); + await Services.saveRoomSettings(rid, { roomAvatar: state?.data }); } catch (e) { log(e); - setSaving(false); - return handleError(e, 'saveRoomSettings', 'changing_avatar'); + return handleError(e, 'changing_avatar'); } }; const changeUserAvatar = async (avatarUpload: IAvatar) => { try { - setSaving(true); + console.log('🚀 ~ file: index.tsx:144 ~ changeUserAvatar ~ avatarUpload', avatarUpload); await Services.setAvatarFromService(avatarUpload); - return true; } catch (e) { - log(e); - setSaving(false); - return handleError(e, 'resetAvatar', 'changing_avatar'); + return handleError(e, 'changing_avatar'); } }; const resetUserAvatar = async () => { try { + console.log('🚀 ~ file: index.tsx:154 ~ resetUserAvatar ~ userId', userId); await Services.resetAvatar(userId); - return true; } catch (e) { - return handleError(e, 'setAvatarFromService', 'changing_avatar'); + return handleError(e, 'changing_avatar'); } }; - const handleError = (e: any, _func: string, action: string) => { + const handleError = (e: any, action: string) => { if (e.data && e.data.error.includes('[error-too-many-requests]')) { - return showErrorAlert(e.data.error); + throw new Error(e.data.error); } if (e.error && e.error === 'error-avatar-invalid-url') { - return showErrorAlert(I18n.t(e.error, { url: e.details.url })); + throw new Error(I18n.t(e.error, { url: e.details.url })); } if (I18n.isTranslated(e.error)) { - return showErrorAlert(I18n.t(e.error)); + throw new Error(I18n.t(e.error)); } - showErrorAlert(I18n.t('There_was_an_error_while_action', { action: I18n.t(action) })); - }; - - const resetAvatar = () => { - setAvatar(null); - setTextAvatar(`@${username}`); - avatarUrl.current = `@${username}`; - }; - - const resetRoomAvatar = () => { - setAvatar({ data: null }); - avatarUrl.current = RESET_ROOM_AVATAR; + throw new Error(I18n.t('There_was_an_error_while_action', { action: I18n.t(action) })); }; const pickImage = async () => { @@ -166,13 +184,16 @@ const ChangeAvatarView = () => { }; try { const response: Image = await ImagePicker.openPicker(options); - setAvatar({ url: response.path, data: `data:image/jpeg;base64,${response.data}`, service: 'upload' }); + dispatchAvatar({ + type: AvatarStateActions.CHANGE_AVATAR, + payload: { url: response.path, data: `data:image/jpeg;base64,${response.data}`, service: 'upload' } + }); } catch (error) { log(error); } }; - const ridProps = avatarUrl.current !== RESET_ROOM_AVATAR ? { rid: room?.rid } : {}; + const deletingRoomAvatar = context === 'room' && state.data === null ? {} : { rid: room?.rid }; return ( { > - {context === 'profile' ? setAvatar({ url: value, data: value, service: 'url' })} /> : null} + {context === 'profile' ? ( + + dispatchAvatar({ + type: AvatarStateActions.CHANGE_AVATAR, + payload: { url: value, data: value, service: 'url' } + }) + } + /> + ) : null} - {context === 'profile' ? : null} + {context === 'profile' ? ( + + dispatchAvatar({ + type: AvatarStateActions.RESET_USER_AVATAR, + payload: { resetUserAvatar: `@${username}` } + }) + } + username={username} + onPress={value => + dispatchAvatar({ + type: AvatarStateActions.CHANGE_AVATAR, + payload: value + }) + } + /> + ) : null}