feat: Change Avatar View (#4746)
* [NEW] Change Avatar View * change avatar view and avatar suggestion * avatar Url * change avatar for profile it's done, missing fix revalidate the avatar in profile and drawer * pick image * clean profile view * Finished the profile and fixed the avatar when change the user profile * fix the profile update through all the app * refactor avatar suggestion * fix the delete from rooms and finished RoomInfoView, missing RoomInfoEditView * room info edit view * ipad navigation * refactor the submit function * refactor the e2e test * minor tweak with themes and buttons * minor tweak in edit button * minor tweaks * tweak IAvatarContainer * fixing pt-br * removed the avatarETagUser and search by username and text * created the useHooke useAvatarETag * fix lint * refactor image.ts and test * refactor avatar component * fix the edit button * refactor useAvatarETag * fix the pt-br translation * minor tweak tests * back handleError to views and refactor the submit * avatar suggestion item * types for change avatar view context * fix avatar on header for tablets * minor tweak changeavatarview * refactor avatar url to use hook form * tweak on yup validation * minor tweak with handle error * minor tweak * minor tweak * interface avatar container * minor tweak * Refactor all the changeAvatarView and fix how to test the image url * fixed the layout * minor refactor * minor tweaks * fix the visual bug when the user change the avatar to new then clear cache * fix the flicker when upload an image * update package.json * test the reset, discard alert, cancel and discard * separate the avatar test from profile and create new tests for change avatar * mock imagepicker * minor tweak, adding console and add echo to config.yml * use RUNNING_E2E_TESTS as env to other files * exprt env at android build * change the to way to set the running e2e test env * update test * delete the .env and update the e2e/readme and the file review * minor tweak * minor tweak * update the test, fixing how to dismiss the keyboard --------- Co-authored-by: Gleidson Daniel Silva <gleidson10daniel@hotmail.com>
This commit is contained in:
parent
056e314bec
commit
fd210c4713
|
@ -484,7 +484,7 @@ jobs:
|
|||
- run:
|
||||
name: Build Android
|
||||
command: |
|
||||
echo "RUNNING_E2E_TESTS=true" > ./.env
|
||||
export RUNNING_E2E_TESTS=true
|
||||
yarn e2e:android-build
|
||||
- save_cache: *save-gradle-cache
|
||||
- store_artifacts:
|
||||
|
@ -582,7 +582,7 @@ jobs:
|
|||
/usr/libexec/PlistBuddy -c "Set :bugsnag:apiKey $BUGSNAG_KEY" ./ios/RocketChatRN/Info.plist
|
||||
/usr/libexec/PlistBuddy -c "Set :bugsnag:apiKey $BUGSNAG_KEY" ./ios/ShareRocketChatRN/Info.plist
|
||||
yarn detox clean-framework-cache && yarn detox build-framework-cache
|
||||
echo "RUNNING_E2E_TESTS=true" > ./.env
|
||||
export RUNNING_E2E_TESTS=true
|
||||
yarn e2e:ios-build
|
||||
- persist_to_workspace:
|
||||
root: /Users/distiller/project
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import { Q } from '@nozbe/watermelondb';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React from 'react';
|
||||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
|
||||
import { IApplicationState, TSubscriptionModel, TUserModel } from '../../definitions';
|
||||
import database from '../../lib/database';
|
||||
import { IApplicationState } from '../../definitions';
|
||||
import { getUserSelector } from '../../selectors/login';
|
||||
import Avatar from './Avatar';
|
||||
import { IAvatar } from './interfaces';
|
||||
import { useAvatarETag } from './useAvatarETag';
|
||||
|
||||
const AvatarContainer = ({
|
||||
style,
|
||||
|
@ -23,17 +21,13 @@ const AvatarContainer = ({
|
|||
isStatic,
|
||||
rid
|
||||
}: IAvatar): React.ReactElement => {
|
||||
const subscription = useRef<Subscription>();
|
||||
const [avatarETag, setAvatarETag] = useState<string | undefined>('');
|
||||
|
||||
const isDirect = () => type === 'd';
|
||||
|
||||
const server = useSelector((state: IApplicationState) => state.share.server.server || state.server.server);
|
||||
const serverVersion = useSelector((state: IApplicationState) => state.share.server.version || state.server.version);
|
||||
const { id, token } = useSelector(
|
||||
const { id, token, username } = useSelector(
|
||||
(state: IApplicationState) => ({
|
||||
id: getUserSelector(state).id,
|
||||
token: getUserSelector(state).token
|
||||
token: getUserSelector(state).token,
|
||||
username: getUserSelector(state).username
|
||||
}),
|
||||
shallowEqual
|
||||
);
|
||||
|
@ -48,41 +42,7 @@ const AvatarContainer = ({
|
|||
true
|
||||
);
|
||||
|
||||
const init = async () => {
|
||||
const db = database.active;
|
||||
const usersCollection = db.get('users');
|
||||
const subsCollection = db.get('subscriptions');
|
||||
|
||||
let record;
|
||||
try {
|
||||
if (isDirect()) {
|
||||
const [user] = await usersCollection.query(Q.where('username', text)).fetch();
|
||||
record = user;
|
||||
} else if (rid) {
|
||||
record = await subsCollection.find(rid);
|
||||
}
|
||||
} catch {
|
||||
// Record not found
|
||||
}
|
||||
|
||||
if (record) {
|
||||
const observable = record.observe() as Observable<TSubscriptionModel | TUserModel>;
|
||||
subscription.current = observable.subscribe(r => {
|
||||
setAvatarETag(r.avatarETag);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!avatarETag) {
|
||||
init();
|
||||
}
|
||||
return () => {
|
||||
if (subscription?.current?.unsubscribe) {
|
||||
subscription.current.unsubscribe();
|
||||
}
|
||||
};
|
||||
}, [text, type, size, avatarETag, externalProviderUrl]);
|
||||
const { avatarETag } = useAvatarETag({ username, text, type, rid, id });
|
||||
|
||||
return (
|
||||
<Avatar
|
|
@ -0,0 +1,85 @@
|
|||
import React from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
import Button from '../Button';
|
||||
import AvatarContainer from './AvatarContainer';
|
||||
import { IAvatar } from './interfaces';
|
||||
import I18n from '../../i18n';
|
||||
import { useTheme } from '../../theme';
|
||||
import { BUTTON_HIT_SLOP } from '../message/utils';
|
||||
import { useAppSelector } from '../../lib/hooks';
|
||||
import { compareServerVersion } from '../../lib/methods/helpers';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
editAvatarButton: {
|
||||
marginTop: 8,
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 12,
|
||||
marginBottom: 0,
|
||||
height: undefined
|
||||
},
|
||||
textButton: {
|
||||
fontSize: 12,
|
||||
...sharedStyles.textSemibold
|
||||
}
|
||||
});
|
||||
|
||||
interface IAvatarContainer extends Omit<IAvatar, 'size'> {
|
||||
handleEdit?: () => void;
|
||||
}
|
||||
|
||||
const AvatarWithEdit = ({
|
||||
style,
|
||||
text = '',
|
||||
avatar,
|
||||
emoji,
|
||||
borderRadius,
|
||||
type,
|
||||
children,
|
||||
onPress,
|
||||
getCustomEmoji,
|
||||
isStatic,
|
||||
rid,
|
||||
handleEdit
|
||||
}: IAvatarContainer): React.ReactElement => {
|
||||
const { colors } = useTheme();
|
||||
|
||||
const { serverVersion } = useAppSelector(state => ({
|
||||
serverVersion: state.server.version
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<AvatarContainer
|
||||
style={style}
|
||||
text={text}
|
||||
avatar={avatar}
|
||||
emoji={emoji}
|
||||
size={120}
|
||||
borderRadius={borderRadius}
|
||||
type={type}
|
||||
children={children}
|
||||
onPress={onPress}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
isStatic={isStatic}
|
||||
rid={rid}
|
||||
/>
|
||||
{handleEdit && serverVersion && compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '3.6.0') ? (
|
||||
<Button
|
||||
title={I18n.t('Edit')}
|
||||
type='secondary'
|
||||
backgroundColor={colors.editAndUploadButtonAvatar}
|
||||
onPress={handleEdit}
|
||||
testID='avatar-edit-button'
|
||||
style={styles.editAvatarButton}
|
||||
styleText={styles.textButton}
|
||||
color={colors.titleText}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AvatarWithEdit;
|
|
@ -0,0 +1,5 @@
|
|||
import Avatar from './AvatarContainer';
|
||||
|
||||
export { default as AvatarWithEdit } from './AvatarWithEdit';
|
||||
|
||||
export default Avatar;
|
|
@ -0,0 +1,67 @@
|
|||
import { Q } from '@nozbe/watermelondb';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
|
||||
import { TLoggedUserModel, TSubscriptionModel, TUserModel } from '../../definitions';
|
||||
import database from '../../lib/database';
|
||||
|
||||
export const useAvatarETag = ({
|
||||
username,
|
||||
text,
|
||||
type = '',
|
||||
rid,
|
||||
id
|
||||
}: {
|
||||
type?: string;
|
||||
username: string;
|
||||
text: string;
|
||||
rid?: string;
|
||||
id: string;
|
||||
}) => {
|
||||
const [avatarETag, setAvatarETag] = useState<string | undefined>('');
|
||||
|
||||
const isDirect = () => type === 'd';
|
||||
|
||||
useEffect(() => {
|
||||
let subscription: Subscription;
|
||||
if (!avatarETag) {
|
||||
const observeAvatarETag = async () => {
|
||||
const db = database.active;
|
||||
const usersCollection = db.get('users');
|
||||
const subsCollection = db.get('subscriptions');
|
||||
|
||||
let record;
|
||||
try {
|
||||
if (username === text) {
|
||||
const serversDB = database.servers;
|
||||
const userCollections = serversDB.get('users');
|
||||
const user = await userCollections.find(id);
|
||||
record = user;
|
||||
} else if (isDirect()) {
|
||||
const [user] = await usersCollection.query(Q.where('username', text)).fetch();
|
||||
record = user;
|
||||
} else if (rid) {
|
||||
record = await subsCollection.find(rid);
|
||||
}
|
||||
} catch {
|
||||
// Record not found
|
||||
}
|
||||
|
||||
if (record) {
|
||||
const observable = record.observe() as Observable<TSubscriptionModel | TUserModel | TLoggedUserModel>;
|
||||
subscription = observable.subscribe(r => {
|
||||
setAvatarETag(r.avatarETag);
|
||||
});
|
||||
}
|
||||
};
|
||||
observeAvatarETag();
|
||||
return () => {
|
||||
if (subscription?.unsubscribe) {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
};
|
||||
}
|
||||
}, [text]);
|
||||
|
||||
return { avatarETag };
|
||||
};
|
|
@ -14,7 +14,7 @@ interface IButtonProps extends PlatformTouchableProps {
|
|||
loading?: boolean;
|
||||
color?: string;
|
||||
fontSize?: number;
|
||||
styleText?: StyleProp<TextStyle>[];
|
||||
styleText?: StyleProp<TextStyle> | StyleProp<TextStyle>[];
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
|
|
@ -17,16 +17,14 @@ export interface IAvatarButton {
|
|||
}
|
||||
|
||||
export interface IAvatar {
|
||||
data: {} | string | null;
|
||||
data: string | null;
|
||||
url?: string;
|
||||
contentType?: string;
|
||||
service?: any;
|
||||
}
|
||||
|
||||
export interface IAvatarSuggestion {
|
||||
[service: string]: {
|
||||
url: string;
|
||||
blob: string;
|
||||
contentType: string;
|
||||
};
|
||||
url: string;
|
||||
blob: string;
|
||||
contentType: string;
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export type TChangeAvatarViewContext = 'profile' | 'room';
|
|
@ -877,6 +877,14 @@
|
|||
"Reply_in_direct_message": "Reply in Direct Message",
|
||||
"room_archived": "archived room",
|
||||
"room_unarchived": "unarchived room",
|
||||
"Upload_image": "Upload image",
|
||||
"Delete_image": "Delete image",
|
||||
"Images_uploaded": "Images uploaded",
|
||||
"Avatar": "Avatar",
|
||||
"insert_Avatar_URL": "insert image URL here",
|
||||
"Discard_changes":"Discard changes?",
|
||||
"Discard":"Discard",
|
||||
"Discard_changes_description":"All changes will be lost if you go back without saving.",
|
||||
"no-videoconf-provider-app-header": "Conference call not available",
|
||||
"no-videoconf-provider-app-body": "Conference call apps can be installed in the Rocket.Chat marketplace by a workspace admin.",
|
||||
"admin-no-videoconf-provider-app-header": "Conference call not enabled",
|
||||
|
|
|
@ -876,6 +876,14 @@
|
|||
"Reply_in_direct_message": "Responder por mensagem direta",
|
||||
"room_archived": "{{username}} arquivou a sala",
|
||||
"room_unarchived": "{{username}} desarquivou a sala",
|
||||
"Upload_image": "Carregar imagem",
|
||||
"Delete_image": "Deletar imagem",
|
||||
"Images_uploaded": "Imagens carregadas",
|
||||
"Avatar": "Avatar",
|
||||
"insert_Avatar_URL": "insira o URL da imagem aqui",
|
||||
"Discard_changes":"Descartar alterações?",
|
||||
"Discard":"Descartar",
|
||||
"Discard_changes_description":"Todas as alterações serão perdidas, se você sair sem salvar.",
|
||||
"Presence_Cap_Warning_Title": "Status do usuário desabilitado temporariamente",
|
||||
"Presence_Cap_Warning_Description": "O limite de conexões ativas para a workspace foi atingido, por isso o serviço responsável pela presença dos usuários está temporariamente desabilitado. Ele pode ser reabilitado manualmente nas configurações da workspace."
|
||||
}
|
|
@ -59,6 +59,7 @@ export const colors = {
|
|||
buttonText: '#ffffff',
|
||||
passcodeBackground: '#EEEFF1',
|
||||
passcodeButtonActive: '#E4E7EA',
|
||||
editAndUploadButtonAvatar: '#E4E7EA',
|
||||
passcodeLockIcon: '#6C727A',
|
||||
passcodePrimary: '#2F343D',
|
||||
passcodeSecondary: '#6C727A',
|
||||
|
@ -128,6 +129,7 @@ export const colors = {
|
|||
buttonText: '#ffffff',
|
||||
passcodeBackground: '#030C1B',
|
||||
passcodeButtonActive: '#0B182C',
|
||||
editAndUploadButtonAvatar: '#0B182C',
|
||||
passcodeLockIcon: '#6C727A',
|
||||
passcodePrimary: '#FFFFFF',
|
||||
passcodeSecondary: '#CBCED1',
|
||||
|
@ -197,6 +199,7 @@ export const colors = {
|
|||
buttonText: '#ffffff',
|
||||
passcodeBackground: '#000000',
|
||||
passcodeButtonActive: '#0E0D0D',
|
||||
editAndUploadButtonAvatar: '#0E0D0D',
|
||||
passcodeLockIcon: '#6C727A',
|
||||
passcodePrimary: '#FFFFFF',
|
||||
passcodeSecondary: '#CBCED1',
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import { Image } from 'react-native';
|
||||
|
||||
export const isImageURL = async (url: string): Promise<boolean> => {
|
||||
try {
|
||||
const result = await Image.prefetch(url);
|
||||
return result;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
|
@ -14,3 +14,4 @@ export * from './server';
|
|||
export * from './url';
|
||||
export * from './isValidEmail';
|
||||
export * from './random';
|
||||
export * from './image';
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import { Alert, Linking } from 'react-native';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { RUNNING_E2E_TESTS } from '@env';
|
||||
|
||||
import I18n from '../../../i18n';
|
||||
import { isFDroidBuild, STORE_REVIEW_LINK } from '../../constants';
|
||||
|
@ -88,7 +86,7 @@ class ReviewApp {
|
|||
positiveEventCount = 0;
|
||||
|
||||
pushPositiveEvent = () => {
|
||||
if (isFDroidBuild || RUNNING_E2E_TESTS === 'true') {
|
||||
if (isFDroidBuild || process.env.RUNNING_E2E_TESTS === 'true') {
|
||||
return;
|
||||
}
|
||||
if (this.positiveEventCount >= numberOfPositiveEvent) {
|
||||
|
|
|
@ -291,7 +291,7 @@ export default function subscribeRooms() {
|
|||
const [type, data] = ddpMessage.fields.args;
|
||||
const [, ev] = ddpMessage.fields.eventName.split('/');
|
||||
if (/userData/.test(ev)) {
|
||||
const [{ diff }] = ddpMessage.fields.args;
|
||||
const [{ diff, unset }] = ddpMessage.fields.args;
|
||||
if (diff?.statusLivechat) {
|
||||
store.dispatch(setUser({ statusLivechat: diff.statusLivechat }));
|
||||
}
|
||||
|
@ -301,6 +301,12 @@ export default function subscribeRooms() {
|
|||
if ((['settings.preferences.alsoSendThreadToChannel'] as any) in diff) {
|
||||
store.dispatch(setUser({ alsoSendThreadToChannel: diff['settings.preferences.alsoSendThreadToChannel'] }));
|
||||
}
|
||||
if (diff?.avatarETag) {
|
||||
store.dispatch(setUser({ avatarETag: diff.avatarETag }));
|
||||
}
|
||||
if (unset?.avatarETag) {
|
||||
store.dispatch(setUser({ avatarETag: '' }));
|
||||
}
|
||||
}
|
||||
if (/subscriptions/.test(ev)) {
|
||||
if (type === 'removed') {
|
||||
|
|
|
@ -561,7 +561,7 @@ export const saveRoomSettings = (
|
|||
rid: string,
|
||||
params: {
|
||||
roomName?: string;
|
||||
roomAvatar?: string;
|
||||
roomAvatar?: string | null;
|
||||
roomDescription?: string;
|
||||
roomTopic?: string;
|
||||
roomAnnouncement?: string;
|
||||
|
@ -602,7 +602,7 @@ export const getRoomRoles = (
|
|||
// RC 0.65.0
|
||||
sdk.get(`${roomTypeToApiType(type)}.roles`, { roomId });
|
||||
|
||||
export const getAvatarSuggestion = (): Promise<IAvatarSuggestion> =>
|
||||
export const getAvatarSuggestion = (): Promise<{ [service: string]: IAvatarSuggestion }> =>
|
||||
// RC 0.51.0
|
||||
sdk.methodCallWrapper('getAvatarSuggestion');
|
||||
|
||||
|
|
|
@ -247,6 +247,22 @@ const handleLogout = function* handleLogout({ forcedByServer, message }) {
|
|||
};
|
||||
|
||||
const handleSetUser = function* handleSetUser({ user }) {
|
||||
if ('avatarETag' in user) {
|
||||
const userId = yield select(state => state.login.user.id);
|
||||
const serversDB = database.servers;
|
||||
const userCollections = serversDB.get('users');
|
||||
yield serversDB.write(async () => {
|
||||
try {
|
||||
const userRecord = await userCollections.find(userId);
|
||||
await userRecord.update(record => {
|
||||
record.avatarETag = user.avatarETag;
|
||||
});
|
||||
} catch {
|
||||
//
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setLanguage(user?.language);
|
||||
|
||||
if (user?.statusLivechat && isOmnichannelModuleAvailable()) {
|
||||
|
|
|
@ -68,6 +68,7 @@ import AddChannelTeamView from '../views/AddChannelTeamView';
|
|||
import AddExistingChannelView from '../views/AddExistingChannelView';
|
||||
import SelectListView from '../views/SelectListView';
|
||||
import DiscussionsView from '../views/DiscussionsView';
|
||||
import ChangeAvatarView from '../views/ChangeAvatarView';
|
||||
import {
|
||||
AdminPanelStackParamList,
|
||||
ChatsStackParamList,
|
||||
|
@ -96,6 +97,7 @@ const ChatsStackNavigator = () => {
|
|||
<ChatsStack.Screen name='SelectListView' component={SelectListView} options={SelectListView.navigationOptions} />
|
||||
<ChatsStack.Screen name='RoomInfoView' component={RoomInfoView} options={RoomInfoView.navigationOptions} />
|
||||
<ChatsStack.Screen name='RoomInfoEditView' component={RoomInfoEditView} options={RoomInfoEditView.navigationOptions} />
|
||||
<ChatsStack.Screen name='ChangeAvatarView' component={ChangeAvatarView} />
|
||||
<ChatsStack.Screen name='RoomMembersView' component={RoomMembersView} />
|
||||
<ChatsStack.Screen name='DiscussionsView' component={DiscussionsView} />
|
||||
<ChatsStack.Screen
|
||||
|
@ -151,6 +153,7 @@ const ProfileStackNavigator = () => {
|
|||
>
|
||||
<ProfileStack.Screen name='ProfileView' component={ProfileView} options={ProfileView.navigationOptions} />
|
||||
<ProfileStack.Screen name='UserPreferencesView' component={UserPreferencesView} />
|
||||
<ProfileStack.Screen name='ChangeAvatarView' component={ChangeAvatarView} />
|
||||
<ProfileStack.Screen name='UserNotificationPrefView' component={UserNotificationPrefView} />
|
||||
<ProfileStack.Screen name='PickerView' component={PickerView} options={PickerView.navigationOptions} />
|
||||
</ProfileStack.Navigator>
|
||||
|
|
|
@ -17,6 +17,7 @@ import RoomsListView from '../../views/RoomsListView';
|
|||
import RoomActionsView from '../../views/RoomActionsView';
|
||||
import RoomInfoView from '../../views/RoomInfoView';
|
||||
import RoomInfoEditView from '../../views/RoomInfoEditView';
|
||||
import ChangeAvatarView from '../../views/ChangeAvatarView';
|
||||
import RoomMembersView from '../../views/RoomMembersView';
|
||||
import SearchMessagesView from '../../views/SearchMessagesView';
|
||||
import SelectedUsersView from '../../views/SelectedUsersView';
|
||||
|
@ -128,6 +129,7 @@ const ModalStackNavigator = React.memo(({ navigation }: INavigation) => {
|
|||
<ModalStack.Screen name='RoomInfoView' component={RoomInfoView} options={RoomInfoView.navigationOptions} />
|
||||
<ModalStack.Screen name='SelectListView' component={SelectListView} />
|
||||
<ModalStack.Screen name='RoomInfoEditView' component={RoomInfoEditView} options={RoomInfoEditView.navigationOptions} />
|
||||
<ModalStack.Screen name='ChangeAvatarView' component={ChangeAvatarView} />
|
||||
<ModalStack.Screen name='RoomMembersView' component={RoomMembersView} />
|
||||
<ModalStack.Screen
|
||||
name='SearchMessagesView'
|
||||
|
|
|
@ -6,6 +6,7 @@ import { IMessage } from '../../definitions/IMessage';
|
|||
import { ISubscription, SubscriptionType, TSubscriptionModel } from '../../definitions/ISubscription';
|
||||
import { ILivechatDepartment } from '../../definitions/ILivechatDepartment';
|
||||
import { ILivechatTag } from '../../definitions/ILivechatTag';
|
||||
import { TChangeAvatarViewContext } from '../../definitions/TChangeAvatarViewContext';
|
||||
|
||||
export type MasterDetailChatsStackParamList = {
|
||||
RoomView: {
|
||||
|
@ -58,6 +59,12 @@ export type ModalStackParamList = {
|
|||
onSearch?: Function;
|
||||
isRadio?: boolean;
|
||||
};
|
||||
ChangeAvatarView: {
|
||||
context: TChangeAvatarViewContext;
|
||||
titleHeader?: string;
|
||||
room?: ISubscription;
|
||||
t?: SubscriptionType;
|
||||
};
|
||||
RoomInfoEditView: {
|
||||
rid: string;
|
||||
};
|
||||
|
|
|
@ -13,6 +13,7 @@ import { ModalStackParamList } from './MasterDetailStack/types';
|
|||
import { TThreadModel } from '../definitions';
|
||||
import { ILivechatDepartment } from '../definitions/ILivechatDepartment';
|
||||
import { ILivechatTag } from '../definitions/ILivechatTag';
|
||||
import { TChangeAvatarViewContext } from '../definitions/TChangeAvatarViewContext';
|
||||
|
||||
export type ChatsStackParamList = {
|
||||
ModalStackNavigator: NavigatorScreenParams<ModalStackParamList>;
|
||||
|
@ -181,6 +182,12 @@ export type ChatsStackParamList = {
|
|||
onlyAudio?: boolean;
|
||||
videoConf?: boolean;
|
||||
};
|
||||
ChangeAvatarView: {
|
||||
context: TChangeAvatarViewContext;
|
||||
titleHeader?: string;
|
||||
room?: ISubscription;
|
||||
t?: SubscriptionType;
|
||||
};
|
||||
};
|
||||
|
||||
export type ProfileStackParamList = {
|
||||
|
@ -195,6 +202,12 @@ export type ProfileStackParamList = {
|
|||
goBack?: Function;
|
||||
onChangeValue: Function;
|
||||
};
|
||||
ChangeAvatarView: {
|
||||
context: TChangeAvatarViewContext;
|
||||
titleHeader?: string;
|
||||
room?: ISubscription;
|
||||
t?: SubscriptionType;
|
||||
};
|
||||
};
|
||||
|
||||
export type SettingsStackParamList = {
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
|
||||
import { IAvatar } from '../../definitions';
|
||||
import { Services } from '../../lib/services';
|
||||
import I18n from '../../i18n';
|
||||
import styles from './styles';
|
||||
import { useTheme } from '../../theme';
|
||||
import AvatarSuggestionItem from './AvatarSuggestionItem';
|
||||
|
||||
const AvatarSuggestion = ({
|
||||
onPress,
|
||||
username,
|
||||
resetAvatar
|
||||
}: {
|
||||
onPress: (value: IAvatar) => void;
|
||||
username?: string;
|
||||
resetAvatar?: () => void;
|
||||
}) => {
|
||||
const [avatarSuggestions, setAvatarSuggestions] = useState<IAvatar[]>([]);
|
||||
|
||||
const { colors } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
const getAvatarSuggestion = async () => {
|
||||
const result = await Services.getAvatarSuggestion();
|
||||
const suggestions = Object.keys(result).map(service => {
|
||||
const { url, blob, contentType } = result[service];
|
||||
return {
|
||||
url,
|
||||
data: blob,
|
||||
service,
|
||||
contentType
|
||||
};
|
||||
});
|
||||
setAvatarSuggestions(suggestions);
|
||||
};
|
||||
getAvatarSuggestion();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View style={styles.containerImagesUploaded}>
|
||||
<Text style={[styles.itemLabel, { color: colors.titleText }]}>{I18n.t('Images_uploaded')}</Text>
|
||||
<View style={styles.containerAvatarSuggestion}>
|
||||
{username && resetAvatar ? (
|
||||
<AvatarSuggestionItem text={`@${username}`} testID={`reset-avatar-suggestion`} onPress={resetAvatar} />
|
||||
) : null}
|
||||
{avatarSuggestions.slice(0, 7).map(item => (
|
||||
<AvatarSuggestionItem item={item} testID={`${item?.service}-avatar-suggestion`} onPress={onPress} />
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default AvatarSuggestion;
|
|
@ -0,0 +1,40 @@
|
|||
import React from 'react';
|
||||
import { View, StyleSheet } from 'react-native';
|
||||
|
||||
import { IAvatar } from '../../definitions';
|
||||
import Avatar from '../../containers/Avatar';
|
||||
import { useTheme } from '../../theme';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
width: 64,
|
||||
height: 64,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: 20,
|
||||
marginBottom: 12,
|
||||
borderRadius: 4
|
||||
}
|
||||
});
|
||||
|
||||
const AvatarSuggestionItem = ({
|
||||
item,
|
||||
onPress,
|
||||
text,
|
||||
testID
|
||||
}: {
|
||||
item?: IAvatar;
|
||||
testID?: string;
|
||||
onPress: Function;
|
||||
text?: string;
|
||||
}) => {
|
||||
const { colors } = useTheme();
|
||||
|
||||
return (
|
||||
<View key={item?.service} testID={testID} style={[styles.container, { backgroundColor: colors.borderColor }]}>
|
||||
<Avatar avatar={item?.url} text={text} size={64} onPress={() => onPress(item)} />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default AvatarSuggestionItem;
|
|
@ -0,0 +1,29 @@
|
|||
import React from 'react';
|
||||
|
||||
import I18n from '../../i18n';
|
||||
import { FormTextInput } from '../../containers/TextInput';
|
||||
import { useDebounce, isImageURL } from '../../lib/methods/helpers';
|
||||
|
||||
const AvatarUrl = ({ submit }: { submit: (value: string) => void }) => {
|
||||
const handleChangeText = useDebounce(async (value: string) => {
|
||||
if (value) {
|
||||
const result = await isImageURL(value);
|
||||
if (result) {
|
||||
return submit(value);
|
||||
}
|
||||
return submit('');
|
||||
}
|
||||
}, 500);
|
||||
|
||||
return (
|
||||
<FormTextInput
|
||||
label={I18n.t('Avatar_Url')}
|
||||
placeholder={I18n.t('insert_Avatar_URL')}
|
||||
onChangeText={handleChangeText}
|
||||
testID='change-avatar-view-avatar-url'
|
||||
containerStyle={{ marginBottom: 0 }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AvatarUrl;
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,4 @@
|
|||
import ImagePicker, { Image as ImageInterface } from 'react-native-image-crop-picker';
|
||||
|
||||
export type Image = ImageInterface;
|
||||
export default ImagePicker;
|
|
@ -0,0 +1,250 @@
|
|||
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';
|
||||
import { shallowEqual } from 'react-redux';
|
||||
|
||||
import KeyboardView from '../../containers/KeyboardView';
|
||||
import sharedStyles from '../Styles';
|
||||
import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps';
|
||||
import { showConfirmationAlert, showErrorAlert } from '../../lib/methods/helpers/info';
|
||||
import StatusBar from '../../containers/StatusBar';
|
||||
import { useTheme } from '../../theme';
|
||||
import SafeAreaView from '../../containers/SafeAreaView';
|
||||
import * as List from '../../containers/List';
|
||||
import styles from './styles';
|
||||
import { useAppSelector } from '../../lib/hooks';
|
||||
import { getUserSelector } from '../../selectors/login';
|
||||
import Avatar from '../../containers/Avatar';
|
||||
import AvatarPresentational from '../../containers/Avatar/Avatar';
|
||||
import AvatarUrl from './AvatarUrl';
|
||||
import Button from '../../containers/Button';
|
||||
import I18n from '../../i18n';
|
||||
import { ChatsStackParamList } from '../../stacks/types';
|
||||
import { IAvatar } from '../../definitions';
|
||||
import AvatarSuggestion from './AvatarSuggestion';
|
||||
import log from '../../lib/methods/helpers/log';
|
||||
import { changeRoomsAvatar, changeUserAvatar, resetUserAvatar } from './submitServices';
|
||||
import ImagePicker, { Image } from './ImagePicker';
|
||||
|
||||
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 [state, dispatch] = useReducer(reducer, initialState);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const { colors } = useTheme();
|
||||
const { userId, username, server } = useAppSelector(
|
||||
state => ({
|
||||
userId: getUserSelector(state).id,
|
||||
username: getUserSelector(state).username,
|
||||
server: state.server.server
|
||||
}),
|
||||
shallowEqual
|
||||
);
|
||||
const isDirty = useRef<boolean>(false);
|
||||
const navigation = useNavigation<StackNavigationProp<ChatsStackParamList, 'ChangeAvatarView'>>();
|
||||
const { context, titleHeader, room, t } = useRoute<RouteProp<ChatsStackParamList, 'ChangeAvatarView'>>().params;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
title: titleHeader || I18n.t('Avatar')
|
||||
});
|
||||
}, [titleHeader, navigation]);
|
||||
|
||||
useEffect(() => {
|
||||
navigation.addListener('beforeRemove', e => {
|
||||
if (!isDirty.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
showConfirmationAlert({
|
||||
title: I18n.t('Discard_changes'),
|
||||
message: I18n.t('Discard_changes_description'),
|
||||
confirmationText: I18n.t('Discard'),
|
||||
onPress: () => {
|
||||
navigation.dispatch(e.data.action);
|
||||
}
|
||||
});
|
||||
});
|
||||
}, [navigation]);
|
||||
|
||||
const dispatchAvatar = (action: IReducerAction) => {
|
||||
isDirty.current = true;
|
||||
dispatch(action);
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
try {
|
||||
setSaving(true);
|
||||
if (context === 'room' && room?.rid) {
|
||||
// Change Rooms Avatar
|
||||
await changeRoomsAvatar(room.rid, state?.data);
|
||||
} else if (state?.url) {
|
||||
// Change User's Avatar
|
||||
await changeUserAvatar(state);
|
||||
} else if (state.resetUserAvatar) {
|
||||
// Change User's Avatar
|
||||
await resetUserAvatar(userId);
|
||||
}
|
||||
isDirty.current = false;
|
||||
} catch (e: any) {
|
||||
log(e);
|
||||
return showErrorAlert(e.message, I18n.t('Oops'));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
return navigation.goBack();
|
||||
};
|
||||
|
||||
const pickImage = async () => {
|
||||
const options = {
|
||||
cropping: true,
|
||||
compressImageQuality: 0.8,
|
||||
freeStyleCropEnabled: true,
|
||||
cropperAvoidEmptySpaceAroundImage: false,
|
||||
cropperChooseText: I18n.t('Choose'),
|
||||
cropperCancelText: I18n.t('Cancel'),
|
||||
includeBase64: true
|
||||
};
|
||||
try {
|
||||
const response: Image = await ImagePicker.openPicker(options);
|
||||
dispatchAvatar({
|
||||
type: AvatarStateActions.CHANGE_AVATAR,
|
||||
payload: { url: response.path, data: `data:image/jpeg;base64,${response.data}`, service: 'upload' }
|
||||
});
|
||||
} catch (error) {
|
||||
log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const deletingRoomAvatar = context === 'room' && state.data === null;
|
||||
|
||||
return (
|
||||
<KeyboardView
|
||||
style={{ backgroundColor: colors.auxiliaryBackground }}
|
||||
contentContainerStyle={sharedStyles.container}
|
||||
keyboardVerticalOffset={128}
|
||||
>
|
||||
<StatusBar />
|
||||
<SafeAreaView testID='change-avatar-view'>
|
||||
<ScrollView
|
||||
contentContainerStyle={sharedStyles.containerScrollView}
|
||||
testID='change-avatar-view-list'
|
||||
{...scrollPersistTaps}
|
||||
>
|
||||
<View style={styles.avatarContainer} testID='change-avatar-view-avatar'>
|
||||
{deletingRoomAvatar ? (
|
||||
<AvatarPresentational
|
||||
text={room?.name || state.resetUserAvatar || username}
|
||||
avatar={state?.url}
|
||||
isStatic={state?.url}
|
||||
size={120}
|
||||
type={t}
|
||||
server={server}
|
||||
/>
|
||||
) : (
|
||||
<Avatar
|
||||
text={room?.name || state.resetUserAvatar || username}
|
||||
avatar={state?.url}
|
||||
isStatic={state?.url}
|
||||
size={120}
|
||||
type={t}
|
||||
rid={room?.rid}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
{context === 'profile' ? (
|
||||
<AvatarUrl
|
||||
submit={value =>
|
||||
dispatchAvatar({
|
||||
type: AvatarStateActions.CHANGE_AVATAR,
|
||||
payload: { url: value, data: value, service: 'url' }
|
||||
})
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
<List.Separator style={styles.separator} />
|
||||
{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
|
||||
title={I18n.t('Upload_image')}
|
||||
type='secondary'
|
||||
disabled={saving}
|
||||
backgroundColor={colors.editAndUploadButtonAvatar}
|
||||
onPress={pickImage}
|
||||
testID='change-avatar-view-upload-image'
|
||||
/>
|
||||
{context === 'room' ? (
|
||||
<Button
|
||||
title={I18n.t('Delete_image')}
|
||||
type='primary'
|
||||
disabled={saving}
|
||||
backgroundColor={colors.dangerColor}
|
||||
onPress={() => dispatchAvatar({ type: AvatarStateActions.RESET_ROOM_AVATAR, payload: { data: null } })}
|
||||
testID='change-avatar-view-delete-my-account'
|
||||
/>
|
||||
) : null}
|
||||
<Button
|
||||
title={I18n.t('Save')}
|
||||
disabled={!isDirty.current || saving}
|
||||
type='primary'
|
||||
loading={saving}
|
||||
onPress={submit}
|
||||
testID='change-avatar-view-submit'
|
||||
/>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
</KeyboardView>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChangeAvatarView;
|
|
@ -0,0 +1,27 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
|
||||
import sharedStyles from '../Styles';
|
||||
|
||||
export default StyleSheet.create({
|
||||
avatarContainer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 24
|
||||
},
|
||||
separator: {
|
||||
marginVertical: 16
|
||||
},
|
||||
itemLabel: {
|
||||
marginBottom: 12,
|
||||
fontSize: 14,
|
||||
...sharedStyles.textSemibold
|
||||
},
|
||||
containerImagesUploaded: {
|
||||
flex: 1
|
||||
},
|
||||
containerAvatarSuggestion: {
|
||||
flex: 1,
|
||||
flexWrap: 'wrap',
|
||||
flexDirection: 'row'
|
||||
}
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
import I18n from '../../i18n';
|
||||
|
||||
export const handleError = (e: any, action: string) => {
|
||||
if (e.data && e.data.error.includes('[error-too-many-requests]')) {
|
||||
throw new Error(e.data.error);
|
||||
}
|
||||
if (e.error && e.error === 'error-avatar-invalid-url') {
|
||||
throw new Error(I18n.t(e.error, { url: e.details.url }));
|
||||
}
|
||||
if (I18n.isTranslated(e.error)) {
|
||||
throw new Error(I18n.t(e.error));
|
||||
}
|
||||
throw new Error(I18n.t('There_was_an_error_while_action', { action: I18n.t(action) }));
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
import { Services } from '../../lib/services';
|
||||
import log from '../../lib/methods/helpers/log';
|
||||
import { IAvatar } from '../../definitions';
|
||||
import { handleError } from './submitHelpers';
|
||||
|
||||
export const changeRoomsAvatar = async (rid: string, roomAvatar: string | null) => {
|
||||
try {
|
||||
await Services.saveRoomSettings(rid, { roomAvatar });
|
||||
} catch (e) {
|
||||
log(e);
|
||||
return handleError(e, 'changing_avatar');
|
||||
}
|
||||
};
|
||||
|
||||
export const changeUserAvatar = async (avatarUpload: IAvatar) => {
|
||||
try {
|
||||
await Services.setAvatarFromService(avatarUpload);
|
||||
} catch (e) {
|
||||
return handleError(e, 'changing_avatar');
|
||||
}
|
||||
};
|
||||
|
||||
export const resetUserAvatar = async (userId: string) => {
|
||||
try {
|
||||
await Services.resetAvatar(userId);
|
||||
} catch (e) {
|
||||
return handleError(e, 'changing_avatar');
|
||||
}
|
||||
};
|
|
@ -2,7 +2,6 @@ import React from 'react';
|
|||
import { Keyboard, ScrollView, TextInput, View } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
import { sha256 } from 'js-sha256';
|
||||
import ImagePicker, { Image } from 'react-native-image-crop-picker';
|
||||
import RNPickerSelect from 'react-native-picker-select';
|
||||
import { dequal } from 'dequal';
|
||||
import omit from 'lodash/omit';
|
||||
|
@ -12,16 +11,15 @@ import Touch from '../../containers/Touch';
|
|||
import KeyboardView from '../../containers/KeyboardView';
|
||||
import sharedStyles from '../Styles';
|
||||
import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps';
|
||||
import { showConfirmationAlert, showErrorAlert } from '../../lib/methods/helpers';
|
||||
import { showErrorAlert, showConfirmationAlert } from '../../lib/methods/helpers';
|
||||
import { LISTENER } from '../../containers/Toast';
|
||||
import EventEmitter from '../../lib/methods/helpers/events';
|
||||
import { FormTextInput } from '../../containers/TextInput';
|
||||
import log, { events, logEvent } from '../../lib/methods/helpers/log';
|
||||
import { events, logEvent } from '../../lib/methods/helpers/log';
|
||||
import I18n from '../../i18n';
|
||||
import Button from '../../containers/Button';
|
||||
import Avatar from '../../containers/Avatar';
|
||||
import { AvatarWithEdit } from '../../containers/Avatar';
|
||||
import { setUser } from '../../actions/login';
|
||||
import { CustomIcon } from '../../containers/CustomIcon';
|
||||
import * as HeaderButton from '../../containers/HeaderButton';
|
||||
import StatusBar from '../../containers/StatusBar';
|
||||
import { themes } from '../../lib/constants';
|
||||
|
@ -31,15 +29,7 @@ import SafeAreaView from '../../containers/SafeAreaView';
|
|||
import styles from './styles';
|
||||
import { ProfileStackParamList } from '../../stacks/types';
|
||||
import { Services } from '../../lib/services';
|
||||
import {
|
||||
IApplicationState,
|
||||
IAvatar,
|
||||
IAvatarButton,
|
||||
IAvatarSuggestion,
|
||||
IBaseScreen,
|
||||
IProfileParams,
|
||||
IUser
|
||||
} from '../../definitions';
|
||||
import { IApplicationState, IAvatarButton, IBaseScreen, IProfileParams, IUser } from '../../definitions';
|
||||
import { twoFactor } from '../../lib/services/twoFactor';
|
||||
import { TwoFactorMethods } from '../../definitions/ITotp';
|
||||
import { withActionSheet, IActionSheetProvider } from '../../containers/ActionSheet';
|
||||
|
@ -67,9 +57,6 @@ interface IProfileViewState {
|
|||
email: string | null;
|
||||
newPassword: string | null;
|
||||
currentPassword: string | null;
|
||||
avatarUrl: string | null;
|
||||
avatar: IAvatar;
|
||||
avatarSuggestions: IAvatarSuggestion;
|
||||
customFields: {
|
||||
[key: string | number]: string;
|
||||
};
|
||||
|
@ -113,25 +100,12 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
|||
email: '',
|
||||
newPassword: '',
|
||||
currentPassword: '',
|
||||
avatarUrl: '',
|
||||
avatar: {
|
||||
data: {},
|
||||
url: ''
|
||||
},
|
||||
avatarSuggestions: {},
|
||||
customFields: {},
|
||||
twoFactorCode: null
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
componentDidMount() {
|
||||
this.init();
|
||||
|
||||
try {
|
||||
const result = await Services.getAvatarSuggestion();
|
||||
this.setState({ avatarSuggestions: result });
|
||||
} catch (e) {
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps: IProfileViewProps) {
|
||||
|
@ -147,16 +121,6 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
|||
}
|
||||
}
|
||||
|
||||
setAvatar = (avatar: IAvatar) => {
|
||||
const { Accounts_AllowUserAvatarChange } = this.props;
|
||||
|
||||
if (!Accounts_AllowUserAvatarChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ avatar });
|
||||
};
|
||||
|
||||
init = (user?: IUser) => {
|
||||
const { user: userProps } = this.props;
|
||||
const { name, username, emails, customFields } = user || userProps;
|
||||
|
@ -167,17 +131,12 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
|||
email: emails ? emails[0].address : null,
|
||||
newPassword: null,
|
||||
currentPassword: null,
|
||||
avatarUrl: null,
|
||||
avatar: {
|
||||
data: {},
|
||||
url: ''
|
||||
},
|
||||
customFields: customFields || {}
|
||||
});
|
||||
};
|
||||
|
||||
formIsChanged = () => {
|
||||
const { name, username, email, newPassword, avatar, customFields } = this.state;
|
||||
const { name, username, email, newPassword, customFields } = this.state;
|
||||
const { user } = this.props;
|
||||
let customFieldsChanged = false;
|
||||
|
||||
|
@ -196,21 +155,10 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
|||
!newPassword &&
|
||||
user.emails &&
|
||||
user.emails[0].address === email &&
|
||||
!avatar.data &&
|
||||
!customFieldsChanged
|
||||
);
|
||||
};
|
||||
|
||||
handleError = (e: any, _func: string, action: string) => {
|
||||
if (e.data && e.data.error.includes('[error-too-many-requests]')) {
|
||||
return showErrorAlert(e.data.error);
|
||||
}
|
||||
if (I18n.isTranslated(e.error)) {
|
||||
return showErrorAlert(I18n.t(e.error));
|
||||
}
|
||||
showErrorAlert(I18n.t('There_was_an_error_while_action', { action: I18n.t(action) }));
|
||||
};
|
||||
|
||||
submit = async (): Promise<void> => {
|
||||
Keyboard.dismiss();
|
||||
|
||||
|
@ -220,7 +168,7 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
|||
|
||||
this.setState({ saving: true });
|
||||
|
||||
const { name, username, email, newPassword, currentPassword, avatar, customFields, twoFactorCode } = this.state;
|
||||
const { name, username, email, newPassword, currentPassword, customFields, twoFactorCode } = this.state;
|
||||
const { user, dispatch } = this.props;
|
||||
const params = {} as IProfileParams;
|
||||
|
||||
|
@ -273,17 +221,6 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
|||
}
|
||||
|
||||
try {
|
||||
if (avatar.url) {
|
||||
try {
|
||||
logEvent(events.PROFILE_SAVE_AVATAR);
|
||||
await Services.setAvatarFromService(avatar);
|
||||
} catch (e) {
|
||||
logEvent(events.PROFILE_SAVE_AVATAR_F);
|
||||
this.setState({ saving: false, currentPassword: null });
|
||||
return this.handleError(e, 'setAvatarFromService', 'changing_avatar');
|
||||
}
|
||||
}
|
||||
|
||||
const twoFactorOptions = params.currentPassword
|
||||
? {
|
||||
twoFactorCode: params.currentPassword,
|
||||
|
@ -317,7 +254,7 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
|||
}
|
||||
logEvent(events.PROFILE_SAVE_CHANGES_F);
|
||||
this.setState({ saving: false, currentPassword: null, twoFactorCode: null });
|
||||
this.handleError(e, 'saveUserProfile', 'saving_profile');
|
||||
this.handleError(e, 'saving_profile');
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -334,39 +271,23 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
|||
EventEmitter.emit(LISTENER, { message: I18n.t('Avatar_changed_successfully') });
|
||||
this.init();
|
||||
} catch (e) {
|
||||
this.handleError(e, 'resetAvatar', 'changing_avatar');
|
||||
this.handleError(e, 'changing_avatar');
|
||||
}
|
||||
};
|
||||
|
||||
pickImage = async () => {
|
||||
const { Accounts_AllowUserAvatarChange } = this.props;
|
||||
|
||||
if (!Accounts_AllowUserAvatarChange) {
|
||||
return;
|
||||
handleError = (e: any, action: string) => {
|
||||
if (e.data && e.data.error.includes('[error-too-many-requests]')) {
|
||||
return showErrorAlert(e.data.error);
|
||||
}
|
||||
|
||||
const options = {
|
||||
cropping: true,
|
||||
compressImageQuality: 0.8,
|
||||
freeStyleCropEnabled: true,
|
||||
cropperAvoidEmptySpaceAroundImage: false,
|
||||
cropperChooseText: I18n.t('Choose'),
|
||||
cropperCancelText: I18n.t('Cancel'),
|
||||
includeBase64: true
|
||||
};
|
||||
try {
|
||||
logEvent(events.PROFILE_PICK_AVATAR);
|
||||
const response: Image = await ImagePicker.openPicker(options);
|
||||
this.setAvatar({ url: response.path, data: `data:image/jpeg;base64,${response.data}`, service: 'upload' });
|
||||
} catch (error) {
|
||||
logEvent(events.PROFILE_PICK_AVATAR_F);
|
||||
console.warn(error);
|
||||
if (I18n.isTranslated(e.error)) {
|
||||
return showErrorAlert(I18n.t(e.error));
|
||||
}
|
||||
showErrorAlert(I18n.t('There_was_an_error_while_action', { action: I18n.t(action) }));
|
||||
};
|
||||
|
||||
pickImageWithURL = (avatarUrl: string) => {
|
||||
logEvent(events.PROFILE_PICK_AVATAR_WITH_URL);
|
||||
this.setAvatar({ url: avatarUrl, data: avatarUrl, service: 'url' });
|
||||
handleEditAvatar = () => {
|
||||
const { navigation } = this.props;
|
||||
navigation.navigate('ChangeAvatarView', { context: 'profile' });
|
||||
};
|
||||
|
||||
renderAvatarButton = ({ key, child, onPress, disabled = false }: IAvatarButton) => {
|
||||
|
@ -384,49 +305,6 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
|||
);
|
||||
};
|
||||
|
||||
renderAvatarButtons = () => {
|
||||
const { avatarUrl, avatarSuggestions } = this.state;
|
||||
const { user, theme, Accounts_AllowUserAvatarChange } = this.props;
|
||||
|
||||
return (
|
||||
<View style={styles.avatarButtons}>
|
||||
{this.renderAvatarButton({
|
||||
child: <Avatar text={`@${user.username}`} size={50} />,
|
||||
onPress: () => this.resetAvatar(),
|
||||
disabled: !Accounts_AllowUserAvatarChange,
|
||||
key: 'profile-view-reset-avatar'
|
||||
})}
|
||||
{this.renderAvatarButton({
|
||||
child: <CustomIcon name='upload' size={30} color={themes[theme].bodyText} />,
|
||||
onPress: () => this.pickImage(),
|
||||
disabled: !Accounts_AllowUserAvatarChange,
|
||||
key: 'profile-view-upload-avatar'
|
||||
})}
|
||||
{this.renderAvatarButton({
|
||||
child: <CustomIcon name='link' size={30} color={themes[theme].bodyText} />,
|
||||
onPress: () => (avatarUrl ? this.pickImageWithURL(avatarUrl) : null),
|
||||
disabled: !avatarUrl,
|
||||
key: 'profile-view-avatar-url-button'
|
||||
})}
|
||||
{Object.keys(avatarSuggestions).map(service => {
|
||||
const { url, blob, contentType } = avatarSuggestions[service];
|
||||
return this.renderAvatarButton({
|
||||
disabled: !Accounts_AllowUserAvatarChange,
|
||||
key: `profile-view-avatar-${service}`,
|
||||
child: <Avatar avatar={url} size={50} />,
|
||||
onPress: () =>
|
||||
this.setAvatar({
|
||||
url,
|
||||
data: blob,
|
||||
service,
|
||||
contentType
|
||||
})
|
||||
});
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
renderCustomFields = () => {
|
||||
const { customFields } = this.state;
|
||||
const { Accounts_CustomFields } = this.props;
|
||||
|
@ -520,7 +398,7 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
|||
};
|
||||
|
||||
render() {
|
||||
const { name, username, email, newPassword, avatarUrl, customFields, avatar, saving } = this.state;
|
||||
const { name, username, email, newPassword, customFields, saving } = this.state;
|
||||
const {
|
||||
user,
|
||||
theme,
|
||||
|
@ -543,7 +421,10 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
|||
<SafeAreaView testID='profile-view'>
|
||||
<ScrollView contentContainerStyle={sharedStyles.containerScrollView} testID='profile-view-list' {...scrollPersistTaps}>
|
||||
<View style={styles.avatarContainer} testID='profile-view-avatar'>
|
||||
<Avatar text={user.username} avatar={avatar?.url} isStatic={avatar?.url} size={100} />
|
||||
<AvatarWithEdit
|
||||
text={user.username}
|
||||
handleEdit={Accounts_AllowUserAvatarChange ? this.handleEditAvatar : undefined}
|
||||
/>
|
||||
</View>
|
||||
<FormTextInput
|
||||
editable={Accounts_AllowRealNameChange}
|
||||
|
@ -615,22 +496,6 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
|||
testID='profile-view-new-password'
|
||||
/>
|
||||
{this.renderCustomFields()}
|
||||
<FormTextInput
|
||||
editable={Accounts_AllowUserAvatarChange}
|
||||
inputStyle={[!Accounts_AllowUserAvatarChange && styles.disabled]}
|
||||
inputRef={e => {
|
||||
if (e) {
|
||||
this.avatarUrl = e;
|
||||
}
|
||||
}}
|
||||
label={I18n.t('Avatar_Url')}
|
||||
placeholder={I18n.t('Avatar_Url')}
|
||||
value={avatarUrl || undefined}
|
||||
onChangeText={value => this.setState({ avatarUrl: value })}
|
||||
onSubmitEditing={this.submit}
|
||||
testID='profile-view-avatar-url'
|
||||
/>
|
||||
{this.renderAvatarButtons()}
|
||||
<Button
|
||||
title={I18n.t('Save_Changes')}
|
||||
type='primary'
|
||||
|
|
|
@ -7,7 +7,7 @@ export default StyleSheet.create({
|
|||
avatarContainer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 10
|
||||
marginBottom: 24
|
||||
},
|
||||
avatarButtons: {
|
||||
flexWrap: 'wrap',
|
||||
|
|
|
@ -2,15 +2,13 @@ import React from 'react';
|
|||
import { Q } from '@nozbe/watermelondb';
|
||||
import { BlockContext } from '@rocket.chat/ui-kit';
|
||||
import { dequal } from 'dequal';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { Alert, Keyboard, ScrollView, Text, TextInput, TouchableOpacity, View, StyleSheet } from 'react-native';
|
||||
import ImagePicker, { Image } from 'react-native-image-crop-picker';
|
||||
import { connect } from 'react-redux';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { deleteRoom } from '../../actions/room';
|
||||
import { themes } from '../../lib/constants';
|
||||
import Avatar from '../../containers/Avatar';
|
||||
import { AvatarWithEdit } from '../../containers/Avatar';
|
||||
import { sendLoadingEvent } from '../../containers/Loading';
|
||||
import SafeAreaView from '../../containers/SafeAreaView';
|
||||
import StatusBar from '../../containers/StatusBar';
|
||||
|
@ -23,13 +21,11 @@ import {
|
|||
IRoomSettings,
|
||||
ISubscription,
|
||||
SubscriptionType,
|
||||
TSubscriptionModel,
|
||||
IAvatar
|
||||
TSubscriptionModel
|
||||
} from '../../definitions';
|
||||
import { ERoomType } from '../../definitions/ERoomType';
|
||||
import I18n from '../../i18n';
|
||||
import database from '../../lib/database';
|
||||
import { CustomIcon } from '../../containers/CustomIcon';
|
||||
import KeyboardView from '../../containers/KeyboardView';
|
||||
import { TSupportedPermissions } from '../../reducers/permissions';
|
||||
import { ModalStackParamList } from '../../stacks/MasterDetailStack/types';
|
||||
|
@ -54,7 +50,6 @@ import { Services } from '../../lib/services';
|
|||
|
||||
interface IRoomInfoEditViewState {
|
||||
room: ISubscription;
|
||||
avatar: IAvatar;
|
||||
permissions: { [key in TSupportedPermissions]?: boolean };
|
||||
name: string;
|
||||
description?: string;
|
||||
|
@ -102,7 +97,6 @@ class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfo
|
|||
this.room = {} as TSubscriptionModel;
|
||||
this.state = {
|
||||
room: {} as ISubscription,
|
||||
avatar: {} as IAvatar,
|
||||
permissions: {},
|
||||
name: '',
|
||||
description: '',
|
||||
|
@ -192,7 +186,6 @@ class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfo
|
|||
topic,
|
||||
announcement,
|
||||
t: t === 'p',
|
||||
avatar: {} as IAvatar,
|
||||
ro,
|
||||
reactWhenReadOnly,
|
||||
joinCode: joinCodeRequired ? this.randomValue : '',
|
||||
|
@ -228,8 +221,7 @@ class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfo
|
|||
joinCode,
|
||||
systemMessages,
|
||||
enableSysMes,
|
||||
encrypted,
|
||||
avatar
|
||||
encrypted
|
||||
} = this.state;
|
||||
const { joinCodeRequired } = room;
|
||||
const sysMes = room.sysMes as string[];
|
||||
|
@ -244,28 +236,15 @@ class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfo
|
|||
room.reactWhenReadOnly === reactWhenReadOnly &&
|
||||
dequal(sysMes, systemMessages) &&
|
||||
enableSysMes === (sysMes && sysMes.length > 0) &&
|
||||
room.encrypted === encrypted &&
|
||||
isEmpty(avatar)
|
||||
room.encrypted === encrypted
|
||||
);
|
||||
};
|
||||
|
||||
submit = async () => {
|
||||
logEvent(events.RI_EDIT_SAVE);
|
||||
Keyboard.dismiss();
|
||||
const {
|
||||
room,
|
||||
name,
|
||||
description,
|
||||
topic,
|
||||
announcement,
|
||||
t,
|
||||
ro,
|
||||
reactWhenReadOnly,
|
||||
joinCode,
|
||||
systemMessages,
|
||||
encrypted,
|
||||
avatar
|
||||
} = this.state;
|
||||
const { room, name, description, topic, announcement, t, ro, reactWhenReadOnly, joinCode, systemMessages, encrypted } =
|
||||
this.state;
|
||||
|
||||
sendLoadingEvent({ visible: true });
|
||||
let error = false;
|
||||
|
@ -284,10 +263,6 @@ class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfo
|
|||
if (getRoomTitle(room) !== name) {
|
||||
params.roomName = name;
|
||||
}
|
||||
// Avatar
|
||||
if (!isEmpty(avatar)) {
|
||||
params.roomAvatar = avatar.data as string;
|
||||
}
|
||||
// Description
|
||||
if (room.description !== description) {
|
||||
params.roomDescription = description;
|
||||
|
@ -487,26 +462,10 @@ class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfo
|
|||
);
|
||||
};
|
||||
|
||||
changeAvatar = async () => {
|
||||
const options = {
|
||||
cropping: true,
|
||||
compressImageQuality: 0.8,
|
||||
cropperAvoidEmptySpaceAroundImage: false,
|
||||
cropperChooseText: I18n.t('Choose'),
|
||||
cropperCancelText: I18n.t('Cancel'),
|
||||
includeBase64: true
|
||||
};
|
||||
|
||||
try {
|
||||
const response: Image = await ImagePicker.openPicker(options);
|
||||
this.setState({ avatar: { url: response.path, data: `data:image/jpeg;base64,${response.data}`, service: 'upload' } });
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
|
||||
resetAvatar = () => {
|
||||
this.setState({ avatar: { data: null } });
|
||||
handleEditAvatar = () => {
|
||||
const { navigation } = this.props;
|
||||
const { room } = this.state;
|
||||
navigation.navigate('ChangeAvatarView', { titleHeader: I18n.t('Room_Info'), room, t: room.t, context: 'room' });
|
||||
};
|
||||
|
||||
toggleRoomType = (value: boolean) => {
|
||||
|
@ -549,8 +508,7 @@ class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfo
|
|||
permissions,
|
||||
archived,
|
||||
enableSysMes,
|
||||
encrypted,
|
||||
avatar
|
||||
encrypted
|
||||
} = this.state;
|
||||
const { serverVersion, encryptionEnabled, theme } = this.props;
|
||||
const { dangerColor } = themes[theme];
|
||||
|
@ -568,29 +526,9 @@ class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfo
|
|||
testID='room-info-edit-view-list'
|
||||
{...scrollPersistTaps}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={styles.avatarContainer}
|
||||
onPress={this.changeAvatar}
|
||||
disabled={compareServerVersion(serverVersion || '', 'lowerThan', '3.6.0')}
|
||||
>
|
||||
<Avatar
|
||||
type={room.t}
|
||||
text={room.name}
|
||||
avatar={avatar?.url}
|
||||
isStatic={avatar?.url}
|
||||
rid={isEmpty(avatar) ? room.rid : undefined}
|
||||
size={100}
|
||||
>
|
||||
{serverVersion && compareServerVersion(serverVersion, 'lowerThan', '3.6.0') ? undefined : (
|
||||
<TouchableOpacity
|
||||
style={[styles.resetButton, { backgroundColor: themes[theme].dangerColor }]}
|
||||
onPress={this.resetAvatar}
|
||||
>
|
||||
<CustomIcon name='delete' color={themes[theme].backgroundColor} size={24} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</Avatar>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.avatarContainer}>
|
||||
<AvatarWithEdit type={room.t} text={room.name} rid={room.rid} handleEdit={this.handleEditAvatar} />
|
||||
</View>
|
||||
<FormTextInput
|
||||
inputRef={e => {
|
||||
this.name = e;
|
||||
|
|
|
@ -76,7 +76,8 @@ export default StyleSheet.create({
|
|||
avatarContainer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 10
|
||||
marginBottom: 32,
|
||||
marginTop: 16
|
||||
},
|
||||
resetButton: {
|
||||
padding: 4,
|
||||
|
|
|
@ -8,7 +8,7 @@ import { connect } from 'react-redux';
|
|||
import { Observable, Subscription } from 'rxjs';
|
||||
import UAParser from 'ua-parser-js';
|
||||
|
||||
import Avatar from '../../containers/Avatar';
|
||||
import { AvatarWithEdit } from '../../containers/Avatar';
|
||||
import { CustomIcon, TIconsName } from '../../containers/CustomIcon';
|
||||
import * as HeaderButton from '../../containers/HeaderButton';
|
||||
import { MarkdownPreview } from '../../containers/markdown';
|
||||
|
@ -394,17 +394,32 @@ class RoomInfoView extends React.Component<IRoomInfoViewProps, IRoomInfoViewStat
|
|||
log(e);
|
||||
}
|
||||
};
|
||||
|
||||
handleEditAvatar = () => {
|
||||
const { navigation } = this.props;
|
||||
const { room } = this.state;
|
||||
navigation.navigate('ChangeAvatarView', { titleHeader: I18n.t('Room_Info'), room, t: this.t, context: 'room' });
|
||||
};
|
||||
|
||||
renderAvatar = (room: ISubscription, roomUser: IUserParsed) => {
|
||||
const { theme } = this.props;
|
||||
const { showEdit } = this.state;
|
||||
const showAvatarEdit = showEdit && this.t !== SubscriptionType.OMNICHANNEL;
|
||||
|
||||
return (
|
||||
<Avatar text={room.name || roomUser.username} style={styles.avatar} type={this.t} size={100} rid={room?.rid}>
|
||||
<AvatarWithEdit
|
||||
text={room.name || roomUser.username}
|
||||
style={styles.avatar}
|
||||
type={this.t}
|
||||
rid={room?.rid}
|
||||
handleEdit={showAvatarEdit ? this.handleEditAvatar : undefined}
|
||||
>
|
||||
{this.t === SubscriptionType.DIRECT && roomUser._id ? (
|
||||
<View style={[sharedStyles.status, { backgroundColor: themes[theme].auxiliaryBackground }]}>
|
||||
<Status size={20} id={roomUser._id} />
|
||||
</View>
|
||||
) : null}
|
||||
</Avatar>
|
||||
</AvatarWithEdit>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -21,13 +21,14 @@ export default StyleSheet.create({
|
|||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 20,
|
||||
paddingVertical: 8
|
||||
paddingBottom: 8,
|
||||
paddingTop: 32
|
||||
},
|
||||
avatar: {
|
||||
marginHorizontal: 10
|
||||
},
|
||||
roomTitleContainer: {
|
||||
paddingTop: 20,
|
||||
paddingTop: 32,
|
||||
marginHorizontal: 16,
|
||||
alignItems: 'center'
|
||||
},
|
||||
|
|
|
@ -24,6 +24,7 @@ const styles = StyleSheet.create({
|
|||
});
|
||||
|
||||
interface ILeftButtonsProps {
|
||||
rid?: string;
|
||||
tmid?: string;
|
||||
unreadsCount: number | null;
|
||||
navigation: StackNavigationProp<ChatsStackParamList, 'RoomView'>;
|
||||
|
@ -38,6 +39,7 @@ interface ILeftButtonsProps {
|
|||
}
|
||||
|
||||
const LeftButtons = ({
|
||||
rid,
|
||||
tmid,
|
||||
unreadsCount,
|
||||
navigation,
|
||||
|
@ -78,7 +80,7 @@ const LeftButtons = ({
|
|||
}
|
||||
|
||||
if (baseUrl && userId && token) {
|
||||
return <Avatar text={title} size={30} type={t} style={styles.avatar} onPress={onPress} />;
|
||||
return <Avatar rid={rid} text={title} size={30} type={t} style={styles.avatar} onPress={onPress} />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
|
|
@ -589,6 +589,7 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
|
|||
headerRightContainerStyle: { flexGrow: undefined, flexBasis: undefined },
|
||||
headerLeft: () => (
|
||||
<LeftButtons
|
||||
rid={rid}
|
||||
tmid={tmid}
|
||||
unreadsCount={unreadsCount}
|
||||
navigation={navigation}
|
||||
|
|
|
@ -33,8 +33,7 @@ WIP: End-to-end tests are a work in progress and they're going to change.
|
|||
- Ask Diego Mello for credentials
|
||||
|
||||
## Shared config
|
||||
- Change `.env` to `RUNNING_E2E_TESTS=true`
|
||||
- You can also `RUNNING_E2E_TESTS=true yarn start reset-cache`, but it's easier to change the file as long as you don't commit it
|
||||
- To start the Metro bundler in the mocked mode, you should run `yarn e2e:start`
|
||||
|
||||
## Setup and run iOS
|
||||
|
||||
|
|
|
@ -136,3 +136,8 @@ export const post = async (endpoint: string, body: any, user: ITestUser) => {
|
|||
console.log(`POST /${endpoint} ${JSON.stringify(body)}`);
|
||||
return rocketchat.post(endpoint, body);
|
||||
};
|
||||
|
||||
export const getProfileInfo = async (userId: string) => {
|
||||
const result = await get(`users.info?userId=${userId}`);
|
||||
return result.data.user;
|
||||
};
|
||||
|
|
|
@ -11,14 +11,13 @@ async function waitForToast() {
|
|||
}
|
||||
|
||||
describe('Profile screen', () => {
|
||||
let scrollViewType: string;
|
||||
let textMatcher: TTextMatcher;
|
||||
let user: ITestUser;
|
||||
|
||||
beforeAll(async () => {
|
||||
user = await createRandomUser();
|
||||
await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
|
||||
({ scrollViewType, textMatcher } = platformTypes[device.getPlatform()]);
|
||||
({ textMatcher } = platformTypes[device.getPlatform()]);
|
||||
await navigateToLogin();
|
||||
await login(user.username, user.password);
|
||||
await element(by.id('rooms-list-view-sidebar')).tap();
|
||||
|
@ -43,6 +42,10 @@ describe('Profile screen', () => {
|
|||
await expect(element(by.id('profile-view-avatar')).atIndex(0)).toExist();
|
||||
});
|
||||
|
||||
it('should have avatar edit button', async () => {
|
||||
await expect(element(by.id('avatar-edit-button'))).toExist();
|
||||
});
|
||||
|
||||
it('should have name', async () => {
|
||||
await expect(element(by.id('profile-view-name'))).toExist();
|
||||
});
|
||||
|
@ -59,31 +62,6 @@ describe('Profile screen', () => {
|
|||
await expect(element(by.id('profile-view-new-password'))).toExist();
|
||||
});
|
||||
|
||||
it('should have avatar url', async () => {
|
||||
await expect(element(by.id('profile-view-avatar-url'))).toExist();
|
||||
});
|
||||
|
||||
it('should have reset avatar button', async () => {
|
||||
await waitFor(element(by.id('profile-view-reset-avatar')))
|
||||
.toExist()
|
||||
.whileElement(by.id('profile-view-list'))
|
||||
.scroll(scrollDown, 'down');
|
||||
});
|
||||
|
||||
it('should have upload avatar button', async () => {
|
||||
await waitFor(element(by.id('profile-view-upload-avatar')))
|
||||
.toExist()
|
||||
.whileElement(by.id('profile-view-list'))
|
||||
.scroll(scrollDown, 'down');
|
||||
});
|
||||
|
||||
it('should have avatar url button', async () => {
|
||||
await waitFor(element(by.id('profile-view-avatar-url-button')))
|
||||
.toExist()
|
||||
.whileElement(by.id('profile-view-list'))
|
||||
.scroll(scrollDown, 'down');
|
||||
});
|
||||
|
||||
it('should have submit button', async () => {
|
||||
await waitFor(element(by.id('profile-view-submit')))
|
||||
.toExist()
|
||||
|
@ -96,17 +74,23 @@ describe('Profile screen', () => {
|
|||
it('should change name and username', async () => {
|
||||
await element(by.id('profile-view-name')).replaceText(`${user.username}new`);
|
||||
await element(by.id('profile-view-username')).replaceText(`${user.username}new`);
|
||||
// dismiss keyboard
|
||||
await element(by.id('profile-view-list')).swipe('down');
|
||||
await element(by.id('profile-view-submit')).tap();
|
||||
await waitForToast();
|
||||
});
|
||||
|
||||
it('should change email and password', async () => {
|
||||
await element(by.id('profile-view-list')).swipe('up');
|
||||
await waitFor(element(by.id('profile-view-email')))
|
||||
.toBeVisible()
|
||||
.withTimeout(2000);
|
||||
await element(by.id('profile-view-email')).replaceText(`mobile+profileChangesNew${random()}@rocket.chat`);
|
||||
// dismiss keyboard
|
||||
await element(by.id('profile-view-list')).swipe('down');
|
||||
await element(by.id('profile-view-new-password')).replaceText(`${user.password}new`);
|
||||
// dismiss keyboard
|
||||
await element(by.id('profile-view-list')).swipe('down');
|
||||
await waitFor(element(by.id('profile-view-submit')))
|
||||
.toExist()
|
||||
.withTimeout(2000);
|
||||
|
@ -120,11 +104,5 @@ describe('Profile screen', () => {
|
|||
.tap();
|
||||
await waitForToast();
|
||||
});
|
||||
|
||||
it('should reset avatar', async () => {
|
||||
await element(by.type(scrollViewType)).atIndex(1).swipe('up');
|
||||
await element(by.id('profile-view-reset-avatar')).tap();
|
||||
await waitForToast();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
import { device, waitFor, element, by } from 'detox';
|
||||
|
||||
import { navigateToLogin, login, sleep, platformTypes, TTextMatcher, tapBack } from '../../helpers/app';
|
||||
import { createRandomUser, getProfileInfo, ITestUser, login as loginSetup } from '../../helpers/data_setup';
|
||||
|
||||
describe('Profile screen', () => {
|
||||
let scrollViewType: string;
|
||||
let textMatcher: TTextMatcher;
|
||||
let user: ITestUser;
|
||||
let userId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
user = await createRandomUser();
|
||||
const result = await loginSetup(user.username, user.password);
|
||||
userId = result.userId;
|
||||
await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
|
||||
({ scrollViewType, textMatcher } = platformTypes[device.getPlatform()]);
|
||||
await navigateToLogin();
|
||||
await login(user.username, user.password);
|
||||
await element(by.id('rooms-list-view-sidebar')).tap();
|
||||
await waitFor(element(by.id('sidebar-view')))
|
||||
.toBeVisible()
|
||||
.withTimeout(2000);
|
||||
await waitFor(element(by.id('sidebar-profile')))
|
||||
.toBeVisible()
|
||||
.withTimeout(2000);
|
||||
await element(by.id('sidebar-profile')).tap();
|
||||
await waitFor(element(by.id('profile-view')))
|
||||
.toBeVisible()
|
||||
.withTimeout(2000);
|
||||
});
|
||||
|
||||
describe('Usage', () => {
|
||||
it('should click on the reset avatar button', async () => {
|
||||
await element(by.type(scrollViewType)).atIndex(1).swipe('down');
|
||||
await element(by.id('avatar-edit-button')).tap();
|
||||
await waitFor(element(by.id('change-avatar-view-avatar')))
|
||||
.toBeVisible()
|
||||
.withTimeout(2000);
|
||||
await waitFor(element(by.id('reset-avatar-suggestion')))
|
||||
.toBeVisible()
|
||||
.withTimeout(2000);
|
||||
await element(by.id('reset-avatar-suggestion')).tap();
|
||||
await sleep(300);
|
||||
});
|
||||
|
||||
it('should appear the discard alert when click the back icon ', async () => {
|
||||
await tapBack();
|
||||
await waitFor(element(by[textMatcher]('Discard changes?')).atIndex(0))
|
||||
.toBeVisible()
|
||||
.withTimeout(2000);
|
||||
await waitFor(element(by[textMatcher]('Cancel')).atIndex(0))
|
||||
.toBeVisible()
|
||||
.withTimeout(2000);
|
||||
await element(by[textMatcher]('Cancel')).atIndex(0).tap();
|
||||
await sleep(200);
|
||||
await tapBack();
|
||||
await waitFor(element(by[textMatcher]('Discard changes?')).atIndex(0))
|
||||
.toBeVisible()
|
||||
.withTimeout(2000);
|
||||
await waitFor(element(by[textMatcher]('Discard')).atIndex(0))
|
||||
.toBeVisible()
|
||||
.withTimeout(2000);
|
||||
await element(by[textMatcher]('Discard')).atIndex(0).tap();
|
||||
await sleep(200);
|
||||
});
|
||||
|
||||
it('should change the avatar through a base64 image mocked', async () => {
|
||||
await element(by.type(scrollViewType)).atIndex(1).swipe('down');
|
||||
await element(by.id('avatar-edit-button')).tap();
|
||||
const previousUserInfo = await getProfileInfo(userId);
|
||||
const previousAvatarEtag = previousUserInfo.avatarETag;
|
||||
await sleep(500);
|
||||
await waitFor(element(by.id('change-avatar-view-upload-image')))
|
||||
.toBeVisible()
|
||||
.withTimeout(2000);
|
||||
await element(by.id('change-avatar-view-upload-image')).tap();
|
||||
await waitFor(element(by.id('change-avatar-view-submit')))
|
||||
.toBeVisible()
|
||||
.withTimeout(2000);
|
||||
await element(by.id('change-avatar-view-submit')).tap();
|
||||
await waitFor(element(by.id('profile-view')))
|
||||
.toBeVisible()
|
||||
.withTimeout(2000);
|
||||
await sleep(300);
|
||||
const newUserInfo = await getProfileInfo(userId);
|
||||
const newAvatarEtag = newUserInfo.avatarETag;
|
||||
await sleep(500);
|
||||
if (previousAvatarEtag === newAvatarEtag) {
|
||||
throw new Error('Failed to update the avatar');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -16,7 +16,7 @@ async function navigateToRoomInfo(room: string) {
|
|||
}
|
||||
|
||||
async function swipe(direction: Detox.Direction) {
|
||||
await element(by.id('room-info-edit-view-list')).swipe(direction, 'fast', 0.8, 0.2);
|
||||
await element(by.id('room-info-edit-view-list')).swipe(direction);
|
||||
}
|
||||
|
||||
async function waitForToast() {
|
||||
|
@ -185,6 +185,8 @@ describe('Room info screen', () => {
|
|||
await sleep(5000); // wait for changes to be applied from socket
|
||||
await element(by.id('room-info-edit-view-description')).replaceText('new description');
|
||||
await element(by.id('room-info-edit-view-topic')).replaceText('new topic');
|
||||
await swipe('down'); // dismiss keyboard
|
||||
// announcement is hide by the keyboard
|
||||
await element(by.id('room-info-edit-view-announcement')).replaceText('new announcement');
|
||||
await element(by.id('room-info-edit-view-announcement')).tapReturnKey();
|
||||
await element(by.id('room-info-edit-view-password')).tapReturnKey();
|
||||
|
@ -243,6 +245,7 @@ describe('Room info screen', () => {
|
|||
});
|
||||
|
||||
it('should delete room', async () => {
|
||||
await element(by.id('room-info-edit-view-list')).swipe('up');
|
||||
await element(by.id('room-info-edit-view-delete')).tap();
|
||||
await waitFor(element(by[textMatcher]('Yes, delete it!')))
|
||||
.toExist()
|
||||
|
|
|
@ -85,7 +85,7 @@ describe('Create team screen', () => {
|
|||
await waitFor(element(by.id('room-info-edit-view-list')))
|
||||
.toBeVisible()
|
||||
.withTimeout(2000);
|
||||
await element(by.id('room-info-edit-view-list')).swipe('up', 'fast', 0.5);
|
||||
await element(by.id('room-info-edit-view-list')).swipe('up');
|
||||
await waitFor(element(by.id('room-info-edit-view-delete')))
|
||||
.toBeVisible()
|
||||
.withTimeout(2000);
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
const blocklist = require('metro-config/src/defaults/exclusionList');
|
||||
|
||||
const defaultSourceExts = require('metro-config/src/defaults/defaults').sourceExts;
|
||||
|
||||
module.exports = {
|
||||
transformer: {
|
||||
getTransformOptions: () => ({
|
||||
|
@ -20,6 +22,7 @@ module.exports = {
|
|||
maxWorkers: 2,
|
||||
resolver: {
|
||||
blocklistRE: blocklist([/ios\/Pods\/JitsiMeetSDK\/Frameworks\/JitsiMeet.framework\/assets\/node_modules\/react-native\/.*/]),
|
||||
resolverMainFields: ['sbmodern', 'react-native', 'browser', 'main']
|
||||
resolverMainFields: ['sbmodern', 'react-native', 'browser', 'main'],
|
||||
sourceExts: process.env.RUNNING_E2E_TESTS ? ['mock.ts', ...defaultSourceExts] : defaultSourceExts
|
||||
}
|
||||
};
|
||||
|
|
|
@ -26,7 +26,8 @@
|
|||
"e2e:ios-build-debug": "yarn detox build -c ios.sim.debug",
|
||||
"e2e:ios-test-debug": "yarn detox test -c ios.sim.debug",
|
||||
"e2e:ios-build": "yarn detox build -c ios.sim.release",
|
||||
"e2e:ios-test": "yarn detox test -c ios.sim.release"
|
||||
"e2e:ios-test": "yarn detox test -c ios.sim.release",
|
||||
"e2e:start": "RUNNING_E2E_TESTS=true npx react-native start"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,ts,tsx}": [
|
||||
|
|
|
@ -17413,7 +17413,7 @@ react-native-gradle-plugin@^0.0.6:
|
|||
|
||||
react-native-image-crop-picker@RocketChat/react-native-image-crop-picker:
|
||||
version "0.36.3"
|
||||
resolved "https://codeload.github.com/RocketChat/react-native-image-crop-picker/tar.gz/f347776247afb5cbd1400dde215689d7ca8fd6f2"
|
||||
resolved "https://codeload.github.com/RocketChat/react-native-image-crop-picker/tar.gz/35f5c90576cadcdd4c0eb5cef83a9f1b7b7e26e3"
|
||||
|
||||
react-native-image-progress@^1.1.1:
|
||||
version "1.1.1"
|
||||
|
|
Loading…
Reference in New Issue