Compare commits

...

51 Commits

Author SHA1 Message Date
Reinaldo Neto 0fde16159d Merge branch 'develop' into new.change-avatar-view 2023-02-14 18:34:25 -03:00
Reinaldo Neto e95102d38e fix the flicker when upload an image 2023-02-14 18:29:20 -03:00
Reinaldo Neto b2b2989e46 fix the visual bug when the user change the avatar to new then clear cache 2023-02-07 13:47:06 -03:00
Gleidson Daniel Silva de9551c78a
Merge branch 'develop' into new.change-avatar-view 2023-02-03 09:53:26 -03:00
Reinaldo Neto 159d527796
Merge branch 'develop' into new.change-avatar-view 2023-01-19 18:02:23 -03:00
Reinaldo Neto a5da4999e1 minor tweaks 2023-01-18 17:11:25 -03:00
Reinaldo Neto 8903c52ddb minor refactor 2023-01-17 13:02:24 -03:00
Reinaldo Neto ed92eb80f3 fixed the layout 2023-01-16 23:12:09 -03:00
Reinaldo Neto dfcea278de Refactor all the changeAvatarView and fix how to test the image url 2023-01-16 21:35:55 -03:00
Reinaldo Neto 6d22747707 minor tweak 2023-01-14 00:09:16 -03:00
Reinaldo Neto 1255f4ac3b interface avatar container 2023-01-12 16:13:35 -03:00
Reinaldo Neto e8672ce827 minor tweak 2023-01-11 14:18:57 -03:00
Reinaldo Neto 0393aecbdd minor tweak 2023-01-11 14:00:23 -03:00
Reinaldo Neto 3db96db70c minor tweak with handle error 2023-01-11 12:43:05 -03:00
Reinaldo Neto 2dc8a0c355 tweak on yup validation 2023-01-10 21:11:04 -03:00
Reinaldo Neto af2b2a6185 refactor avatar url to use hook form 2023-01-10 20:45:33 -03:00
Reinaldo Neto dee200ba3f minor tweak changeavatarview 2023-01-10 17:20:33 -03:00
Reinaldo Neto 759504e46f fix avatar on header for tablets 2023-01-10 16:57:51 -03:00
Reinaldo Neto fe35747b52 types for change avatar view context 2023-01-10 00:38:37 -03:00
Reinaldo Neto 0ea494e3e7 avatar suggestion item 2023-01-09 23:39:59 -03:00
Reinaldo Neto 65dc56ad2d back handleError to views and refactor the submit 2023-01-09 18:01:44 -03:00
Reinaldo Neto ea20167981 minor tweak tests 2023-01-06 19:34:57 -03:00
Reinaldo Neto e8aab76148 fix the pt-br translation 2023-01-06 19:31:27 -03:00
Reinaldo Neto 22230e0584 refactor useAvatarETag 2023-01-06 19:20:45 -03:00
Reinaldo Neto 9c073fa6dc fix the edit button 2023-01-06 18:52:48 -03:00
Reinaldo Neto 80c358838f refactor avatar component 2023-01-06 17:51:16 -03:00
Reinaldo Neto d46c86778c refactor image.ts and test 2023-01-05 15:28:11 -03:00
Reinaldo Neto ac8e30387f fix lint 2022-12-21 19:49:16 -03:00
Reinaldo Neto 2a4195c10a created the useHooke useAvatarETag 2022-12-21 19:41:22 -03:00
Reinaldo Neto 7e1dec41e9 removed the avatarETagUser and search by username and text 2022-12-21 19:22:22 -03:00
Reinaldo Neto 34993dace2 fixing pt-br 2022-12-20 15:20:06 -03:00
Reinaldo Neto 1e29a8efac tweak IAvatarContainer 2022-12-20 15:13:31 -03:00
Reinaldo Neto 744565ad21 minor tweaks 2022-12-20 13:40:53 -03:00
Reinaldo Neto 0ce1fdcbf2 Merge branch 'new.change-avatar-view' of https://github.com/RocketChat/Rocket.Chat.ReactNative into new.change-avatar-view 2022-12-15 10:43:58 -03:00
Reinaldo Neto 2d8751c4bb minor tweak in edit button 2022-12-15 10:37:30 -03:00
Reinaldo Neto 0cb7e3020a
Merge branch 'develop' into new.change-avatar-view 2022-12-15 09:57:35 -03:00
Reinaldo Neto a2dfbcbe30 minor tweak with themes and buttons 2022-12-15 09:51:26 -03:00
Reinaldo Neto 84d0401e3c refactor the e2e test 2022-12-14 19:09:14 -03:00
Reinaldo Neto 84b16b2d97 refactor the submit function 2022-12-14 14:15:16 -03:00
Reinaldo Neto 4651a2fb91 ipad navigation 2022-12-13 22:42:10 -03:00
Reinaldo Neto a7a4d9bb00 room info edit view 2022-12-13 14:18:31 -03:00
Reinaldo Neto e48b174118 fix the delete from rooms and finished RoomInfoView, missing RoomInfoEditView 2022-12-13 12:54:47 -03:00
Reinaldo Neto 13f5075f7c refactor avatar suggestion 2022-12-12 18:13:12 -03:00
Reinaldo Neto a478f1ff52 fix the profile update through all the app 2022-12-12 14:47:12 -03:00
Reinaldo Neto a283f41022 Finished the profile and fixed the avatar when change the user profile 2022-12-09 18:29:16 -03:00
Reinaldo Neto 90984de444 clean profile view 2022-12-08 23:00:02 -03:00
Reinaldo Neto c4f09d8b7a pick image 2022-12-08 22:27:30 -03:00
Reinaldo Neto 7813efbb26 change avatar for profile it's done, missing fix revalidate the avatar in profile and drawer 2022-12-07 20:17:08 -03:00
Reinaldo Neto 5e1f1c89da avatar Url 2022-12-07 18:02:16 -03:00
Reinaldo Neto cc9a9d523d change avatar view and avatar suggestion 2022-12-07 02:01:00 -03:00
Reinaldo Neto 49f2c28b3e [NEW] Change Avatar View 2022-12-07 00:11:25 -03:00
37 changed files with 785 additions and 330 deletions

View File

@ -1,13 +1,11 @@
import { Q } from '@nozbe/watermelondb';
import React, { useEffect, useRef, useState } from 'react';
import React from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import { Observable, Subscription } from 'rxjs';
import { IApplicationState, TSubscriptionModel, TUserModel } from '../../definitions';
import database from '../../lib/database';
import { IApplicationState } from '../../definitions';
import { getUserSelector } from '../../selectors/login';
import Avatar from './Avatar';
import { IAvatar } from './interfaces';
import { useAvatarETag } from './useAvatarETag';
const AvatarContainer = ({
style,
@ -23,17 +21,13 @@ const AvatarContainer = ({
isStatic,
rid
}: IAvatar): React.ReactElement => {
const subscription = useRef<Subscription>();
const [avatarETag, setAvatarETag] = useState<string | undefined>('');
const isDirect = () => type === 'd';
const server = useSelector((state: IApplicationState) => state.share.server.server || state.server.server);
const serverVersion = useSelector((state: IApplicationState) => state.share.server.version || state.server.version);
const { id, token } = useSelector(
const { id, token, username } = useSelector(
(state: IApplicationState) => ({
id: getUserSelector(state).id,
token: getUserSelector(state).token
token: getUserSelector(state).token,
username: getUserSelector(state).username
}),
shallowEqual
);
@ -48,41 +42,7 @@ const AvatarContainer = ({
true
);
const init = async () => {
const db = database.active;
const usersCollection = db.get('users');
const subsCollection = db.get('subscriptions');
let record;
try {
if (isDirect()) {
const [user] = await usersCollection.query(Q.where('username', text)).fetch();
record = user;
} else if (rid) {
record = await subsCollection.find(rid);
}
} catch {
// Record not found
}
if (record) {
const observable = record.observe() as Observable<TSubscriptionModel | TUserModel>;
subscription.current = observable.subscribe(r => {
setAvatarETag(r.avatarETag);
});
}
};
useEffect(() => {
if (!avatarETag) {
init();
}
return () => {
if (subscription?.current?.unsubscribe) {
subscription.current.unsubscribe();
}
};
}, [text, type, size, avatarETag, externalProviderUrl]);
const { avatarETag } = useAvatarETag({ username, text, type, rid, id });
return (
<Avatar

View File

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

View File

@ -0,0 +1,5 @@
import Avatar from './AvatarContainer';
export { default as AvatarWithEdit } from './AvatarWithEdit';
export default Avatar;

View File

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

View File

@ -14,7 +14,7 @@ interface IButtonProps extends PlatformTouchableProps {
loading?: boolean;
color?: string;
fontSize?: number;
styleText?: StyleProp<TextStyle>[];
styleText?: StyleProp<TextStyle> | StyleProp<TextStyle>[];
}
const styles = StyleSheet.create({

View File

@ -17,16 +17,14 @@ export interface IAvatarButton {
}
export interface IAvatar {
data: {} | string | null;
data: string | null;
url?: string;
contentType?: string;
service?: any;
}
export interface IAvatarSuggestion {
[service: string]: {
url: string;
blob: string;
contentType: string;
};
url: string;
blob: string;
contentType: string;
}

View File

@ -0,0 +1 @@
export type TChangeAvatarViewContext = 'profile' | 'room';

View File

@ -877,6 +877,14 @@
"Reply_in_direct_message": "Reply in Direct Message",
"room_archived": "archived room",
"room_unarchived": "unarchived room",
"Upload_image": "Upload image",
"Delete_image": "Delete image",
"Images_uploaded": "Images uploaded",
"Avatar": "Avatar",
"insert_Avatar_URL": "insert image URL here",
"Discard_changes":"Discard changes?",
"Discard":"Discard",
"Discard_changes_description":"All changes will be lost if you go back without saving.",
"Presence_Cap_Warning_Title": "User status temporarily disabled",
"Presence_Cap_Warning_Description": "Active connections have reached the limit for the workspace, thus the service that handles user status is disabled. It can be re-enabled manually in workspace settings.",
"Learn_more": "Learn more"

View File

@ -876,6 +876,14 @@
"Reply_in_direct_message": "Responder por mensagem direta",
"room_archived": "{{username}} arquivou a sala",
"room_unarchived": "{{username}} desarquivou a sala",
"Upload_image": "Carregar imagem",
"Delete_image": "Deletar imagem",
"Images_uploaded": "Imagens carregadas",
"Avatar": "Avatar",
"insert_Avatar_URL": "insira o URL da imagem aqui",
"Discard_changes":"Descartar alterações?",
"Discard":"Descartar",
"Discard_changes_description":"Todas as alterações serão perdidas, se você sair sem salvar.",
"Presence_Cap_Warning_Title": "Status do usuário desabilitado temporariamente",
"Presence_Cap_Warning_Description": "O limite de conexões ativas para a workspace foi atingido, por isso o serviço responsável pela presença dos usuários está temporariamente desabilitado. Ele pode ser reabilitado manualmente nas configurações da workspace."
}

View File

@ -59,6 +59,7 @@ export const colors = {
buttonText: '#ffffff',
passcodeBackground: '#EEEFF1',
passcodeButtonActive: '#E4E7EA',
editAndUploadButtonAvatar: '#E4E7EA',
passcodeLockIcon: '#6C727A',
passcodePrimary: '#2F343D',
passcodeSecondary: '#6C727A',
@ -128,6 +129,7 @@ export const colors = {
buttonText: '#ffffff',
passcodeBackground: '#030C1B',
passcodeButtonActive: '#0B182C',
editAndUploadButtonAvatar: '#0B182C',
passcodeLockIcon: '#6C727A',
passcodePrimary: '#FFFFFF',
passcodeSecondary: '#CBCED1',
@ -197,6 +199,7 @@ export const colors = {
buttonText: '#ffffff',
passcodeBackground: '#000000',
passcodeButtonActive: '#0E0D0D',
editAndUploadButtonAvatar: '#0E0D0D',
passcodeLockIcon: '#6C727A',
passcodePrimary: '#FFFFFF',
passcodeSecondary: '#CBCED1',

View File

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

View File

@ -14,3 +14,4 @@ export * from './server';
export * from './url';
export * from './isValidEmail';
export * from './random';
export * from './image';

View File

@ -291,7 +291,7 @@ export default function subscribeRooms() {
const [type, data] = ddpMessage.fields.args;
const [, ev] = ddpMessage.fields.eventName.split('/');
if (/userData/.test(ev)) {
const [{ diff }] = ddpMessage.fields.args;
const [{ diff, unset }] = ddpMessage.fields.args;
if (diff?.statusLivechat) {
store.dispatch(setUser({ statusLivechat: diff.statusLivechat }));
}
@ -301,6 +301,12 @@ export default function subscribeRooms() {
if ((['settings.preferences.alsoSendThreadToChannel'] as any) in diff) {
store.dispatch(setUser({ alsoSendThreadToChannel: diff['settings.preferences.alsoSendThreadToChannel'] }));
}
if (diff?.avatarETag) {
store.dispatch(setUser({ avatarETag: diff.avatarETag }));
}
if (unset?.avatarETag) {
store.dispatch(setUser({ avatarETag: '' }));
}
}
if (/subscriptions/.test(ev)) {
if (type === 'removed') {

View File

@ -561,7 +561,7 @@ export const saveRoomSettings = (
rid: string,
params: {
roomName?: string;
roomAvatar?: string;
roomAvatar?: string | null;
roomDescription?: string;
roomTopic?: string;
roomAnnouncement?: string;
@ -602,7 +602,7 @@ export const getRoomRoles = (
// RC 0.65.0
sdk.get(`${roomTypeToApiType(type)}.roles`, { roomId });
export const getAvatarSuggestion = (): Promise<IAvatarSuggestion> =>
export const getAvatarSuggestion = (): Promise<{ [service: string]: IAvatarSuggestion }> =>
// RC 0.51.0
sdk.methodCallWrapper('getAvatarSuggestion');

View File

@ -247,6 +247,22 @@ const handleLogout = function* handleLogout({ forcedByServer, message }) {
};
const handleSetUser = function* handleSetUser({ user }) {
if ('avatarETag' in user) {
const userId = yield select(state => state.login.user.id);
const serversDB = database.servers;
const userCollections = serversDB.get('users');
yield serversDB.write(async () => {
try {
const userRecord = await userCollections.find(userId);
await userRecord.update(record => {
record.avatarETag = user.avatarETag;
});
} catch {
//
}
});
}
setLanguage(user?.language);
if (user?.statusLivechat && isOmnichannelModuleAvailable()) {

View File

@ -68,6 +68,7 @@ import AddChannelTeamView from '../views/AddChannelTeamView';
import AddExistingChannelView from '../views/AddExistingChannelView';
import SelectListView from '../views/SelectListView';
import DiscussionsView from '../views/DiscussionsView';
import ChangeAvatarView from '../views/ChangeAvatarView';
import {
AdminPanelStackParamList,
ChatsStackParamList,
@ -96,6 +97,7 @@ const ChatsStackNavigator = () => {
<ChatsStack.Screen name='SelectListView' component={SelectListView} options={SelectListView.navigationOptions} />
<ChatsStack.Screen name='RoomInfoView' component={RoomInfoView} options={RoomInfoView.navigationOptions} />
<ChatsStack.Screen name='RoomInfoEditView' component={RoomInfoEditView} options={RoomInfoEditView.navigationOptions} />
<ChatsStack.Screen name='ChangeAvatarView' component={ChangeAvatarView} />
<ChatsStack.Screen name='RoomMembersView' component={RoomMembersView} />
<ChatsStack.Screen name='DiscussionsView' component={DiscussionsView} />
<ChatsStack.Screen
@ -151,6 +153,7 @@ const ProfileStackNavigator = () => {
>
<ProfileStack.Screen name='ProfileView' component={ProfileView} options={ProfileView.navigationOptions} />
<ProfileStack.Screen name='UserPreferencesView' component={UserPreferencesView} />
<ProfileStack.Screen name='ChangeAvatarView' component={ChangeAvatarView} />
<ProfileStack.Screen name='UserNotificationPrefView' component={UserNotificationPrefView} />
<ProfileStack.Screen name='PickerView' component={PickerView} options={PickerView.navigationOptions} />
</ProfileStack.Navigator>

View File

@ -17,6 +17,7 @@ import RoomsListView from '../../views/RoomsListView';
import RoomActionsView from '../../views/RoomActionsView';
import RoomInfoView from '../../views/RoomInfoView';
import RoomInfoEditView from '../../views/RoomInfoEditView';
import ChangeAvatarView from '../../views/ChangeAvatarView';
import RoomMembersView from '../../views/RoomMembersView';
import SearchMessagesView from '../../views/SearchMessagesView';
import SelectedUsersView from '../../views/SelectedUsersView';
@ -128,6 +129,7 @@ const ModalStackNavigator = React.memo(({ navigation }: INavigation) => {
<ModalStack.Screen name='RoomInfoView' component={RoomInfoView} options={RoomInfoView.navigationOptions} />
<ModalStack.Screen name='SelectListView' component={SelectListView} />
<ModalStack.Screen name='RoomInfoEditView' component={RoomInfoEditView} options={RoomInfoEditView.navigationOptions} />
<ModalStack.Screen name='ChangeAvatarView' component={ChangeAvatarView} />
<ModalStack.Screen name='RoomMembersView' component={RoomMembersView} />
<ModalStack.Screen
name='SearchMessagesView'

View File

@ -6,6 +6,7 @@ import { IMessage } from '../../definitions/IMessage';
import { ISubscription, SubscriptionType, TSubscriptionModel } from '../../definitions/ISubscription';
import { ILivechatDepartment } from '../../definitions/ILivechatDepartment';
import { ILivechatTag } from '../../definitions/ILivechatTag';
import { TChangeAvatarViewContext } from '../../definitions/TChangeAvatarViewContext';
export type MasterDetailChatsStackParamList = {
RoomView: {
@ -58,6 +59,12 @@ export type ModalStackParamList = {
onSearch?: Function;
isRadio?: boolean;
};
ChangeAvatarView: {
context: TChangeAvatarViewContext;
titleHeader?: string;
room?: ISubscription;
t?: SubscriptionType;
};
RoomInfoEditView: {
rid: string;
};

View File

@ -13,6 +13,7 @@ import { ModalStackParamList } from './MasterDetailStack/types';
import { TThreadModel } from '../definitions';
import { ILivechatDepartment } from '../definitions/ILivechatDepartment';
import { ILivechatTag } from '../definitions/ILivechatTag';
import { TChangeAvatarViewContext } from '../definitions/TChangeAvatarViewContext';
export type ChatsStackParamList = {
ModalStackNavigator: NavigatorScreenParams<ModalStackParamList>;
@ -181,6 +182,12 @@ export type ChatsStackParamList = {
onlyAudio?: boolean;
videoConf?: boolean;
};
ChangeAvatarView: {
context: TChangeAvatarViewContext;
titleHeader?: string;
room?: ISubscription;
t?: SubscriptionType;
};
};
export type ProfileStackParamList = {
@ -195,6 +202,12 @@ export type ProfileStackParamList = {
goBack?: Function;
onChangeValue: Function;
};
ChangeAvatarView: {
context: TChangeAvatarViewContext;
titleHeader?: string;
room?: ISubscription;
t?: SubscriptionType;
};
};
export type SettingsStackParamList = {

View File

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

View File

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

View File

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

View File

@ -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 ImagePicker, { Image } from 'react-native-image-crop-picker';
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';
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-logout-other-locations'
/>
{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;

View File

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

View File

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

View File

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

View File

@ -2,7 +2,6 @@ import React from 'react';
import { Keyboard, ScrollView, TextInput, View } from 'react-native';
import { connect } from 'react-redux';
import { sha256 } from 'js-sha256';
import ImagePicker, { Image } from 'react-native-image-crop-picker';
import RNPickerSelect from 'react-native-picker-select';
import { dequal } from 'dequal';
import omit from 'lodash/omit';
@ -12,16 +11,15 @@ import Touch from '../../containers/Touch';
import KeyboardView from '../../containers/KeyboardView';
import sharedStyles from '../Styles';
import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps';
import { showConfirmationAlert, showErrorAlert } from '../../lib/methods/helpers';
import { showErrorAlert, showConfirmationAlert } from '../../lib/methods/helpers';
import { LISTENER } from '../../containers/Toast';
import EventEmitter from '../../lib/methods/helpers/events';
import { FormTextInput } from '../../containers/TextInput';
import log, { events, logEvent } from '../../lib/methods/helpers/log';
import { events, logEvent } from '../../lib/methods/helpers/log';
import I18n from '../../i18n';
import Button from '../../containers/Button';
import Avatar from '../../containers/Avatar';
import { AvatarWithEdit } from '../../containers/Avatar';
import { setUser } from '../../actions/login';
import { CustomIcon } from '../../containers/CustomIcon';
import * as HeaderButton from '../../containers/HeaderButton';
import StatusBar from '../../containers/StatusBar';
import { themes } from '../../lib/constants';
@ -31,15 +29,7 @@ import SafeAreaView from '../../containers/SafeAreaView';
import styles from './styles';
import { ProfileStackParamList } from '../../stacks/types';
import { Services } from '../../lib/services';
import {
IApplicationState,
IAvatar,
IAvatarButton,
IAvatarSuggestion,
IBaseScreen,
IProfileParams,
IUser
} from '../../definitions';
import { IApplicationState, IAvatarButton, IBaseScreen, IProfileParams, IUser } from '../../definitions';
import { twoFactor } from '../../lib/services/twoFactor';
import { TwoFactorMethods } from '../../definitions/ITotp';
import { withActionSheet, IActionSheetProvider } from '../../containers/ActionSheet';
@ -67,9 +57,6 @@ interface IProfileViewState {
email: string | null;
newPassword: string | null;
currentPassword: string | null;
avatarUrl: string | null;
avatar: IAvatar;
avatarSuggestions: IAvatarSuggestion;
customFields: {
[key: string | number]: string;
};
@ -113,25 +100,12 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
email: '',
newPassword: '',
currentPassword: '',
avatarUrl: '',
avatar: {
data: {},
url: ''
},
avatarSuggestions: {},
customFields: {},
twoFactorCode: null
};
async componentDidMount() {
componentDidMount() {
this.init();
try {
const result = await Services.getAvatarSuggestion();
this.setState({ avatarSuggestions: result });
} catch (e) {
log(e);
}
}
UNSAFE_componentWillReceiveProps(nextProps: IProfileViewProps) {
@ -147,16 +121,6 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
}
}
setAvatar = (avatar: IAvatar) => {
const { Accounts_AllowUserAvatarChange } = this.props;
if (!Accounts_AllowUserAvatarChange) {
return;
}
this.setState({ avatar });
};
init = (user?: IUser) => {
const { user: userProps } = this.props;
const { name, username, emails, customFields } = user || userProps;
@ -167,17 +131,12 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
email: emails ? emails[0].address : null,
newPassword: null,
currentPassword: null,
avatarUrl: null,
avatar: {
data: {},
url: ''
},
customFields: customFields || {}
});
};
formIsChanged = () => {
const { name, username, email, newPassword, avatar, customFields } = this.state;
const { name, username, email, newPassword, customFields } = this.state;
const { user } = this.props;
let customFieldsChanged = false;
@ -196,21 +155,10 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
!newPassword &&
user.emails &&
user.emails[0].address === email &&
!avatar.data &&
!customFieldsChanged
);
};
handleError = (e: any, _func: string, action: string) => {
if (e.data && e.data.error.includes('[error-too-many-requests]')) {
return showErrorAlert(e.data.error);
}
if (I18n.isTranslated(e.error)) {
return showErrorAlert(I18n.t(e.error));
}
showErrorAlert(I18n.t('There_was_an_error_while_action', { action: I18n.t(action) }));
};
submit = async (): Promise<void> => {
Keyboard.dismiss();
@ -220,7 +168,7 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
this.setState({ saving: true });
const { name, username, email, newPassword, currentPassword, avatar, customFields, twoFactorCode } = this.state;
const { name, username, email, newPassword, currentPassword, customFields, twoFactorCode } = this.state;
const { user, dispatch } = this.props;
const params = {} as IProfileParams;
@ -273,17 +221,6 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
}
try {
if (avatar.url) {
try {
logEvent(events.PROFILE_SAVE_AVATAR);
await Services.setAvatarFromService(avatar);
} catch (e) {
logEvent(events.PROFILE_SAVE_AVATAR_F);
this.setState({ saving: false, currentPassword: null });
return this.handleError(e, 'setAvatarFromService', 'changing_avatar');
}
}
const twoFactorOptions = params.currentPassword
? {
twoFactorCode: params.currentPassword,
@ -317,7 +254,7 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
}
logEvent(events.PROFILE_SAVE_CHANGES_F);
this.setState({ saving: false, currentPassword: null, twoFactorCode: null });
this.handleError(e, 'saveUserProfile', 'saving_profile');
this.handleError(e, 'saving_profile');
}
};
@ -334,39 +271,23 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
EventEmitter.emit(LISTENER, { message: I18n.t('Avatar_changed_successfully') });
this.init();
} catch (e) {
this.handleError(e, 'resetAvatar', 'changing_avatar');
this.handleError(e, 'changing_avatar');
}
};
pickImage = async () => {
const { Accounts_AllowUserAvatarChange } = this.props;
if (!Accounts_AllowUserAvatarChange) {
return;
handleError = (e: any, action: string) => {
if (e.data && e.data.error.includes('[error-too-many-requests]')) {
return showErrorAlert(e.data.error);
}
const options = {
cropping: true,
compressImageQuality: 0.8,
freeStyleCropEnabled: true,
cropperAvoidEmptySpaceAroundImage: false,
cropperChooseText: I18n.t('Choose'),
cropperCancelText: I18n.t('Cancel'),
includeBase64: true
};
try {
logEvent(events.PROFILE_PICK_AVATAR);
const response: Image = await ImagePicker.openPicker(options);
this.setAvatar({ url: response.path, data: `data:image/jpeg;base64,${response.data}`, service: 'upload' });
} catch (error) {
logEvent(events.PROFILE_PICK_AVATAR_F);
console.warn(error);
if (I18n.isTranslated(e.error)) {
return showErrorAlert(I18n.t(e.error));
}
showErrorAlert(I18n.t('There_was_an_error_while_action', { action: I18n.t(action) }));
};
pickImageWithURL = (avatarUrl: string) => {
logEvent(events.PROFILE_PICK_AVATAR_WITH_URL);
this.setAvatar({ url: avatarUrl, data: avatarUrl, service: 'url' });
handleEditAvatar = () => {
const { navigation } = this.props;
navigation.navigate('ChangeAvatarView', { context: 'profile' });
};
renderAvatarButton = ({ key, child, onPress, disabled = false }: IAvatarButton) => {
@ -384,49 +305,6 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
);
};
renderAvatarButtons = () => {
const { avatarUrl, avatarSuggestions } = this.state;
const { user, theme, Accounts_AllowUserAvatarChange } = this.props;
return (
<View style={styles.avatarButtons}>
{this.renderAvatarButton({
child: <Avatar text={`@${user.username}`} size={50} />,
onPress: () => this.resetAvatar(),
disabled: !Accounts_AllowUserAvatarChange,
key: 'profile-view-reset-avatar'
})}
{this.renderAvatarButton({
child: <CustomIcon name='upload' size={30} color={themes[theme].bodyText} />,
onPress: () => this.pickImage(),
disabled: !Accounts_AllowUserAvatarChange,
key: 'profile-view-upload-avatar'
})}
{this.renderAvatarButton({
child: <CustomIcon name='link' size={30} color={themes[theme].bodyText} />,
onPress: () => (avatarUrl ? this.pickImageWithURL(avatarUrl) : null),
disabled: !avatarUrl,
key: 'profile-view-avatar-url-button'
})}
{Object.keys(avatarSuggestions).map(service => {
const { url, blob, contentType } = avatarSuggestions[service];
return this.renderAvatarButton({
disabled: !Accounts_AllowUserAvatarChange,
key: `profile-view-avatar-${service}`,
child: <Avatar avatar={url} size={50} />,
onPress: () =>
this.setAvatar({
url,
data: blob,
service,
contentType
})
});
})}
</View>
);
};
renderCustomFields = () => {
const { customFields } = this.state;
const { Accounts_CustomFields } = this.props;
@ -520,7 +398,7 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
};
render() {
const { name, username, email, newPassword, avatarUrl, customFields, avatar, saving } = this.state;
const { name, username, email, newPassword, customFields, saving } = this.state;
const {
user,
theme,
@ -543,7 +421,10 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
<SafeAreaView testID='profile-view'>
<ScrollView contentContainerStyle={sharedStyles.containerScrollView} testID='profile-view-list' {...scrollPersistTaps}>
<View style={styles.avatarContainer} testID='profile-view-avatar'>
<Avatar text={user.username} avatar={avatar?.url} isStatic={avatar?.url} size={100} />
<AvatarWithEdit
text={user.username}
handleEdit={Accounts_AllowUserAvatarChange ? this.handleEditAvatar : undefined}
/>
</View>
<FormTextInput
editable={Accounts_AllowRealNameChange}
@ -615,22 +496,6 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
testID='profile-view-new-password'
/>
{this.renderCustomFields()}
<FormTextInput
editable={Accounts_AllowUserAvatarChange}
inputStyle={[!Accounts_AllowUserAvatarChange && styles.disabled]}
inputRef={e => {
if (e) {
this.avatarUrl = e;
}
}}
label={I18n.t('Avatar_Url')}
placeholder={I18n.t('Avatar_Url')}
value={avatarUrl || undefined}
onChangeText={value => this.setState({ avatarUrl: value })}
onSubmitEditing={this.submit}
testID='profile-view-avatar-url'
/>
{this.renderAvatarButtons()}
<Button
title={I18n.t('Save_Changes')}
type='primary'

View File

@ -7,7 +7,7 @@ export default StyleSheet.create({
avatarContainer: {
alignItems: 'center',
justifyContent: 'center',
marginBottom: 10
marginBottom: 24
},
avatarButtons: {
flexWrap: 'wrap',

View File

@ -2,15 +2,13 @@ import React from 'react';
import { Q } from '@nozbe/watermelondb';
import { BlockContext } from '@rocket.chat/ui-kit';
import { dequal } from 'dequal';
import isEmpty from 'lodash/isEmpty';
import { Alert, Keyboard, ScrollView, Text, TextInput, TouchableOpacity, View, StyleSheet } from 'react-native';
import ImagePicker, { Image } from 'react-native-image-crop-picker';
import { connect } from 'react-redux';
import { Subscription } from 'rxjs';
import { deleteRoom } from '../../actions/room';
import { themes } from '../../lib/constants';
import Avatar from '../../containers/Avatar';
import { AvatarWithEdit } from '../../containers/Avatar';
import { sendLoadingEvent } from '../../containers/Loading';
import SafeAreaView from '../../containers/SafeAreaView';
import StatusBar from '../../containers/StatusBar';
@ -23,13 +21,11 @@ import {
IRoomSettings,
ISubscription,
SubscriptionType,
TSubscriptionModel,
IAvatar
TSubscriptionModel
} from '../../definitions';
import { ERoomType } from '../../definitions/ERoomType';
import I18n from '../../i18n';
import database from '../../lib/database';
import { CustomIcon } from '../../containers/CustomIcon';
import KeyboardView from '../../containers/KeyboardView';
import { TSupportedPermissions } from '../../reducers/permissions';
import { ModalStackParamList } from '../../stacks/MasterDetailStack/types';
@ -54,7 +50,6 @@ import { Services } from '../../lib/services';
interface IRoomInfoEditViewState {
room: ISubscription;
avatar: IAvatar;
permissions: { [key in TSupportedPermissions]?: boolean };
name: string;
description?: string;
@ -102,7 +97,6 @@ class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfo
this.room = {} as TSubscriptionModel;
this.state = {
room: {} as ISubscription,
avatar: {} as IAvatar,
permissions: {},
name: '',
description: '',
@ -192,7 +186,6 @@ class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfo
topic,
announcement,
t: t === 'p',
avatar: {} as IAvatar,
ro,
reactWhenReadOnly,
joinCode: joinCodeRequired ? this.randomValue : '',
@ -228,8 +221,7 @@ class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfo
joinCode,
systemMessages,
enableSysMes,
encrypted,
avatar
encrypted
} = this.state;
const { joinCodeRequired } = room;
const sysMes = room.sysMes as string[];
@ -244,28 +236,15 @@ class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfo
room.reactWhenReadOnly === reactWhenReadOnly &&
dequal(sysMes, systemMessages) &&
enableSysMes === (sysMes && sysMes.length > 0) &&
room.encrypted === encrypted &&
isEmpty(avatar)
room.encrypted === encrypted
);
};
submit = async () => {
logEvent(events.RI_EDIT_SAVE);
Keyboard.dismiss();
const {
room,
name,
description,
topic,
announcement,
t,
ro,
reactWhenReadOnly,
joinCode,
systemMessages,
encrypted,
avatar
} = this.state;
const { room, name, description, topic, announcement, t, ro, reactWhenReadOnly, joinCode, systemMessages, encrypted } =
this.state;
sendLoadingEvent({ visible: true });
let error = false;
@ -284,10 +263,6 @@ class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfo
if (room.name !== name) {
params.roomName = name;
}
// Avatar
if (!isEmpty(avatar)) {
params.roomAvatar = avatar.data as string;
}
// Description
if (room.description !== description) {
params.roomDescription = description;
@ -487,26 +462,10 @@ class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfo
);
};
changeAvatar = async () => {
const options = {
cropping: true,
compressImageQuality: 0.8,
cropperAvoidEmptySpaceAroundImage: false,
cropperChooseText: I18n.t('Choose'),
cropperCancelText: I18n.t('Cancel'),
includeBase64: true
};
try {
const response: Image = await ImagePicker.openPicker(options);
this.setState({ avatar: { url: response.path, data: `data:image/jpeg;base64,${response.data}`, service: 'upload' } });
} catch (e) {
console.log(e);
}
};
resetAvatar = () => {
this.setState({ avatar: { data: null } });
handleEditAvatar = () => {
const { navigation } = this.props;
const { room } = this.state;
navigation.navigate('ChangeAvatarView', { titleHeader: I18n.t('Room_Info'), room, t: room.t, context: 'room' });
};
toggleRoomType = (value: boolean) => {
@ -549,8 +508,7 @@ class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfo
permissions,
archived,
enableSysMes,
encrypted,
avatar
encrypted
} = this.state;
const { serverVersion, encryptionEnabled, theme } = this.props;
const { dangerColor } = themes[theme];
@ -568,29 +526,9 @@ class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfo
testID='room-info-edit-view-list'
{...scrollPersistTaps}
>
<TouchableOpacity
style={styles.avatarContainer}
onPress={this.changeAvatar}
disabled={compareServerVersion(serverVersion || '', 'lowerThan', '3.6.0')}
>
<Avatar
type={room.t}
text={room.name}
avatar={avatar?.url}
isStatic={avatar?.url}
rid={isEmpty(avatar) ? room.rid : undefined}
size={100}
>
{serverVersion && compareServerVersion(serverVersion, 'lowerThan', '3.6.0') ? undefined : (
<TouchableOpacity
style={[styles.resetButton, { backgroundColor: themes[theme].dangerColor }]}
onPress={this.resetAvatar}
>
<CustomIcon name='delete' color={themes[theme].backgroundColor} size={24} />
</TouchableOpacity>
)}
</Avatar>
</TouchableOpacity>
<View style={styles.avatarContainer}>
<AvatarWithEdit type={room.t} text={room.name} rid={room.rid} handleEdit={this.handleEditAvatar} />
</View>
<FormTextInput
inputRef={e => {
this.name = e;

View File

@ -76,7 +76,8 @@ export default StyleSheet.create({
avatarContainer: {
alignItems: 'center',
justifyContent: 'center',
marginBottom: 10
marginBottom: 32,
marginTop: 16
},
resetButton: {
padding: 4,

View File

@ -10,7 +10,7 @@ import { Observable, Subscription } from 'rxjs';
import { CustomIcon, TIconsName } from '../../containers/CustomIcon';
import Status from '../../containers/Status';
import Avatar from '../../containers/Avatar';
import { AvatarWithEdit } from '../../containers/Avatar';
import sharedStyles from '../Styles';
import RoomTypeIcon from '../../containers/RoomTypeIcon';
import I18n from '../../i18n';
@ -399,17 +399,32 @@ class RoomInfoView extends React.Component<IRoomInfoViewProps, IRoomInfoViewStat
log(e);
}
};
handleEditAvatar = () => {
const { navigation } = this.props;
const { room } = this.state;
navigation.navigate('ChangeAvatarView', { titleHeader: I18n.t('Room_Info'), room, t: this.t, context: 'room' });
};
renderAvatar = (room: ISubscription, roomUser: IUserParsed) => {
const { theme } = this.props;
const { showEdit } = this.state;
const showAvatarEdit = showEdit && this.t !== SubscriptionType.OMNICHANNEL;
return (
<Avatar text={room.name || roomUser.username} style={styles.avatar} type={this.t} size={100} rid={room?.rid}>
<AvatarWithEdit
text={room.name || roomUser.username}
style={styles.avatar}
type={this.t}
rid={room?.rid}
handleEdit={showAvatarEdit ? this.handleEditAvatar : undefined}
>
{this.t === SubscriptionType.DIRECT && roomUser._id ? (
<View style={[sharedStyles.status, { backgroundColor: themes[theme].auxiliaryBackground }]}>
<Status size={20} id={roomUser._id} />
</View>
) : null}
</Avatar>
</AvatarWithEdit>
);
};

View File

@ -21,13 +21,14 @@ export default StyleSheet.create({
alignItems: 'center',
justifyContent: 'center',
marginBottom: 20,
paddingVertical: 8
paddingBottom: 8,
paddingTop: 32
},
avatar: {
marginHorizontal: 10
},
roomTitleContainer: {
paddingTop: 20,
paddingTop: 32,
marginHorizontal: 16,
alignItems: 'center'
},

View File

@ -24,6 +24,7 @@ const styles = StyleSheet.create({
});
interface ILeftButtonsProps {
rid?: string;
tmid?: string;
unreadsCount: number | null;
navigation: StackNavigationProp<ChatsStackParamList, 'RoomView'>;
@ -38,6 +39,7 @@ interface ILeftButtonsProps {
}
const LeftButtons = ({
rid,
tmid,
unreadsCount,
navigation,
@ -78,7 +80,7 @@ const LeftButtons = ({
}
if (baseUrl && userId && token) {
return <Avatar text={title} size={30} type={t} style={styles.avatar} onPress={onPress} />;
return <Avatar rid={rid} text={title} size={30} type={t} style={styles.avatar} onPress={onPress} />;
}
return null;
};

View File

@ -589,6 +589,7 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
headerRightContainerStyle: { flexGrow: undefined, flexBasis: undefined },
headerLeft: () => (
<LeftButtons
rid={rid}
tmid={tmid}
unreadsCount={unreadsCount}
navigation={navigation}

View File

@ -46,6 +46,10 @@ describe('Profile screen', () => {
await expect(element(by.id('profile-view-avatar')).atIndex(0)).toExist();
});
it('should have avatar edit button', async () => {
await expect(element(by.id('avatar-edit-button'))).toExist();
});
it('should have name', async () => {
await expect(element(by.id('profile-view-name'))).toExist();
});
@ -62,31 +66,6 @@ describe('Profile screen', () => {
await expect(element(by.id('profile-view-new-password'))).toExist();
});
it('should have avatar url', async () => {
await expect(element(by.id('profile-view-avatar-url'))).toExist();
});
it('should have reset avatar button', async () => {
await waitFor(element(by.id('profile-view-reset-avatar')))
.toExist()
.whileElement(by.id('profile-view-list'))
.scroll(scrollDown, 'down');
});
it('should have upload avatar button', async () => {
await waitFor(element(by.id('profile-view-upload-avatar')))
.toExist()
.whileElement(by.id('profile-view-list'))
.scroll(scrollDown, 'down');
});
it('should have avatar url button', async () => {
await waitFor(element(by.id('profile-view-avatar-url-button')))
.toExist()
.whileElement(by.id('profile-view-list'))
.scroll(scrollDown, 'down');
});
it('should have submit button', async () => {
await waitFor(element(by.id('profile-view-submit')))
.toExist()
@ -122,9 +101,24 @@ describe('Profile screen', () => {
});
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();
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);
await waitFor(element(by.id('change-avatar-view-submit')))
.toBeVisible()
.withTimeout(2000);
await element(by.id('change-avatar-view-submit')).tap();
await sleep(300);
await waitFor(element(by.id('profile-view')))
.toBeVisible()
.withTimeout(2000);
});
});
});

View File

@ -95,7 +95,7 @@
"react-native-fast-image": "RocketChat/react-native-fast-image.git#bump-version",
"react-native-file-viewer": "^2.1.4",
"react-native-gesture-handler": "2.4.2",
"react-native-image-crop-picker": "RocketChat/react-native-image-crop-picker",
"react-native-image-crop-picker": "RocketChat/react-native-image-crop-picker#fix.flicker-ios-15-crop-modal",
"react-native-image-progress": "^1.1.1",
"react-native-keycommands": "2.0.3",
"react-native-linear-gradient": "^2.6.2",

View File

@ -17301,9 +17301,9 @@ react-native-gradle-plugin@^0.0.6:
resolved "https://registry.yarnpkg.com/react-native-gradle-plugin/-/react-native-gradle-plugin-0.0.6.tgz#b61a9234ad2f61430937911003cddd7e15c72b45"
integrity sha512-eIlgtsmDp1jLC24dRn43hB3kEcZVqx6DUQbR0N1ABXGnMEafm9I3V3dUUeD1vh+Dy5WqijSoEwLNUPLgu5zDMg==
react-native-image-crop-picker@RocketChat/react-native-image-crop-picker:
react-native-image-crop-picker@RocketChat/react-native-image-crop-picker#fix.flicker-ios-15-crop-modal:
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/8cc2b36b48a3e5948efe84d26744f8608b252054"
react-native-image-progress@^1.1.1:
version "1.1.1"