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:
|
- run:
|
||||||
name: Build Android
|
name: Build Android
|
||||||
command: |
|
command: |
|
||||||
echo "RUNNING_E2E_TESTS=true" > ./.env
|
export RUNNING_E2E_TESTS=true
|
||||||
yarn e2e:android-build
|
yarn e2e:android-build
|
||||||
- save_cache: *save-gradle-cache
|
- save_cache: *save-gradle-cache
|
||||||
- store_artifacts:
|
- 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/RocketChatRN/Info.plist
|
||||||
/usr/libexec/PlistBuddy -c "Set :bugsnag:apiKey $BUGSNAG_KEY" ./ios/ShareRocketChatRN/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
|
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
|
yarn e2e:ios-build
|
||||||
- persist_to_workspace:
|
- persist_to_workspace:
|
||||||
root: /Users/distiller/project
|
root: /Users/distiller/project
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
import { Q } from '@nozbe/watermelondb';
|
import React from 'react';
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
|
||||||
import { shallowEqual, useSelector } from 'react-redux';
|
import { shallowEqual, useSelector } from 'react-redux';
|
||||||
import { Observable, Subscription } from 'rxjs';
|
|
||||||
|
|
||||||
import { IApplicationState, TSubscriptionModel, TUserModel } from '../../definitions';
|
import { IApplicationState } from '../../definitions';
|
||||||
import database from '../../lib/database';
|
|
||||||
import { getUserSelector } from '../../selectors/login';
|
import { getUserSelector } from '../../selectors/login';
|
||||||
import Avatar from './Avatar';
|
import Avatar from './Avatar';
|
||||||
import { IAvatar } from './interfaces';
|
import { IAvatar } from './interfaces';
|
||||||
|
import { useAvatarETag } from './useAvatarETag';
|
||||||
|
|
||||||
const AvatarContainer = ({
|
const AvatarContainer = ({
|
||||||
style,
|
style,
|
||||||
|
@ -23,17 +21,13 @@ const AvatarContainer = ({
|
||||||
isStatic,
|
isStatic,
|
||||||
rid
|
rid
|
||||||
}: IAvatar): React.ReactElement => {
|
}: 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 server = useSelector((state: IApplicationState) => state.share.server.server || state.server.server);
|
||||||
const serverVersion = useSelector((state: IApplicationState) => state.share.server.version || state.server.version);
|
const serverVersion = useSelector((state: IApplicationState) => state.share.server.version || state.server.version);
|
||||||
const { id, token } = useSelector(
|
const { id, token, username } = useSelector(
|
||||||
(state: IApplicationState) => ({
|
(state: IApplicationState) => ({
|
||||||
id: getUserSelector(state).id,
|
id: getUserSelector(state).id,
|
||||||
token: getUserSelector(state).token
|
token: getUserSelector(state).token,
|
||||||
|
username: getUserSelector(state).username
|
||||||
}),
|
}),
|
||||||
shallowEqual
|
shallowEqual
|
||||||
);
|
);
|
||||||
|
@ -48,41 +42,7 @@ const AvatarContainer = ({
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
const init = async () => {
|
const { avatarETag } = useAvatarETag({ username, text, type, rid, id });
|
||||||
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]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Avatar
|
<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;
|
loading?: boolean;
|
||||||
color?: string;
|
color?: string;
|
||||||
fontSize?: number;
|
fontSize?: number;
|
||||||
styleText?: StyleProp<TextStyle>[];
|
styleText?: StyleProp<TextStyle> | StyleProp<TextStyle>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
|
|
@ -17,16 +17,14 @@ export interface IAvatarButton {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAvatar {
|
export interface IAvatar {
|
||||||
data: {} | string | null;
|
data: string | null;
|
||||||
url?: string;
|
url?: string;
|
||||||
contentType?: string;
|
contentType?: string;
|
||||||
service?: any;
|
service?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAvatarSuggestion {
|
export interface IAvatarSuggestion {
|
||||||
[service: string]: {
|
url: string;
|
||||||
url: string;
|
blob: string;
|
||||||
blob: string;
|
contentType: string;
|
||||||
contentType: string;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
export type TChangeAvatarViewContext = 'profile' | 'room';
|
|
@ -877,6 +877,14 @@
|
||||||
"Reply_in_direct_message": "Reply in Direct Message",
|
"Reply_in_direct_message": "Reply in Direct Message",
|
||||||
"room_archived": "archived room",
|
"room_archived": "archived room",
|
||||||
"room_unarchived": "unarchived 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-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.",
|
"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",
|
"admin-no-videoconf-provider-app-header": "Conference call not enabled",
|
||||||
|
|
|
@ -876,6 +876,14 @@
|
||||||
"Reply_in_direct_message": "Responder por mensagem direta",
|
"Reply_in_direct_message": "Responder por mensagem direta",
|
||||||
"room_archived": "{{username}} arquivou a sala",
|
"room_archived": "{{username}} arquivou a sala",
|
||||||
"room_unarchived": "{{username}} desarquivou 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_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."
|
"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',
|
buttonText: '#ffffff',
|
||||||
passcodeBackground: '#EEEFF1',
|
passcodeBackground: '#EEEFF1',
|
||||||
passcodeButtonActive: '#E4E7EA',
|
passcodeButtonActive: '#E4E7EA',
|
||||||
|
editAndUploadButtonAvatar: '#E4E7EA',
|
||||||
passcodeLockIcon: '#6C727A',
|
passcodeLockIcon: '#6C727A',
|
||||||
passcodePrimary: '#2F343D',
|
passcodePrimary: '#2F343D',
|
||||||
passcodeSecondary: '#6C727A',
|
passcodeSecondary: '#6C727A',
|
||||||
|
@ -128,6 +129,7 @@ export const colors = {
|
||||||
buttonText: '#ffffff',
|
buttonText: '#ffffff',
|
||||||
passcodeBackground: '#030C1B',
|
passcodeBackground: '#030C1B',
|
||||||
passcodeButtonActive: '#0B182C',
|
passcodeButtonActive: '#0B182C',
|
||||||
|
editAndUploadButtonAvatar: '#0B182C',
|
||||||
passcodeLockIcon: '#6C727A',
|
passcodeLockIcon: '#6C727A',
|
||||||
passcodePrimary: '#FFFFFF',
|
passcodePrimary: '#FFFFFF',
|
||||||
passcodeSecondary: '#CBCED1',
|
passcodeSecondary: '#CBCED1',
|
||||||
|
@ -197,6 +199,7 @@ export const colors = {
|
||||||
buttonText: '#ffffff',
|
buttonText: '#ffffff',
|
||||||
passcodeBackground: '#000000',
|
passcodeBackground: '#000000',
|
||||||
passcodeButtonActive: '#0E0D0D',
|
passcodeButtonActive: '#0E0D0D',
|
||||||
|
editAndUploadButtonAvatar: '#0E0D0D',
|
||||||
passcodeLockIcon: '#6C727A',
|
passcodeLockIcon: '#6C727A',
|
||||||
passcodePrimary: '#FFFFFF',
|
passcodePrimary: '#FFFFFF',
|
||||||
passcodeSecondary: '#CBCED1',
|
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 './url';
|
||||||
export * from './isValidEmail';
|
export * from './isValidEmail';
|
||||||
export * from './random';
|
export * from './random';
|
||||||
|
export * from './image';
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import { Alert, Linking } from 'react-native';
|
import { Alert, Linking } from 'react-native';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
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 I18n from '../../../i18n';
|
||||||
import { isFDroidBuild, STORE_REVIEW_LINK } from '../../constants';
|
import { isFDroidBuild, STORE_REVIEW_LINK } from '../../constants';
|
||||||
|
@ -88,7 +86,7 @@ class ReviewApp {
|
||||||
positiveEventCount = 0;
|
positiveEventCount = 0;
|
||||||
|
|
||||||
pushPositiveEvent = () => {
|
pushPositiveEvent = () => {
|
||||||
if (isFDroidBuild || RUNNING_E2E_TESTS === 'true') {
|
if (isFDroidBuild || process.env.RUNNING_E2E_TESTS === 'true') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.positiveEventCount >= numberOfPositiveEvent) {
|
if (this.positiveEventCount >= numberOfPositiveEvent) {
|
||||||
|
|
|
@ -291,7 +291,7 @@ export default function subscribeRooms() {
|
||||||
const [type, data] = ddpMessage.fields.args;
|
const [type, data] = ddpMessage.fields.args;
|
||||||
const [, ev] = ddpMessage.fields.eventName.split('/');
|
const [, ev] = ddpMessage.fields.eventName.split('/');
|
||||||
if (/userData/.test(ev)) {
|
if (/userData/.test(ev)) {
|
||||||
const [{ diff }] = ddpMessage.fields.args;
|
const [{ diff, unset }] = ddpMessage.fields.args;
|
||||||
if (diff?.statusLivechat) {
|
if (diff?.statusLivechat) {
|
||||||
store.dispatch(setUser({ statusLivechat: diff.statusLivechat }));
|
store.dispatch(setUser({ statusLivechat: diff.statusLivechat }));
|
||||||
}
|
}
|
||||||
|
@ -301,6 +301,12 @@ export default function subscribeRooms() {
|
||||||
if ((['settings.preferences.alsoSendThreadToChannel'] as any) in diff) {
|
if ((['settings.preferences.alsoSendThreadToChannel'] as any) in diff) {
|
||||||
store.dispatch(setUser({ alsoSendThreadToChannel: diff['settings.preferences.alsoSendThreadToChannel'] }));
|
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 (/subscriptions/.test(ev)) {
|
||||||
if (type === 'removed') {
|
if (type === 'removed') {
|
||||||
|
|
|
@ -561,7 +561,7 @@ export const saveRoomSettings = (
|
||||||
rid: string,
|
rid: string,
|
||||||
params: {
|
params: {
|
||||||
roomName?: string;
|
roomName?: string;
|
||||||
roomAvatar?: string;
|
roomAvatar?: string | null;
|
||||||
roomDescription?: string;
|
roomDescription?: string;
|
||||||
roomTopic?: string;
|
roomTopic?: string;
|
||||||
roomAnnouncement?: string;
|
roomAnnouncement?: string;
|
||||||
|
@ -602,7 +602,7 @@ export const getRoomRoles = (
|
||||||
// RC 0.65.0
|
// RC 0.65.0
|
||||||
sdk.get(`${roomTypeToApiType(type)}.roles`, { roomId });
|
sdk.get(`${roomTypeToApiType(type)}.roles`, { roomId });
|
||||||
|
|
||||||
export const getAvatarSuggestion = (): Promise<IAvatarSuggestion> =>
|
export const getAvatarSuggestion = (): Promise<{ [service: string]: IAvatarSuggestion }> =>
|
||||||
// RC 0.51.0
|
// RC 0.51.0
|
||||||
sdk.methodCallWrapper('getAvatarSuggestion');
|
sdk.methodCallWrapper('getAvatarSuggestion');
|
||||||
|
|
||||||
|
|
|
@ -247,6 +247,22 @@ const handleLogout = function* handleLogout({ forcedByServer, message }) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSetUser = function* handleSetUser({ user }) {
|
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);
|
setLanguage(user?.language);
|
||||||
|
|
||||||
if (user?.statusLivechat && isOmnichannelModuleAvailable()) {
|
if (user?.statusLivechat && isOmnichannelModuleAvailable()) {
|
||||||
|
|
|
@ -68,6 +68,7 @@ import AddChannelTeamView from '../views/AddChannelTeamView';
|
||||||
import AddExistingChannelView from '../views/AddExistingChannelView';
|
import AddExistingChannelView from '../views/AddExistingChannelView';
|
||||||
import SelectListView from '../views/SelectListView';
|
import SelectListView from '../views/SelectListView';
|
||||||
import DiscussionsView from '../views/DiscussionsView';
|
import DiscussionsView from '../views/DiscussionsView';
|
||||||
|
import ChangeAvatarView from '../views/ChangeAvatarView';
|
||||||
import {
|
import {
|
||||||
AdminPanelStackParamList,
|
AdminPanelStackParamList,
|
||||||
ChatsStackParamList,
|
ChatsStackParamList,
|
||||||
|
@ -96,6 +97,7 @@ const ChatsStackNavigator = () => {
|
||||||
<ChatsStack.Screen name='SelectListView' component={SelectListView} options={SelectListView.navigationOptions} />
|
<ChatsStack.Screen name='SelectListView' component={SelectListView} options={SelectListView.navigationOptions} />
|
||||||
<ChatsStack.Screen name='RoomInfoView' component={RoomInfoView} options={RoomInfoView.navigationOptions} />
|
<ChatsStack.Screen name='RoomInfoView' component={RoomInfoView} options={RoomInfoView.navigationOptions} />
|
||||||
<ChatsStack.Screen name='RoomInfoEditView' component={RoomInfoEditView} options={RoomInfoEditView.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='RoomMembersView' component={RoomMembersView} />
|
||||||
<ChatsStack.Screen name='DiscussionsView' component={DiscussionsView} />
|
<ChatsStack.Screen name='DiscussionsView' component={DiscussionsView} />
|
||||||
<ChatsStack.Screen
|
<ChatsStack.Screen
|
||||||
|
@ -151,6 +153,7 @@ const ProfileStackNavigator = () => {
|
||||||
>
|
>
|
||||||
<ProfileStack.Screen name='ProfileView' component={ProfileView} options={ProfileView.navigationOptions} />
|
<ProfileStack.Screen name='ProfileView' component={ProfileView} options={ProfileView.navigationOptions} />
|
||||||
<ProfileStack.Screen name='UserPreferencesView' component={UserPreferencesView} />
|
<ProfileStack.Screen name='UserPreferencesView' component={UserPreferencesView} />
|
||||||
|
<ProfileStack.Screen name='ChangeAvatarView' component={ChangeAvatarView} />
|
||||||
<ProfileStack.Screen name='UserNotificationPrefView' component={UserNotificationPrefView} />
|
<ProfileStack.Screen name='UserNotificationPrefView' component={UserNotificationPrefView} />
|
||||||
<ProfileStack.Screen name='PickerView' component={PickerView} options={PickerView.navigationOptions} />
|
<ProfileStack.Screen name='PickerView' component={PickerView} options={PickerView.navigationOptions} />
|
||||||
</ProfileStack.Navigator>
|
</ProfileStack.Navigator>
|
||||||
|
|
|
@ -17,6 +17,7 @@ import RoomsListView from '../../views/RoomsListView';
|
||||||
import RoomActionsView from '../../views/RoomActionsView';
|
import RoomActionsView from '../../views/RoomActionsView';
|
||||||
import RoomInfoView from '../../views/RoomInfoView';
|
import RoomInfoView from '../../views/RoomInfoView';
|
||||||
import RoomInfoEditView from '../../views/RoomInfoEditView';
|
import RoomInfoEditView from '../../views/RoomInfoEditView';
|
||||||
|
import ChangeAvatarView from '../../views/ChangeAvatarView';
|
||||||
import RoomMembersView from '../../views/RoomMembersView';
|
import RoomMembersView from '../../views/RoomMembersView';
|
||||||
import SearchMessagesView from '../../views/SearchMessagesView';
|
import SearchMessagesView from '../../views/SearchMessagesView';
|
||||||
import SelectedUsersView from '../../views/SelectedUsersView';
|
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='RoomInfoView' component={RoomInfoView} options={RoomInfoView.navigationOptions} />
|
||||||
<ModalStack.Screen name='SelectListView' component={SelectListView} />
|
<ModalStack.Screen name='SelectListView' component={SelectListView} />
|
||||||
<ModalStack.Screen name='RoomInfoEditView' component={RoomInfoEditView} options={RoomInfoEditView.navigationOptions} />
|
<ModalStack.Screen name='RoomInfoEditView' component={RoomInfoEditView} options={RoomInfoEditView.navigationOptions} />
|
||||||
|
<ModalStack.Screen name='ChangeAvatarView' component={ChangeAvatarView} />
|
||||||
<ModalStack.Screen name='RoomMembersView' component={RoomMembersView} />
|
<ModalStack.Screen name='RoomMembersView' component={RoomMembersView} />
|
||||||
<ModalStack.Screen
|
<ModalStack.Screen
|
||||||
name='SearchMessagesView'
|
name='SearchMessagesView'
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { IMessage } from '../../definitions/IMessage';
|
||||||
import { ISubscription, SubscriptionType, TSubscriptionModel } from '../../definitions/ISubscription';
|
import { ISubscription, SubscriptionType, TSubscriptionModel } from '../../definitions/ISubscription';
|
||||||
import { ILivechatDepartment } from '../../definitions/ILivechatDepartment';
|
import { ILivechatDepartment } from '../../definitions/ILivechatDepartment';
|
||||||
import { ILivechatTag } from '../../definitions/ILivechatTag';
|
import { ILivechatTag } from '../../definitions/ILivechatTag';
|
||||||
|
import { TChangeAvatarViewContext } from '../../definitions/TChangeAvatarViewContext';
|
||||||
|
|
||||||
export type MasterDetailChatsStackParamList = {
|
export type MasterDetailChatsStackParamList = {
|
||||||
RoomView: {
|
RoomView: {
|
||||||
|
@ -58,6 +59,12 @@ export type ModalStackParamList = {
|
||||||
onSearch?: Function;
|
onSearch?: Function;
|
||||||
isRadio?: boolean;
|
isRadio?: boolean;
|
||||||
};
|
};
|
||||||
|
ChangeAvatarView: {
|
||||||
|
context: TChangeAvatarViewContext;
|
||||||
|
titleHeader?: string;
|
||||||
|
room?: ISubscription;
|
||||||
|
t?: SubscriptionType;
|
||||||
|
};
|
||||||
RoomInfoEditView: {
|
RoomInfoEditView: {
|
||||||
rid: string;
|
rid: string;
|
||||||
};
|
};
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { ModalStackParamList } from './MasterDetailStack/types';
|
||||||
import { TThreadModel } from '../definitions';
|
import { TThreadModel } from '../definitions';
|
||||||
import { ILivechatDepartment } from '../definitions/ILivechatDepartment';
|
import { ILivechatDepartment } from '../definitions/ILivechatDepartment';
|
||||||
import { ILivechatTag } from '../definitions/ILivechatTag';
|
import { ILivechatTag } from '../definitions/ILivechatTag';
|
||||||
|
import { TChangeAvatarViewContext } from '../definitions/TChangeAvatarViewContext';
|
||||||
|
|
||||||
export type ChatsStackParamList = {
|
export type ChatsStackParamList = {
|
||||||
ModalStackNavigator: NavigatorScreenParams<ModalStackParamList>;
|
ModalStackNavigator: NavigatorScreenParams<ModalStackParamList>;
|
||||||
|
@ -181,6 +182,12 @@ export type ChatsStackParamList = {
|
||||||
onlyAudio?: boolean;
|
onlyAudio?: boolean;
|
||||||
videoConf?: boolean;
|
videoConf?: boolean;
|
||||||
};
|
};
|
||||||
|
ChangeAvatarView: {
|
||||||
|
context: TChangeAvatarViewContext;
|
||||||
|
titleHeader?: string;
|
||||||
|
room?: ISubscription;
|
||||||
|
t?: SubscriptionType;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ProfileStackParamList = {
|
export type ProfileStackParamList = {
|
||||||
|
@ -195,6 +202,12 @@ export type ProfileStackParamList = {
|
||||||
goBack?: Function;
|
goBack?: Function;
|
||||||
onChangeValue: Function;
|
onChangeValue: Function;
|
||||||
};
|
};
|
||||||
|
ChangeAvatarView: {
|
||||||
|
context: TChangeAvatarViewContext;
|
||||||
|
titleHeader?: string;
|
||||||
|
room?: ISubscription;
|
||||||
|
t?: SubscriptionType;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SettingsStackParamList = {
|
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 { Keyboard, ScrollView, TextInput, View } from 'react-native';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { sha256 } from 'js-sha256';
|
import { sha256 } from 'js-sha256';
|
||||||
import ImagePicker, { Image } from 'react-native-image-crop-picker';
|
|
||||||
import RNPickerSelect from 'react-native-picker-select';
|
import RNPickerSelect from 'react-native-picker-select';
|
||||||
import { dequal } from 'dequal';
|
import { dequal } from 'dequal';
|
||||||
import omit from 'lodash/omit';
|
import omit from 'lodash/omit';
|
||||||
|
@ -12,16 +11,15 @@ import Touch from '../../containers/Touch';
|
||||||
import KeyboardView from '../../containers/KeyboardView';
|
import KeyboardView from '../../containers/KeyboardView';
|
||||||
import sharedStyles from '../Styles';
|
import sharedStyles from '../Styles';
|
||||||
import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps';
|
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 { LISTENER } from '../../containers/Toast';
|
||||||
import EventEmitter from '../../lib/methods/helpers/events';
|
import EventEmitter from '../../lib/methods/helpers/events';
|
||||||
import { FormTextInput } from '../../containers/TextInput';
|
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 I18n from '../../i18n';
|
||||||
import Button from '../../containers/Button';
|
import Button from '../../containers/Button';
|
||||||
import Avatar from '../../containers/Avatar';
|
import { AvatarWithEdit } from '../../containers/Avatar';
|
||||||
import { setUser } from '../../actions/login';
|
import { setUser } from '../../actions/login';
|
||||||
import { CustomIcon } from '../../containers/CustomIcon';
|
|
||||||
import * as HeaderButton from '../../containers/HeaderButton';
|
import * as HeaderButton from '../../containers/HeaderButton';
|
||||||
import StatusBar from '../../containers/StatusBar';
|
import StatusBar from '../../containers/StatusBar';
|
||||||
import { themes } from '../../lib/constants';
|
import { themes } from '../../lib/constants';
|
||||||
|
@ -31,15 +29,7 @@ import SafeAreaView from '../../containers/SafeAreaView';
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
import { ProfileStackParamList } from '../../stacks/types';
|
import { ProfileStackParamList } from '../../stacks/types';
|
||||||
import { Services } from '../../lib/services';
|
import { Services } from '../../lib/services';
|
||||||
import {
|
import { IApplicationState, IAvatarButton, IBaseScreen, IProfileParams, IUser } from '../../definitions';
|
||||||
IApplicationState,
|
|
||||||
IAvatar,
|
|
||||||
IAvatarButton,
|
|
||||||
IAvatarSuggestion,
|
|
||||||
IBaseScreen,
|
|
||||||
IProfileParams,
|
|
||||||
IUser
|
|
||||||
} from '../../definitions';
|
|
||||||
import { twoFactor } from '../../lib/services/twoFactor';
|
import { twoFactor } from '../../lib/services/twoFactor';
|
||||||
import { TwoFactorMethods } from '../../definitions/ITotp';
|
import { TwoFactorMethods } from '../../definitions/ITotp';
|
||||||
import { withActionSheet, IActionSheetProvider } from '../../containers/ActionSheet';
|
import { withActionSheet, IActionSheetProvider } from '../../containers/ActionSheet';
|
||||||
|
@ -67,9 +57,6 @@ interface IProfileViewState {
|
||||||
email: string | null;
|
email: string | null;
|
||||||
newPassword: string | null;
|
newPassword: string | null;
|
||||||
currentPassword: string | null;
|
currentPassword: string | null;
|
||||||
avatarUrl: string | null;
|
|
||||||
avatar: IAvatar;
|
|
||||||
avatarSuggestions: IAvatarSuggestion;
|
|
||||||
customFields: {
|
customFields: {
|
||||||
[key: string | number]: string;
|
[key: string | number]: string;
|
||||||
};
|
};
|
||||||
|
@ -113,25 +100,12 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
||||||
email: '',
|
email: '',
|
||||||
newPassword: '',
|
newPassword: '',
|
||||||
currentPassword: '',
|
currentPassword: '',
|
||||||
avatarUrl: '',
|
|
||||||
avatar: {
|
|
||||||
data: {},
|
|
||||||
url: ''
|
|
||||||
},
|
|
||||||
avatarSuggestions: {},
|
|
||||||
customFields: {},
|
customFields: {},
|
||||||
twoFactorCode: null
|
twoFactorCode: null
|
||||||
};
|
};
|
||||||
|
|
||||||
async componentDidMount() {
|
componentDidMount() {
|
||||||
this.init();
|
this.init();
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await Services.getAvatarSuggestion();
|
|
||||||
this.setState({ avatarSuggestions: result });
|
|
||||||
} catch (e) {
|
|
||||||
log(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
UNSAFE_componentWillReceiveProps(nextProps: IProfileViewProps) {
|
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) => {
|
init = (user?: IUser) => {
|
||||||
const { user: userProps } = this.props;
|
const { user: userProps } = this.props;
|
||||||
const { name, username, emails, customFields } = user || userProps;
|
const { name, username, emails, customFields } = user || userProps;
|
||||||
|
@ -167,17 +131,12 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
||||||
email: emails ? emails[0].address : null,
|
email: emails ? emails[0].address : null,
|
||||||
newPassword: null,
|
newPassword: null,
|
||||||
currentPassword: null,
|
currentPassword: null,
|
||||||
avatarUrl: null,
|
|
||||||
avatar: {
|
|
||||||
data: {},
|
|
||||||
url: ''
|
|
||||||
},
|
|
||||||
customFields: customFields || {}
|
customFields: customFields || {}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
formIsChanged = () => {
|
formIsChanged = () => {
|
||||||
const { name, username, email, newPassword, avatar, customFields } = this.state;
|
const { name, username, email, newPassword, customFields } = this.state;
|
||||||
const { user } = this.props;
|
const { user } = this.props;
|
||||||
let customFieldsChanged = false;
|
let customFieldsChanged = false;
|
||||||
|
|
||||||
|
@ -196,21 +155,10 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
||||||
!newPassword &&
|
!newPassword &&
|
||||||
user.emails &&
|
user.emails &&
|
||||||
user.emails[0].address === email &&
|
user.emails[0].address === email &&
|
||||||
!avatar.data &&
|
|
||||||
!customFieldsChanged
|
!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> => {
|
submit = async (): Promise<void> => {
|
||||||
Keyboard.dismiss();
|
Keyboard.dismiss();
|
||||||
|
|
||||||
|
@ -220,7 +168,7 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
||||||
|
|
||||||
this.setState({ saving: true });
|
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 { user, dispatch } = this.props;
|
||||||
const params = {} as IProfileParams;
|
const params = {} as IProfileParams;
|
||||||
|
|
||||||
|
@ -273,17 +221,6 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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
|
const twoFactorOptions = params.currentPassword
|
||||||
? {
|
? {
|
||||||
twoFactorCode: params.currentPassword,
|
twoFactorCode: params.currentPassword,
|
||||||
|
@ -317,7 +254,7 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
||||||
}
|
}
|
||||||
logEvent(events.PROFILE_SAVE_CHANGES_F);
|
logEvent(events.PROFILE_SAVE_CHANGES_F);
|
||||||
this.setState({ saving: false, currentPassword: null, twoFactorCode: null });
|
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') });
|
EventEmitter.emit(LISTENER, { message: I18n.t('Avatar_changed_successfully') });
|
||||||
this.init();
|
this.init();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.handleError(e, 'resetAvatar', 'changing_avatar');
|
this.handleError(e, 'changing_avatar');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pickImage = async () => {
|
handleError = (e: any, action: string) => {
|
||||||
const { Accounts_AllowUserAvatarChange } = this.props;
|
if (e.data && e.data.error.includes('[error-too-many-requests]')) {
|
||||||
|
return showErrorAlert(e.data.error);
|
||||||
if (!Accounts_AllowUserAvatarChange) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
if (I18n.isTranslated(e.error)) {
|
||||||
const options = {
|
return showErrorAlert(I18n.t(e.error));
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
showErrorAlert(I18n.t('There_was_an_error_while_action', { action: I18n.t(action) }));
|
||||||
};
|
};
|
||||||
|
|
||||||
pickImageWithURL = (avatarUrl: string) => {
|
handleEditAvatar = () => {
|
||||||
logEvent(events.PROFILE_PICK_AVATAR_WITH_URL);
|
const { navigation } = this.props;
|
||||||
this.setAvatar({ url: avatarUrl, data: avatarUrl, service: 'url' });
|
navigation.navigate('ChangeAvatarView', { context: 'profile' });
|
||||||
};
|
};
|
||||||
|
|
||||||
renderAvatarButton = ({ key, child, onPress, disabled = false }: IAvatarButton) => {
|
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 = () => {
|
renderCustomFields = () => {
|
||||||
const { customFields } = this.state;
|
const { customFields } = this.state;
|
||||||
const { Accounts_CustomFields } = this.props;
|
const { Accounts_CustomFields } = this.props;
|
||||||
|
@ -520,7 +398,7 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { name, username, email, newPassword, avatarUrl, customFields, avatar, saving } = this.state;
|
const { name, username, email, newPassword, customFields, saving } = this.state;
|
||||||
const {
|
const {
|
||||||
user,
|
user,
|
||||||
theme,
|
theme,
|
||||||
|
@ -543,7 +421,10 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
||||||
<SafeAreaView testID='profile-view'>
|
<SafeAreaView testID='profile-view'>
|
||||||
<ScrollView contentContainerStyle={sharedStyles.containerScrollView} testID='profile-view-list' {...scrollPersistTaps}>
|
<ScrollView contentContainerStyle={sharedStyles.containerScrollView} testID='profile-view-list' {...scrollPersistTaps}>
|
||||||
<View style={styles.avatarContainer} testID='profile-view-avatar'>
|
<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>
|
</View>
|
||||||
<FormTextInput
|
<FormTextInput
|
||||||
editable={Accounts_AllowRealNameChange}
|
editable={Accounts_AllowRealNameChange}
|
||||||
|
@ -615,22 +496,6 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
||||||
testID='profile-view-new-password'
|
testID='profile-view-new-password'
|
||||||
/>
|
/>
|
||||||
{this.renderCustomFields()}
|
{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
|
<Button
|
||||||
title={I18n.t('Save_Changes')}
|
title={I18n.t('Save_Changes')}
|
||||||
type='primary'
|
type='primary'
|
||||||
|
|
|
@ -7,7 +7,7 @@ export default StyleSheet.create({
|
||||||
avatarContainer: {
|
avatarContainer: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
marginBottom: 10
|
marginBottom: 24
|
||||||
},
|
},
|
||||||
avatarButtons: {
|
avatarButtons: {
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
|
|
|
@ -2,15 +2,13 @@ import React from 'react';
|
||||||
import { Q } from '@nozbe/watermelondb';
|
import { Q } from '@nozbe/watermelondb';
|
||||||
import { BlockContext } from '@rocket.chat/ui-kit';
|
import { BlockContext } from '@rocket.chat/ui-kit';
|
||||||
import { dequal } from 'dequal';
|
import { dequal } from 'dequal';
|
||||||
import isEmpty from 'lodash/isEmpty';
|
|
||||||
import { Alert, Keyboard, ScrollView, Text, TextInput, TouchableOpacity, View, StyleSheet } from 'react-native';
|
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 { connect } from 'react-redux';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
import { deleteRoom } from '../../actions/room';
|
import { deleteRoom } from '../../actions/room';
|
||||||
import { themes } from '../../lib/constants';
|
import { themes } from '../../lib/constants';
|
||||||
import Avatar from '../../containers/Avatar';
|
import { AvatarWithEdit } from '../../containers/Avatar';
|
||||||
import { sendLoadingEvent } from '../../containers/Loading';
|
import { sendLoadingEvent } from '../../containers/Loading';
|
||||||
import SafeAreaView from '../../containers/SafeAreaView';
|
import SafeAreaView from '../../containers/SafeAreaView';
|
||||||
import StatusBar from '../../containers/StatusBar';
|
import StatusBar from '../../containers/StatusBar';
|
||||||
|
@ -23,13 +21,11 @@ import {
|
||||||
IRoomSettings,
|
IRoomSettings,
|
||||||
ISubscription,
|
ISubscription,
|
||||||
SubscriptionType,
|
SubscriptionType,
|
||||||
TSubscriptionModel,
|
TSubscriptionModel
|
||||||
IAvatar
|
|
||||||
} from '../../definitions';
|
} from '../../definitions';
|
||||||
import { ERoomType } from '../../definitions/ERoomType';
|
import { ERoomType } from '../../definitions/ERoomType';
|
||||||
import I18n from '../../i18n';
|
import I18n from '../../i18n';
|
||||||
import database from '../../lib/database';
|
import database from '../../lib/database';
|
||||||
import { CustomIcon } from '../../containers/CustomIcon';
|
|
||||||
import KeyboardView from '../../containers/KeyboardView';
|
import KeyboardView from '../../containers/KeyboardView';
|
||||||
import { TSupportedPermissions } from '../../reducers/permissions';
|
import { TSupportedPermissions } from '../../reducers/permissions';
|
||||||
import { ModalStackParamList } from '../../stacks/MasterDetailStack/types';
|
import { ModalStackParamList } from '../../stacks/MasterDetailStack/types';
|
||||||
|
@ -54,7 +50,6 @@ import { Services } from '../../lib/services';
|
||||||
|
|
||||||
interface IRoomInfoEditViewState {
|
interface IRoomInfoEditViewState {
|
||||||
room: ISubscription;
|
room: ISubscription;
|
||||||
avatar: IAvatar;
|
|
||||||
permissions: { [key in TSupportedPermissions]?: boolean };
|
permissions: { [key in TSupportedPermissions]?: boolean };
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
@ -102,7 +97,6 @@ class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfo
|
||||||
this.room = {} as TSubscriptionModel;
|
this.room = {} as TSubscriptionModel;
|
||||||
this.state = {
|
this.state = {
|
||||||
room: {} as ISubscription,
|
room: {} as ISubscription,
|
||||||
avatar: {} as IAvatar,
|
|
||||||
permissions: {},
|
permissions: {},
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
|
@ -192,7 +186,6 @@ class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfo
|
||||||
topic,
|
topic,
|
||||||
announcement,
|
announcement,
|
||||||
t: t === 'p',
|
t: t === 'p',
|
||||||
avatar: {} as IAvatar,
|
|
||||||
ro,
|
ro,
|
||||||
reactWhenReadOnly,
|
reactWhenReadOnly,
|
||||||
joinCode: joinCodeRequired ? this.randomValue : '',
|
joinCode: joinCodeRequired ? this.randomValue : '',
|
||||||
|
@ -228,8 +221,7 @@ class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfo
|
||||||
joinCode,
|
joinCode,
|
||||||
systemMessages,
|
systemMessages,
|
||||||
enableSysMes,
|
enableSysMes,
|
||||||
encrypted,
|
encrypted
|
||||||
avatar
|
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const { joinCodeRequired } = room;
|
const { joinCodeRequired } = room;
|
||||||
const sysMes = room.sysMes as string[];
|
const sysMes = room.sysMes as string[];
|
||||||
|
@ -244,28 +236,15 @@ class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfo
|
||||||
room.reactWhenReadOnly === reactWhenReadOnly &&
|
room.reactWhenReadOnly === reactWhenReadOnly &&
|
||||||
dequal(sysMes, systemMessages) &&
|
dequal(sysMes, systemMessages) &&
|
||||||
enableSysMes === (sysMes && sysMes.length > 0) &&
|
enableSysMes === (sysMes && sysMes.length > 0) &&
|
||||||
room.encrypted === encrypted &&
|
room.encrypted === encrypted
|
||||||
isEmpty(avatar)
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
submit = async () => {
|
submit = async () => {
|
||||||
logEvent(events.RI_EDIT_SAVE);
|
logEvent(events.RI_EDIT_SAVE);
|
||||||
Keyboard.dismiss();
|
Keyboard.dismiss();
|
||||||
const {
|
const { room, name, description, topic, announcement, t, ro, reactWhenReadOnly, joinCode, systemMessages, encrypted } =
|
||||||
room,
|
this.state;
|
||||||
name,
|
|
||||||
description,
|
|
||||||
topic,
|
|
||||||
announcement,
|
|
||||||
t,
|
|
||||||
ro,
|
|
||||||
reactWhenReadOnly,
|
|
||||||
joinCode,
|
|
||||||
systemMessages,
|
|
||||||
encrypted,
|
|
||||||
avatar
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
sendLoadingEvent({ visible: true });
|
sendLoadingEvent({ visible: true });
|
||||||
let error = false;
|
let error = false;
|
||||||
|
@ -284,10 +263,6 @@ class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfo
|
||||||
if (getRoomTitle(room) !== name) {
|
if (getRoomTitle(room) !== name) {
|
||||||
params.roomName = name;
|
params.roomName = name;
|
||||||
}
|
}
|
||||||
// Avatar
|
|
||||||
if (!isEmpty(avatar)) {
|
|
||||||
params.roomAvatar = avatar.data as string;
|
|
||||||
}
|
|
||||||
// Description
|
// Description
|
||||||
if (room.description !== description) {
|
if (room.description !== description) {
|
||||||
params.roomDescription = description;
|
params.roomDescription = description;
|
||||||
|
@ -487,26 +462,10 @@ class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfo
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
changeAvatar = async () => {
|
handleEditAvatar = () => {
|
||||||
const options = {
|
const { navigation } = this.props;
|
||||||
cropping: true,
|
const { room } = this.state;
|
||||||
compressImageQuality: 0.8,
|
navigation.navigate('ChangeAvatarView', { titleHeader: I18n.t('Room_Info'), room, t: room.t, context: 'room' });
|
||||||
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 } });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
toggleRoomType = (value: boolean) => {
|
toggleRoomType = (value: boolean) => {
|
||||||
|
@ -549,8 +508,7 @@ class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfo
|
||||||
permissions,
|
permissions,
|
||||||
archived,
|
archived,
|
||||||
enableSysMes,
|
enableSysMes,
|
||||||
encrypted,
|
encrypted
|
||||||
avatar
|
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const { serverVersion, encryptionEnabled, theme } = this.props;
|
const { serverVersion, encryptionEnabled, theme } = this.props;
|
||||||
const { dangerColor } = themes[theme];
|
const { dangerColor } = themes[theme];
|
||||||
|
@ -568,29 +526,9 @@ class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfo
|
||||||
testID='room-info-edit-view-list'
|
testID='room-info-edit-view-list'
|
||||||
{...scrollPersistTaps}
|
{...scrollPersistTaps}
|
||||||
>
|
>
|
||||||
<TouchableOpacity
|
<View style={styles.avatarContainer}>
|
||||||
style={styles.avatarContainer}
|
<AvatarWithEdit type={room.t} text={room.name} rid={room.rid} handleEdit={this.handleEditAvatar} />
|
||||||
onPress={this.changeAvatar}
|
</View>
|
||||||
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>
|
|
||||||
<FormTextInput
|
<FormTextInput
|
||||||
inputRef={e => {
|
inputRef={e => {
|
||||||
this.name = e;
|
this.name = e;
|
||||||
|
|
|
@ -76,7 +76,8 @@ export default StyleSheet.create({
|
||||||
avatarContainer: {
|
avatarContainer: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
marginBottom: 10
|
marginBottom: 32,
|
||||||
|
marginTop: 16
|
||||||
},
|
},
|
||||||
resetButton: {
|
resetButton: {
|
||||||
padding: 4,
|
padding: 4,
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { connect } from 'react-redux';
|
||||||
import { Observable, Subscription } from 'rxjs';
|
import { Observable, Subscription } from 'rxjs';
|
||||||
import UAParser from 'ua-parser-js';
|
import UAParser from 'ua-parser-js';
|
||||||
|
|
||||||
import Avatar from '../../containers/Avatar';
|
import { AvatarWithEdit } from '../../containers/Avatar';
|
||||||
import { CustomIcon, TIconsName } from '../../containers/CustomIcon';
|
import { CustomIcon, TIconsName } from '../../containers/CustomIcon';
|
||||||
import * as HeaderButton from '../../containers/HeaderButton';
|
import * as HeaderButton from '../../containers/HeaderButton';
|
||||||
import { MarkdownPreview } from '../../containers/markdown';
|
import { MarkdownPreview } from '../../containers/markdown';
|
||||||
|
@ -394,17 +394,32 @@ class RoomInfoView extends React.Component<IRoomInfoViewProps, IRoomInfoViewStat
|
||||||
log(e);
|
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) => {
|
renderAvatar = (room: ISubscription, roomUser: IUserParsed) => {
|
||||||
const { theme } = this.props;
|
const { theme } = this.props;
|
||||||
|
const { showEdit } = this.state;
|
||||||
|
const showAvatarEdit = showEdit && this.t !== SubscriptionType.OMNICHANNEL;
|
||||||
|
|
||||||
return (
|
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 ? (
|
{this.t === SubscriptionType.DIRECT && roomUser._id ? (
|
||||||
<View style={[sharedStyles.status, { backgroundColor: themes[theme].auxiliaryBackground }]}>
|
<View style={[sharedStyles.status, { backgroundColor: themes[theme].auxiliaryBackground }]}>
|
||||||
<Status size={20} id={roomUser._id} />
|
<Status size={20} id={roomUser._id} />
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
</Avatar>
|
</AvatarWithEdit>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -21,13 +21,14 @@ export default StyleSheet.create({
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
paddingVertical: 8
|
paddingBottom: 8,
|
||||||
|
paddingTop: 32
|
||||||
},
|
},
|
||||||
avatar: {
|
avatar: {
|
||||||
marginHorizontal: 10
|
marginHorizontal: 10
|
||||||
},
|
},
|
||||||
roomTitleContainer: {
|
roomTitleContainer: {
|
||||||
paddingTop: 20,
|
paddingTop: 32,
|
||||||
marginHorizontal: 16,
|
marginHorizontal: 16,
|
||||||
alignItems: 'center'
|
alignItems: 'center'
|
||||||
},
|
},
|
||||||
|
|
|
@ -24,6 +24,7 @@ const styles = StyleSheet.create({
|
||||||
});
|
});
|
||||||
|
|
||||||
interface ILeftButtonsProps {
|
interface ILeftButtonsProps {
|
||||||
|
rid?: string;
|
||||||
tmid?: string;
|
tmid?: string;
|
||||||
unreadsCount: number | null;
|
unreadsCount: number | null;
|
||||||
navigation: StackNavigationProp<ChatsStackParamList, 'RoomView'>;
|
navigation: StackNavigationProp<ChatsStackParamList, 'RoomView'>;
|
||||||
|
@ -38,6 +39,7 @@ interface ILeftButtonsProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const LeftButtons = ({
|
const LeftButtons = ({
|
||||||
|
rid,
|
||||||
tmid,
|
tmid,
|
||||||
unreadsCount,
|
unreadsCount,
|
||||||
navigation,
|
navigation,
|
||||||
|
@ -78,7 +80,7 @@ const LeftButtons = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (baseUrl && userId && token) {
|
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;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
|
@ -589,6 +589,7 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
|
||||||
headerRightContainerStyle: { flexGrow: undefined, flexBasis: undefined },
|
headerRightContainerStyle: { flexGrow: undefined, flexBasis: undefined },
|
||||||
headerLeft: () => (
|
headerLeft: () => (
|
||||||
<LeftButtons
|
<LeftButtons
|
||||||
|
rid={rid}
|
||||||
tmid={tmid}
|
tmid={tmid}
|
||||||
unreadsCount={unreadsCount}
|
unreadsCount={unreadsCount}
|
||||||
navigation={navigation}
|
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
|
- Ask Diego Mello for credentials
|
||||||
|
|
||||||
## Shared config
|
## Shared config
|
||||||
- Change `.env` to `RUNNING_E2E_TESTS=true`
|
- To start the Metro bundler in the mocked mode, you should run `yarn e2e:start`
|
||||||
- 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
|
|
||||||
|
|
||||||
## Setup and run iOS
|
## 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)}`);
|
console.log(`POST /${endpoint} ${JSON.stringify(body)}`);
|
||||||
return rocketchat.post(endpoint, 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', () => {
|
describe('Profile screen', () => {
|
||||||
let scrollViewType: string;
|
|
||||||
let textMatcher: TTextMatcher;
|
let textMatcher: TTextMatcher;
|
||||||
let user: ITestUser;
|
let user: ITestUser;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
user = await createRandomUser();
|
user = await createRandomUser();
|
||||||
await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
|
await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
|
||||||
({ scrollViewType, textMatcher } = platformTypes[device.getPlatform()]);
|
({ textMatcher } = platformTypes[device.getPlatform()]);
|
||||||
await navigateToLogin();
|
await navigateToLogin();
|
||||||
await login(user.username, user.password);
|
await login(user.username, user.password);
|
||||||
await element(by.id('rooms-list-view-sidebar')).tap();
|
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();
|
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 () => {
|
it('should have name', async () => {
|
||||||
await expect(element(by.id('profile-view-name'))).toExist();
|
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();
|
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 () => {
|
it('should have submit button', async () => {
|
||||||
await waitFor(element(by.id('profile-view-submit')))
|
await waitFor(element(by.id('profile-view-submit')))
|
||||||
.toExist()
|
.toExist()
|
||||||
|
@ -96,17 +74,23 @@ describe('Profile screen', () => {
|
||||||
it('should change name and username', async () => {
|
it('should change name and username', async () => {
|
||||||
await element(by.id('profile-view-name')).replaceText(`${user.username}new`);
|
await element(by.id('profile-view-name')).replaceText(`${user.username}new`);
|
||||||
await element(by.id('profile-view-username')).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-list')).swipe('down');
|
||||||
await element(by.id('profile-view-submit')).tap();
|
await element(by.id('profile-view-submit')).tap();
|
||||||
await waitForToast();
|
await waitForToast();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should change email and password', async () => {
|
it('should change email and password', async () => {
|
||||||
|
await element(by.id('profile-view-list')).swipe('up');
|
||||||
await waitFor(element(by.id('profile-view-email')))
|
await waitFor(element(by.id('profile-view-email')))
|
||||||
.toBeVisible()
|
.toBeVisible()
|
||||||
.withTimeout(2000);
|
.withTimeout(2000);
|
||||||
await element(by.id('profile-view-email')).replaceText(`mobile+profileChangesNew${random()}@rocket.chat`);
|
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`);
|
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')))
|
await waitFor(element(by.id('profile-view-submit')))
|
||||||
.toExist()
|
.toExist()
|
||||||
.withTimeout(2000);
|
.withTimeout(2000);
|
||||||
|
@ -120,11 +104,5 @@ describe('Profile screen', () => {
|
||||||
.tap();
|
.tap();
|
||||||
await waitForToast();
|
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) {
|
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() {
|
async function waitForToast() {
|
||||||
|
@ -185,6 +185,8 @@ describe('Room info screen', () => {
|
||||||
await sleep(5000); // wait for changes to be applied from socket
|
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-description')).replaceText('new description');
|
||||||
await element(by.id('room-info-edit-view-topic')).replaceText('new topic');
|
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')).replaceText('new announcement');
|
||||||
await element(by.id('room-info-edit-view-announcement')).tapReturnKey();
|
await element(by.id('room-info-edit-view-announcement')).tapReturnKey();
|
||||||
await element(by.id('room-info-edit-view-password')).tapReturnKey();
|
await element(by.id('room-info-edit-view-password')).tapReturnKey();
|
||||||
|
@ -243,6 +245,7 @@ describe('Room info screen', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should delete room', async () => {
|
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 element(by.id('room-info-edit-view-delete')).tap();
|
||||||
await waitFor(element(by[textMatcher]('Yes, delete it!')))
|
await waitFor(element(by[textMatcher]('Yes, delete it!')))
|
||||||
.toExist()
|
.toExist()
|
||||||
|
|
|
@ -85,7 +85,7 @@ describe('Create team screen', () => {
|
||||||
await waitFor(element(by.id('room-info-edit-view-list')))
|
await waitFor(element(by.id('room-info-edit-view-list')))
|
||||||
.toBeVisible()
|
.toBeVisible()
|
||||||
.withTimeout(2000);
|
.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')))
|
await waitFor(element(by.id('room-info-edit-view-delete')))
|
||||||
.toBeVisible()
|
.toBeVisible()
|
||||||
.withTimeout(2000);
|
.withTimeout(2000);
|
||||||
|
|
|
@ -8,6 +8,8 @@
|
||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
const blocklist = require('metro-config/src/defaults/exclusionList');
|
const blocklist = require('metro-config/src/defaults/exclusionList');
|
||||||
|
|
||||||
|
const defaultSourceExts = require('metro-config/src/defaults/defaults').sourceExts;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
transformer: {
|
transformer: {
|
||||||
getTransformOptions: () => ({
|
getTransformOptions: () => ({
|
||||||
|
@ -20,6 +22,7 @@ module.exports = {
|
||||||
maxWorkers: 2,
|
maxWorkers: 2,
|
||||||
resolver: {
|
resolver: {
|
||||||
blocklistRE: blocklist([/ios\/Pods\/JitsiMeetSDK\/Frameworks\/JitsiMeet.framework\/assets\/node_modules\/react-native\/.*/]),
|
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-build-debug": "yarn detox build -c ios.sim.debug",
|
||||||
"e2e:ios-test-debug": "yarn detox test -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-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": {
|
"lint-staged": {
|
||||||
"*.{js,ts,tsx}": [
|
"*.{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:
|
react-native-image-crop-picker@RocketChat/react-native-image-crop-picker:
|
||||||
version "0.36.3"
|
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:
|
react-native-image-progress@^1.1.1:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
|
|
Loading…
Reference in New Issue