Refactor all the changeAvatarView and fix how to test the image url

This commit is contained in:
Reinaldo Neto 2023-01-16 21:35:55 -03:00
parent 6d22747707
commit dfcea278de
6 changed files with 134 additions and 128 deletions

View File

@ -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);
});
});

View File

@ -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 import { Image } from 'react-native';
export const regExpImageType = new RegExp(
'.(jpg|jpeg|png|webp|avif|gif|tiff)' + // type of the URL export const isImageURL = async (url: string) => {
'(\\?[;&a-z\\d%_.~+=-]*)?', try {
'i' // query string const result = await Image.prefetch(url);
); return result;
export const isImage = (url: string) => regExpImageType.test(url); } catch {
return false;
}
};

View File

@ -13,7 +13,7 @@ const AvatarSuggestion = ({
username, username,
resetAvatar resetAvatar
}: { }: {
onPress: (value: IAvatar | null) => void; onPress: (value: IAvatar) => void;
username?: string; username?: string;
resetAvatar?: () => void; resetAvatar?: () => void;
}) => { }) => {

View File

@ -1,35 +1,25 @@
import React, { useEffect } from 'react'; import React from 'react';
import { useForm } from 'react-hook-form';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { ControlledFormTextInput } from '../../containers/TextInput'; import { FormTextInput } from '../../containers/TextInput';
import { useDebounce, isImageURL } from '../../lib/methods/helpers';
interface ISubmit {
avatarUrl: string;
}
const AvatarUrl = ({ submit }: { submit: (value: string) => void }) => { const AvatarUrl = ({ submit }: { submit: (value: string) => void }) => {
const { const handleChangeText = useDebounce(async (value: string) => {
control, if (value) {
formState: { isDirty }, const result = await isImageURL(value);
getValues if (result) {
} = useForm<ISubmit>({ mode: 'onChange', defaultValues: { avatarUrl: '' } }); return submit(value);
}
useEffect(() => { return submit('');
if (isDirty) {
const { avatarUrl } = getValues();
submit(avatarUrl);
} else {
submit('');
} }
}, [isDirty]); }, 500);
return ( return (
<ControlledFormTextInput <FormTextInput
control={control}
name='avatarUrl'
label={I18n.t('Avatar_Url')} label={I18n.t('Avatar_Url')}
placeholder={I18n.t('insert_Avatar_URL')} placeholder={I18n.t('insert_Avatar_URL')}
onChangeText={handleChangeText}
testID='change-avatar-view-avatar-url' testID='change-avatar-view-avatar-url'
containerStyle={{ marginBottom: 0 }} containerStyle={{ marginBottom: 0 }}
/> />

View File

@ -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 { ScrollView, View } from 'react-native';
import { RouteProp, useNavigation, useRoute } from '@react-navigation/native'; import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack'; import { StackNavigationProp } from '@react-navigation/stack';
@ -27,11 +27,42 @@ import { Services } from '../../lib/services';
import AvatarSuggestion from './AvatarSuggestion'; import AvatarSuggestion from './AvatarSuggestion';
import log from '../../lib/methods/helpers/log'; 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<IState>;
}
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 ChangeAvatarView = () => {
const [avatar, setAvatarState] = useState<IAvatar | null>(null); const [state, dispatch] = useReducer(reducer, initialState);
const [textAvatar, setTextAvatar] = useState('');
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const { colors } = useTheme(); const { colors } = useTheme();
const { userId, username, serverVersion } = useAppSelector( const { userId, username, serverVersion } = useAppSelector(
@ -42,9 +73,7 @@ const ChangeAvatarView = () => {
}), }),
shallowEqual shallowEqual
); );
const isDirty = useRef<boolean>(false);
const avatarUrl = useRef<string>('');
const navigation = useNavigation<StackNavigationProp<ChatsStackParamList, 'ChangeAvatarView'>>(); const navigation = useNavigation<StackNavigationProp<ChatsStackParamList, 'ChangeAvatarView'>>();
const { context, titleHeader, room, t } = useRoute<RouteProp<ChatsStackParamList, 'ChangeAvatarView'>>().params; const { context, titleHeader, room, t } = useRoute<RouteProp<ChatsStackParamList, 'ChangeAvatarView'>>().params;
@ -56,7 +85,7 @@ const ChangeAvatarView = () => {
useEffect(() => { useEffect(() => {
navigation.addListener('beforeRemove', e => { navigation.addListener('beforeRemove', e => {
if (!avatarUrl.current) { if (!isDirty.current) {
return; return;
} }
@ -73,85 +102,74 @@ const ChangeAvatarView = () => {
}); });
}, [navigation]); }, [navigation]);
const setAvatar = (value: IAvatar | null) => { const dispatchAvatar = (action: IReducerAction) => {
avatarUrl.current = value?.url || ''; isDirty.current = true;
setAvatarState(value); dispatch(action);
}; };
const submit = async () => { const submit = async () => {
let result; try {
if (context === 'room' && room?.rid) { setSaving(true);
// Change Rooms Avatar console.log('🚀 ~ file: index.tsx:117 ~ submit ~ state', state);
result = await changeRoomsAvatar(room.rid); if (context === 'room' && room?.rid) {
} else if (avatar?.url) { // Change Rooms Avatar
// Change User's Avatar await changeRoomsAvatar(room.rid);
result = await changeUserAvatar(avatar); } else if (state?.url) {
} else if (textAvatar) { // Change User's Avatar
// Change User's Avatar await changeUserAvatar(state);
result = await resetUserAvatar(); } else if (state.resetUserAvatar) {
} // Change User's Avatar
if (result) { await resetUserAvatar();
}
isDirty.current = false;
} catch (e: any) {
log(e);
return showErrorAlert(e.message, I18n.t('Oops'));
} finally {
setSaving(false); setSaving(false);
avatarUrl.current = '';
return navigation.goBack();
} }
return navigation.goBack();
}; };
const changeRoomsAvatar = async (rid: string) => { const changeRoomsAvatar = async (rid: string) => {
try { try {
setSaving(true); console.log('🚀 ~ file: index.tsx:135 ~ changeRoomsAvatar ~ rid', rid, state);
await Services.saveRoomSettings(rid, { roomAvatar: avatar?.data }); await Services.saveRoomSettings(rid, { roomAvatar: state?.data });
return true;
} catch (e) { } catch (e) {
log(e); log(e);
setSaving(false); return handleError(e, 'changing_avatar');
return handleError(e, 'saveRoomSettings', 'changing_avatar');
} }
}; };
const changeUserAvatar = async (avatarUpload: IAvatar) => { const changeUserAvatar = async (avatarUpload: IAvatar) => {
try { try {
setSaving(true); console.log('🚀 ~ file: index.tsx:144 ~ changeUserAvatar ~ avatarUpload', avatarUpload);
await Services.setAvatarFromService(avatarUpload); await Services.setAvatarFromService(avatarUpload);
return true;
} catch (e) { } catch (e) {
log(e); return handleError(e, 'changing_avatar');
setSaving(false);
return handleError(e, 'resetAvatar', 'changing_avatar');
} }
}; };
const resetUserAvatar = async () => { const resetUserAvatar = async () => {
try { try {
console.log('🚀 ~ file: index.tsx:154 ~ resetUserAvatar ~ userId', userId);
await Services.resetAvatar(userId); await Services.resetAvatar(userId);
return true;
} catch (e) { } 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]')) { 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') { 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)) { 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) })); throw new Error(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;
}; };
const pickImage = async () => { const pickImage = async () => {
@ -166,13 +184,16 @@ const ChangeAvatarView = () => {
}; };
try { try {
const response: Image = await ImagePicker.openPicker(options); 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) { } catch (error) {
log(error); log(error);
} }
}; };
const ridProps = avatarUrl.current !== RESET_ROOM_AVATAR ? { rid: room?.rid } : {}; const deletingRoomAvatar = context === 'room' && state.data === null ? {} : { rid: room?.rid };
return ( return (
<KeyboardView <KeyboardView
@ -189,17 +210,42 @@ const ChangeAvatarView = () => {
> >
<View style={styles.avatarContainer} testID='change-avatar-view-avatar'> <View style={styles.avatarContainer} testID='change-avatar-view-avatar'>
<Avatar <Avatar
text={room?.name || textAvatar || username} text={room?.name || state.resetUserAvatar || username}
avatar={avatar?.url} avatar={state?.url}
isStatic={avatar?.url} isStatic={state?.url}
size={100} size={120}
type={t} type={t}
{...ridProps} {...deletingRoomAvatar}
/> />
</View> </View>
{context === 'profile' ? <AvatarUrl submit={value => setAvatar({ url: value, data: value, service: 'url' })} /> : null} {context === 'profile' ? (
<AvatarUrl
submit={value =>
dispatchAvatar({
type: AvatarStateActions.CHANGE_AVATAR,
payload: { url: value, data: value, service: 'url' }
})
}
/>
) : null}
<List.Separator style={styles.separator} /> <List.Separator style={styles.separator} />
{context === 'profile' ? <AvatarSuggestion resetAvatar={resetAvatar} username={username} onPress={setAvatar} /> : null} {context === 'profile' ? (
<AvatarSuggestion
resetAvatar={() =>
dispatchAvatar({
type: AvatarStateActions.RESET_USER_AVATAR,
payload: { resetUserAvatar: `@${username}` }
})
}
username={username}
onPress={value =>
dispatchAvatar({
type: AvatarStateActions.CHANGE_AVATAR,
payload: value
})
}
/>
) : null}
<Button <Button
title={I18n.t('Upload_image')} title={I18n.t('Upload_image')}
@ -215,13 +261,13 @@ const ChangeAvatarView = () => {
type='primary' type='primary'
disabled={saving} disabled={saving}
backgroundColor={colors.dangerColor} backgroundColor={colors.dangerColor}
onPress={resetRoomAvatar} onPress={() => dispatchAvatar({ type: AvatarStateActions.RESET_ROOM_AVATAR, payload: { data: null } })}
testID='change-avatar-view-delete-my-account' testID='change-avatar-view-delete-my-account'
/> />
) : null} ) : null}
<Button <Button
title={I18n.t('Save')} title={I18n.t('Save')}
disabled={!avatarUrl.current || saving} disabled={!isDirty.current || saving}
type='primary' type='primary'
loading={saving} loading={saving}
onPress={submit} onPress={submit}

View File

@ -423,8 +423,6 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
<View style={styles.avatarContainer} testID='profile-view-avatar'> <View style={styles.avatarContainer} testID='profile-view-avatar'>
<AvatarWithEdit <AvatarWithEdit
text={user.username} text={user.username}
avatarETag={user.avatarETag}
size={100}
handleEdit={Accounts_AllowUserAvatarChange ? this.handleEditAvatar : undefined} handleEdit={Accounts_AllowUserAvatarChange ? this.handleEditAvatar : undefined}
/> />
</View> </View>