Refactor all the changeAvatarView and fix how to test the image url
This commit is contained in:
parent
6d22747707
commit
dfcea278de
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -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;
|
||||||
}) => {
|
}) => {
|
||||||
|
|
|
@ -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 }}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue