Compare commits
51 Commits
develop
...
new.change
Author | SHA1 | Date |
---|---|---|
|
0fde16159d | |
|
e95102d38e | |
|
b2b2989e46 | |
|
de9551c78a | |
|
159d527796 | |
|
a5da4999e1 | |
|
8903c52ddb | |
|
ed92eb80f3 | |
|
dfcea278de | |
|
6d22747707 | |
|
1255f4ac3b | |
|
e8672ce827 | |
|
0393aecbdd | |
|
3db96db70c | |
|
2dc8a0c355 | |
|
af2b2a6185 | |
|
dee200ba3f | |
|
759504e46f | |
|
fe35747b52 | |
|
0ea494e3e7 | |
|
65dc56ad2d | |
|
ea20167981 | |
|
e8aab76148 | |
|
22230e0584 | |
|
9c073fa6dc | |
|
80c358838f | |
|
d46c86778c | |
|
ac8e30387f | |
|
2a4195c10a | |
|
7e1dec41e9 | |
|
34993dace2 | |
|
1e29a8efac | |
|
744565ad21 | |
|
0ce1fdcbf2 | |
|
2d8751c4bb | |
|
0cb7e3020a | |
|
a2dfbcbe30 | |
|
84d0401e3c | |
|
84b16b2d97 | |
|
4651a2fb91 | |
|
a7a4d9bb00 | |
|
e48b174118 | |
|
13f5075f7c | |
|
a478f1ff52 | |
|
a283f41022 | |
|
90984de444 | |
|
c4f09d8b7a | |
|
7813efbb26 | |
|
5e1f1c89da | |
|
cc9a9d523d | |
|
49f2c28b3e |
|
@ -1,13 +1,11 @@
|
|||
import { Q } from '@nozbe/watermelondb';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React from 'react';
|
||||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
|
||||
import { IApplicationState, TSubscriptionModel, TUserModel } from '../../definitions';
|
||||
import database from '../../lib/database';
|
||||
import { IApplicationState } from '../../definitions';
|
||||
import { getUserSelector } from '../../selectors/login';
|
||||
import Avatar from './Avatar';
|
||||
import { IAvatar } from './interfaces';
|
||||
import { useAvatarETag } from './useAvatarETag';
|
||||
|
||||
const AvatarContainer = ({
|
||||
style,
|
||||
|
@ -23,17 +21,13 @@ const AvatarContainer = ({
|
|||
isStatic,
|
||||
rid
|
||||
}: IAvatar): React.ReactElement => {
|
||||
const subscription = useRef<Subscription>();
|
||||
const [avatarETag, setAvatarETag] = useState<string | undefined>('');
|
||||
|
||||
const isDirect = () => type === 'd';
|
||||
|
||||
const server = useSelector((state: IApplicationState) => state.share.server.server || state.server.server);
|
||||
const serverVersion = useSelector((state: IApplicationState) => state.share.server.version || state.server.version);
|
||||
const { id, token } = useSelector(
|
||||
const { id, token, username } = useSelector(
|
||||
(state: IApplicationState) => ({
|
||||
id: getUserSelector(state).id,
|
||||
token: getUserSelector(state).token
|
||||
token: getUserSelector(state).token,
|
||||
username: getUserSelector(state).username
|
||||
}),
|
||||
shallowEqual
|
||||
);
|
||||
|
@ -48,41 +42,7 @@ const AvatarContainer = ({
|
|||
true
|
||||
);
|
||||
|
||||
const init = async () => {
|
||||
const db = database.active;
|
||||
const usersCollection = db.get('users');
|
||||
const subsCollection = db.get('subscriptions');
|
||||
|
||||
let record;
|
||||
try {
|
||||
if (isDirect()) {
|
||||
const [user] = await usersCollection.query(Q.where('username', text)).fetch();
|
||||
record = user;
|
||||
} else if (rid) {
|
||||
record = await subsCollection.find(rid);
|
||||
}
|
||||
} catch {
|
||||
// Record not found
|
||||
}
|
||||
|
||||
if (record) {
|
||||
const observable = record.observe() as Observable<TSubscriptionModel | TUserModel>;
|
||||
subscription.current = observable.subscribe(r => {
|
||||
setAvatarETag(r.avatarETag);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!avatarETag) {
|
||||
init();
|
||||
}
|
||||
return () => {
|
||||
if (subscription?.current?.unsubscribe) {
|
||||
subscription.current.unsubscribe();
|
||||
}
|
||||
};
|
||||
}, [text, type, size, avatarETag, externalProviderUrl]);
|
||||
const { avatarETag } = useAvatarETag({ username, text, type, rid, id });
|
||||
|
||||
return (
|
||||
<Avatar
|
|
@ -0,0 +1,85 @@
|
|||
import React from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
import Button from '../Button';
|
||||
import AvatarContainer from './AvatarContainer';
|
||||
import { IAvatar } from './interfaces';
|
||||
import I18n from '../../i18n';
|
||||
import { useTheme } from '../../theme';
|
||||
import { BUTTON_HIT_SLOP } from '../message/utils';
|
||||
import { useAppSelector } from '../../lib/hooks';
|
||||
import { compareServerVersion } from '../../lib/methods/helpers';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
editAvatarButton: {
|
||||
marginTop: 8,
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 12,
|
||||
marginBottom: 0,
|
||||
height: undefined
|
||||
},
|
||||
textButton: {
|
||||
fontSize: 12,
|
||||
...sharedStyles.textSemibold
|
||||
}
|
||||
});
|
||||
|
||||
interface IAvatarContainer extends Omit<IAvatar, 'size'> {
|
||||
handleEdit?: () => void;
|
||||
}
|
||||
|
||||
const AvatarWithEdit = ({
|
||||
style,
|
||||
text = '',
|
||||
avatar,
|
||||
emoji,
|
||||
borderRadius,
|
||||
type,
|
||||
children,
|
||||
onPress,
|
||||
getCustomEmoji,
|
||||
isStatic,
|
||||
rid,
|
||||
handleEdit
|
||||
}: IAvatarContainer): React.ReactElement => {
|
||||
const { colors } = useTheme();
|
||||
|
||||
const { serverVersion } = useAppSelector(state => ({
|
||||
serverVersion: state.server.version
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<AvatarContainer
|
||||
style={style}
|
||||
text={text}
|
||||
avatar={avatar}
|
||||
emoji={emoji}
|
||||
size={120}
|
||||
borderRadius={borderRadius}
|
||||
type={type}
|
||||
children={children}
|
||||
onPress={onPress}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
isStatic={isStatic}
|
||||
rid={rid}
|
||||
/>
|
||||
{handleEdit && serverVersion && compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '3.6.0') ? (
|
||||
<Button
|
||||
title={I18n.t('Edit')}
|
||||
type='secondary'
|
||||
backgroundColor={colors.editAndUploadButtonAvatar}
|
||||
onPress={handleEdit}
|
||||
testID='avatar-edit-button'
|
||||
style={styles.editAvatarButton}
|
||||
styleText={styles.textButton}
|
||||
color={colors.titleText}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AvatarWithEdit;
|
|
@ -0,0 +1,5 @@
|
|||
import Avatar from './AvatarContainer';
|
||||
|
||||
export { default as AvatarWithEdit } from './AvatarWithEdit';
|
||||
|
||||
export default Avatar;
|
|
@ -0,0 +1,67 @@
|
|||
import { Q } from '@nozbe/watermelondb';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
|
||||
import { TLoggedUserModel, TSubscriptionModel, TUserModel } from '../../definitions';
|
||||
import database from '../../lib/database';
|
||||
|
||||
export const useAvatarETag = ({
|
||||
username,
|
||||
text,
|
||||
type = '',
|
||||
rid,
|
||||
id
|
||||
}: {
|
||||
type?: string;
|
||||
username: string;
|
||||
text: string;
|
||||
rid?: string;
|
||||
id: string;
|
||||
}) => {
|
||||
const [avatarETag, setAvatarETag] = useState<string | undefined>('');
|
||||
|
||||
const isDirect = () => type === 'd';
|
||||
|
||||
useEffect(() => {
|
||||
let subscription: Subscription;
|
||||
if (!avatarETag) {
|
||||
const observeAvatarETag = async () => {
|
||||
const db = database.active;
|
||||
const usersCollection = db.get('users');
|
||||
const subsCollection = db.get('subscriptions');
|
||||
|
||||
let record;
|
||||
try {
|
||||
if (username === text) {
|
||||
const serversDB = database.servers;
|
||||
const userCollections = serversDB.get('users');
|
||||
const user = await userCollections.find(id);
|
||||
record = user;
|
||||
} else if (isDirect()) {
|
||||
const [user] = await usersCollection.query(Q.where('username', text)).fetch();
|
||||
record = user;
|
||||
} else if (rid) {
|
||||
record = await subsCollection.find(rid);
|
||||
}
|
||||
} catch {
|
||||
// Record not found
|
||||
}
|
||||
|
||||
if (record) {
|
||||
const observable = record.observe() as Observable<TSubscriptionModel | TUserModel | TLoggedUserModel>;
|
||||
subscription = observable.subscribe(r => {
|
||||
setAvatarETag(r.avatarETag);
|
||||
});
|
||||
}
|
||||
};
|
||||
observeAvatarETag();
|
||||
return () => {
|
||||
if (subscription?.unsubscribe) {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
};
|
||||
}
|
||||
}, [text]);
|
||||
|
||||
return { avatarETag };
|
||||
};
|
|
@ -14,7 +14,7 @@ interface IButtonProps extends PlatformTouchableProps {
|
|||
loading?: boolean;
|
||||
color?: string;
|
||||
fontSize?: number;
|
||||
styleText?: StyleProp<TextStyle>[];
|
||||
styleText?: StyleProp<TextStyle> | StyleProp<TextStyle>[];
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
|
|
@ -17,16 +17,14 @@ export interface IAvatarButton {
|
|||
}
|
||||
|
||||
export interface IAvatar {
|
||||
data: {} | string | null;
|
||||
data: string | null;
|
||||
url?: string;
|
||||
contentType?: string;
|
||||
service?: any;
|
||||
}
|
||||
|
||||
export interface IAvatarSuggestion {
|
||||
[service: string]: {
|
||||
url: string;
|
||||
blob: string;
|
||||
contentType: string;
|
||||
};
|
||||
url: string;
|
||||
blob: string;
|
||||
contentType: string;
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export type TChangeAvatarViewContext = 'profile' | 'room';
|
|
@ -877,6 +877,14 @@
|
|||
"Reply_in_direct_message": "Reply in Direct Message",
|
||||
"room_archived": "archived room",
|
||||
"room_unarchived": "unarchived room",
|
||||
"Upload_image": "Upload image",
|
||||
"Delete_image": "Delete image",
|
||||
"Images_uploaded": "Images uploaded",
|
||||
"Avatar": "Avatar",
|
||||
"insert_Avatar_URL": "insert image URL here",
|
||||
"Discard_changes":"Discard changes?",
|
||||
"Discard":"Discard",
|
||||
"Discard_changes_description":"All changes will be lost if you go back without saving.",
|
||||
"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"
|
||||
|
|
|
@ -876,6 +876,14 @@
|
|||
"Reply_in_direct_message": "Responder por mensagem direta",
|
||||
"room_archived": "{{username}} arquivou a sala",
|
||||
"room_unarchived": "{{username}} desarquivou a sala",
|
||||
"Upload_image": "Carregar imagem",
|
||||
"Delete_image": "Deletar imagem",
|
||||
"Images_uploaded": "Imagens carregadas",
|
||||
"Avatar": "Avatar",
|
||||
"insert_Avatar_URL": "insira o URL da imagem aqui",
|
||||
"Discard_changes":"Descartar alterações?",
|
||||
"Discard":"Descartar",
|
||||
"Discard_changes_description":"Todas as alterações serão perdidas, se você sair sem salvar.",
|
||||
"Presence_Cap_Warning_Title": "Status do usuário desabilitado temporariamente",
|
||||
"Presence_Cap_Warning_Description": "O limite de conexões ativas para a workspace foi atingido, por isso o serviço responsável pela presença dos usuários está temporariamente desabilitado. Ele pode ser reabilitado manualmente nas configurações da workspace."
|
||||
}
|
|
@ -59,6 +59,7 @@ export const colors = {
|
|||
buttonText: '#ffffff',
|
||||
passcodeBackground: '#EEEFF1',
|
||||
passcodeButtonActive: '#E4E7EA',
|
||||
editAndUploadButtonAvatar: '#E4E7EA',
|
||||
passcodeLockIcon: '#6C727A',
|
||||
passcodePrimary: '#2F343D',
|
||||
passcodeSecondary: '#6C727A',
|
||||
|
@ -128,6 +129,7 @@ export const colors = {
|
|||
buttonText: '#ffffff',
|
||||
passcodeBackground: '#030C1B',
|
||||
passcodeButtonActive: '#0B182C',
|
||||
editAndUploadButtonAvatar: '#0B182C',
|
||||
passcodeLockIcon: '#6C727A',
|
||||
passcodePrimary: '#FFFFFF',
|
||||
passcodeSecondary: '#CBCED1',
|
||||
|
@ -197,6 +199,7 @@ export const colors = {
|
|||
buttonText: '#ffffff',
|
||||
passcodeBackground: '#000000',
|
||||
passcodeButtonActive: '#0E0D0D',
|
||||
editAndUploadButtonAvatar: '#0E0D0D',
|
||||
passcodeLockIcon: '#6C727A',
|
||||
passcodePrimary: '#FFFFFF',
|
||||
passcodeSecondary: '#CBCED1',
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import { Image } from 'react-native';
|
||||
|
||||
export const isImageURL = async (url: string): Promise<boolean> => {
|
||||
try {
|
||||
const result = await Image.prefetch(url);
|
||||
return result;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
|
@ -14,3 +14,4 @@ export * from './server';
|
|||
export * from './url';
|
||||
export * from './isValidEmail';
|
||||
export * from './random';
|
||||
export * from './image';
|
||||
|
|
|
@ -291,7 +291,7 @@ export default function subscribeRooms() {
|
|||
const [type, data] = ddpMessage.fields.args;
|
||||
const [, ev] = ddpMessage.fields.eventName.split('/');
|
||||
if (/userData/.test(ev)) {
|
||||
const [{ diff }] = ddpMessage.fields.args;
|
||||
const [{ diff, unset }] = ddpMessage.fields.args;
|
||||
if (diff?.statusLivechat) {
|
||||
store.dispatch(setUser({ statusLivechat: diff.statusLivechat }));
|
||||
}
|
||||
|
@ -301,6 +301,12 @@ export default function subscribeRooms() {
|
|||
if ((['settings.preferences.alsoSendThreadToChannel'] as any) in diff) {
|
||||
store.dispatch(setUser({ alsoSendThreadToChannel: diff['settings.preferences.alsoSendThreadToChannel'] }));
|
||||
}
|
||||
if (diff?.avatarETag) {
|
||||
store.dispatch(setUser({ avatarETag: diff.avatarETag }));
|
||||
}
|
||||
if (unset?.avatarETag) {
|
||||
store.dispatch(setUser({ avatarETag: '' }));
|
||||
}
|
||||
}
|
||||
if (/subscriptions/.test(ev)) {
|
||||
if (type === 'removed') {
|
||||
|
|
|
@ -561,7 +561,7 @@ export const saveRoomSettings = (
|
|||
rid: string,
|
||||
params: {
|
||||
roomName?: string;
|
||||
roomAvatar?: string;
|
||||
roomAvatar?: string | null;
|
||||
roomDescription?: string;
|
||||
roomTopic?: string;
|
||||
roomAnnouncement?: string;
|
||||
|
@ -602,7 +602,7 @@ export const getRoomRoles = (
|
|||
// RC 0.65.0
|
||||
sdk.get(`${roomTypeToApiType(type)}.roles`, { roomId });
|
||||
|
||||
export const getAvatarSuggestion = (): Promise<IAvatarSuggestion> =>
|
||||
export const getAvatarSuggestion = (): Promise<{ [service: string]: IAvatarSuggestion }> =>
|
||||
// RC 0.51.0
|
||||
sdk.methodCallWrapper('getAvatarSuggestion');
|
||||
|
||||
|
|
|
@ -247,6 +247,22 @@ const handleLogout = function* handleLogout({ forcedByServer, message }) {
|
|||
};
|
||||
|
||||
const handleSetUser = function* handleSetUser({ user }) {
|
||||
if ('avatarETag' in user) {
|
||||
const userId = yield select(state => state.login.user.id);
|
||||
const serversDB = database.servers;
|
||||
const userCollections = serversDB.get('users');
|
||||
yield serversDB.write(async () => {
|
||||
try {
|
||||
const userRecord = await userCollections.find(userId);
|
||||
await userRecord.update(record => {
|
||||
record.avatarETag = user.avatarETag;
|
||||
});
|
||||
} catch {
|
||||
//
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setLanguage(user?.language);
|
||||
|
||||
if (user?.statusLivechat && isOmnichannelModuleAvailable()) {
|
||||
|
|
|
@ -68,6 +68,7 @@ import AddChannelTeamView from '../views/AddChannelTeamView';
|
|||
import AddExistingChannelView from '../views/AddExistingChannelView';
|
||||
import SelectListView from '../views/SelectListView';
|
||||
import DiscussionsView from '../views/DiscussionsView';
|
||||
import ChangeAvatarView from '../views/ChangeAvatarView';
|
||||
import {
|
||||
AdminPanelStackParamList,
|
||||
ChatsStackParamList,
|
||||
|
@ -96,6 +97,7 @@ const ChatsStackNavigator = () => {
|
|||
<ChatsStack.Screen name='SelectListView' component={SelectListView} options={SelectListView.navigationOptions} />
|
||||
<ChatsStack.Screen name='RoomInfoView' component={RoomInfoView} options={RoomInfoView.navigationOptions} />
|
||||
<ChatsStack.Screen name='RoomInfoEditView' component={RoomInfoEditView} options={RoomInfoEditView.navigationOptions} />
|
||||
<ChatsStack.Screen name='ChangeAvatarView' component={ChangeAvatarView} />
|
||||
<ChatsStack.Screen name='RoomMembersView' component={RoomMembersView} />
|
||||
<ChatsStack.Screen name='DiscussionsView' component={DiscussionsView} />
|
||||
<ChatsStack.Screen
|
||||
|
@ -151,6 +153,7 @@ const ProfileStackNavigator = () => {
|
|||
>
|
||||
<ProfileStack.Screen name='ProfileView' component={ProfileView} options={ProfileView.navigationOptions} />
|
||||
<ProfileStack.Screen name='UserPreferencesView' component={UserPreferencesView} />
|
||||
<ProfileStack.Screen name='ChangeAvatarView' component={ChangeAvatarView} />
|
||||
<ProfileStack.Screen name='UserNotificationPrefView' component={UserNotificationPrefView} />
|
||||
<ProfileStack.Screen name='PickerView' component={PickerView} options={PickerView.navigationOptions} />
|
||||
</ProfileStack.Navigator>
|
||||
|
|
|
@ -17,6 +17,7 @@ import RoomsListView from '../../views/RoomsListView';
|
|||
import RoomActionsView from '../../views/RoomActionsView';
|
||||
import RoomInfoView from '../../views/RoomInfoView';
|
||||
import RoomInfoEditView from '../../views/RoomInfoEditView';
|
||||
import ChangeAvatarView from '../../views/ChangeAvatarView';
|
||||
import RoomMembersView from '../../views/RoomMembersView';
|
||||
import SearchMessagesView from '../../views/SearchMessagesView';
|
||||
import SelectedUsersView from '../../views/SelectedUsersView';
|
||||
|
@ -128,6 +129,7 @@ const ModalStackNavigator = React.memo(({ navigation }: INavigation) => {
|
|||
<ModalStack.Screen name='RoomInfoView' component={RoomInfoView} options={RoomInfoView.navigationOptions} />
|
||||
<ModalStack.Screen name='SelectListView' component={SelectListView} />
|
||||
<ModalStack.Screen name='RoomInfoEditView' component={RoomInfoEditView} options={RoomInfoEditView.navigationOptions} />
|
||||
<ModalStack.Screen name='ChangeAvatarView' component={ChangeAvatarView} />
|
||||
<ModalStack.Screen name='RoomMembersView' component={RoomMembersView} />
|
||||
<ModalStack.Screen
|
||||
name='SearchMessagesView'
|
||||
|
|
|
@ -6,6 +6,7 @@ import { IMessage } from '../../definitions/IMessage';
|
|||
import { ISubscription, SubscriptionType, TSubscriptionModel } from '../../definitions/ISubscription';
|
||||
import { ILivechatDepartment } from '../../definitions/ILivechatDepartment';
|
||||
import { ILivechatTag } from '../../definitions/ILivechatTag';
|
||||
import { TChangeAvatarViewContext } from '../../definitions/TChangeAvatarViewContext';
|
||||
|
||||
export type MasterDetailChatsStackParamList = {
|
||||
RoomView: {
|
||||
|
@ -58,6 +59,12 @@ export type ModalStackParamList = {
|
|||
onSearch?: Function;
|
||||
isRadio?: boolean;
|
||||
};
|
||||
ChangeAvatarView: {
|
||||
context: TChangeAvatarViewContext;
|
||||
titleHeader?: string;
|
||||
room?: ISubscription;
|
||||
t?: SubscriptionType;
|
||||
};
|
||||
RoomInfoEditView: {
|
||||
rid: string;
|
||||
};
|
||||
|
|
|
@ -13,6 +13,7 @@ import { ModalStackParamList } from './MasterDetailStack/types';
|
|||
import { TThreadModel } from '../definitions';
|
||||
import { ILivechatDepartment } from '../definitions/ILivechatDepartment';
|
||||
import { ILivechatTag } from '../definitions/ILivechatTag';
|
||||
import { TChangeAvatarViewContext } from '../definitions/TChangeAvatarViewContext';
|
||||
|
||||
export type ChatsStackParamList = {
|
||||
ModalStackNavigator: NavigatorScreenParams<ModalStackParamList>;
|
||||
|
@ -181,6 +182,12 @@ export type ChatsStackParamList = {
|
|||
onlyAudio?: boolean;
|
||||
videoConf?: boolean;
|
||||
};
|
||||
ChangeAvatarView: {
|
||||
context: TChangeAvatarViewContext;
|
||||
titleHeader?: string;
|
||||
room?: ISubscription;
|
||||
t?: SubscriptionType;
|
||||
};
|
||||
};
|
||||
|
||||
export type ProfileStackParamList = {
|
||||
|
@ -195,6 +202,12 @@ export type ProfileStackParamList = {
|
|||
goBack?: Function;
|
||||
onChangeValue: Function;
|
||||
};
|
||||
ChangeAvatarView: {
|
||||
context: TChangeAvatarViewContext;
|
||||
titleHeader?: string;
|
||||
room?: ISubscription;
|
||||
t?: SubscriptionType;
|
||||
};
|
||||
};
|
||||
|
||||
export type SettingsStackParamList = {
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
|
||||
import { IAvatar } from '../../definitions';
|
||||
import { Services } from '../../lib/services';
|
||||
import I18n from '../../i18n';
|
||||
import styles from './styles';
|
||||
import { useTheme } from '../../theme';
|
||||
import AvatarSuggestionItem from './AvatarSuggestionItem';
|
||||
|
||||
const AvatarSuggestion = ({
|
||||
onPress,
|
||||
username,
|
||||
resetAvatar
|
||||
}: {
|
||||
onPress: (value: IAvatar) => void;
|
||||
username?: string;
|
||||
resetAvatar?: () => void;
|
||||
}) => {
|
||||
const [avatarSuggestions, setAvatarSuggestions] = useState<IAvatar[]>([]);
|
||||
|
||||
const { colors } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
const getAvatarSuggestion = async () => {
|
||||
const result = await Services.getAvatarSuggestion();
|
||||
const suggestions = Object.keys(result).map(service => {
|
||||
const { url, blob, contentType } = result[service];
|
||||
return {
|
||||
url,
|
||||
data: blob,
|
||||
service,
|
||||
contentType
|
||||
};
|
||||
});
|
||||
setAvatarSuggestions(suggestions);
|
||||
};
|
||||
getAvatarSuggestion();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View style={styles.containerImagesUploaded}>
|
||||
<Text style={[styles.itemLabel, { color: colors.titleText }]}>{I18n.t('Images_uploaded')}</Text>
|
||||
<View style={styles.containerAvatarSuggestion}>
|
||||
{username && resetAvatar ? (
|
||||
<AvatarSuggestionItem text={`@${username}`} testID={`reset-avatar-suggestion`} onPress={resetAvatar} />
|
||||
) : null}
|
||||
{avatarSuggestions.slice(0, 7).map(item => (
|
||||
<AvatarSuggestionItem item={item} testID={`${item?.service}-avatar-suggestion`} onPress={onPress} />
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default AvatarSuggestion;
|
|
@ -0,0 +1,40 @@
|
|||
import React from 'react';
|
||||
import { View, StyleSheet } from 'react-native';
|
||||
|
||||
import { IAvatar } from '../../definitions';
|
||||
import Avatar from '../../containers/Avatar';
|
||||
import { useTheme } from '../../theme';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
width: 64,
|
||||
height: 64,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: 20,
|
||||
marginBottom: 12,
|
||||
borderRadius: 4
|
||||
}
|
||||
});
|
||||
|
||||
const AvatarSuggestionItem = ({
|
||||
item,
|
||||
onPress,
|
||||
text,
|
||||
testID
|
||||
}: {
|
||||
item?: IAvatar;
|
||||
testID?: string;
|
||||
onPress: Function;
|
||||
text?: string;
|
||||
}) => {
|
||||
const { colors } = useTheme();
|
||||
|
||||
return (
|
||||
<View key={item?.service} testID={testID} style={[styles.container, { backgroundColor: colors.borderColor }]}>
|
||||
<Avatar avatar={item?.url} text={text} size={64} onPress={() => onPress(item)} />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default AvatarSuggestionItem;
|
|
@ -0,0 +1,29 @@
|
|||
import React from 'react';
|
||||
|
||||
import I18n from '../../i18n';
|
||||
import { FormTextInput } from '../../containers/TextInput';
|
||||
import { useDebounce, isImageURL } from '../../lib/methods/helpers';
|
||||
|
||||
const AvatarUrl = ({ submit }: { submit: (value: string) => void }) => {
|
||||
const handleChangeText = useDebounce(async (value: string) => {
|
||||
if (value) {
|
||||
const result = await isImageURL(value);
|
||||
if (result) {
|
||||
return submit(value);
|
||||
}
|
||||
return submit('');
|
||||
}
|
||||
}, 500);
|
||||
|
||||
return (
|
||||
<FormTextInput
|
||||
label={I18n.t('Avatar_Url')}
|
||||
placeholder={I18n.t('insert_Avatar_URL')}
|
||||
onChangeText={handleChangeText}
|
||||
testID='change-avatar-view-avatar-url'
|
||||
containerStyle={{ marginBottom: 0 }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AvatarUrl;
|
|
@ -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;
|
|
@ -0,0 +1,27 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
|
||||
import sharedStyles from '../Styles';
|
||||
|
||||
export default StyleSheet.create({
|
||||
avatarContainer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 24
|
||||
},
|
||||
separator: {
|
||||
marginVertical: 16
|
||||
},
|
||||
itemLabel: {
|
||||
marginBottom: 12,
|
||||
fontSize: 14,
|
||||
...sharedStyles.textSemibold
|
||||
},
|
||||
containerImagesUploaded: {
|
||||
flex: 1
|
||||
},
|
||||
containerAvatarSuggestion: {
|
||||
flex: 1,
|
||||
flexWrap: 'wrap',
|
||||
flexDirection: 'row'
|
||||
}
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
import I18n from '../../i18n';
|
||||
|
||||
export const handleError = (e: any, action: string) => {
|
||||
if (e.data && e.data.error.includes('[error-too-many-requests]')) {
|
||||
throw new Error(e.data.error);
|
||||
}
|
||||
if (e.error && e.error === 'error-avatar-invalid-url') {
|
||||
throw new Error(I18n.t(e.error, { url: e.details.url }));
|
||||
}
|
||||
if (I18n.isTranslated(e.error)) {
|
||||
throw new Error(I18n.t(e.error));
|
||||
}
|
||||
throw new Error(I18n.t('There_was_an_error_while_action', { action: I18n.t(action) }));
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
import { Services } from '../../lib/services';
|
||||
import log from '../../lib/methods/helpers/log';
|
||||
import { IAvatar } from '../../definitions';
|
||||
import { handleError } from './submitHelpers';
|
||||
|
||||
export const changeRoomsAvatar = async (rid: string, roomAvatar: string | null) => {
|
||||
try {
|
||||
await Services.saveRoomSettings(rid, { roomAvatar });
|
||||
} catch (e) {
|
||||
log(e);
|
||||
return handleError(e, 'changing_avatar');
|
||||
}
|
||||
};
|
||||
|
||||
export const changeUserAvatar = async (avatarUpload: IAvatar) => {
|
||||
try {
|
||||
await Services.setAvatarFromService(avatarUpload);
|
||||
} catch (e) {
|
||||
return handleError(e, 'changing_avatar');
|
||||
}
|
||||
};
|
||||
|
||||
export const resetUserAvatar = async (userId: string) => {
|
||||
try {
|
||||
await Services.resetAvatar(userId);
|
||||
} catch (e) {
|
||||
return handleError(e, 'changing_avatar');
|
||||
}
|
||||
};
|
|
@ -2,7 +2,6 @@ import React from 'react';
|
|||
import { Keyboard, ScrollView, TextInput, View } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
import { sha256 } from 'js-sha256';
|
||||
import ImagePicker, { Image } from 'react-native-image-crop-picker';
|
||||
import RNPickerSelect from 'react-native-picker-select';
|
||||
import { dequal } from 'dequal';
|
||||
import omit from 'lodash/omit';
|
||||
|
@ -12,16 +11,15 @@ import Touch from '../../containers/Touch';
|
|||
import KeyboardView from '../../containers/KeyboardView';
|
||||
import sharedStyles from '../Styles';
|
||||
import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps';
|
||||
import { showConfirmationAlert, showErrorAlert } from '../../lib/methods/helpers';
|
||||
import { showErrorAlert, showConfirmationAlert } from '../../lib/methods/helpers';
|
||||
import { LISTENER } from '../../containers/Toast';
|
||||
import EventEmitter from '../../lib/methods/helpers/events';
|
||||
import { FormTextInput } from '../../containers/TextInput';
|
||||
import log, { events, logEvent } from '../../lib/methods/helpers/log';
|
||||
import { events, logEvent } from '../../lib/methods/helpers/log';
|
||||
import I18n from '../../i18n';
|
||||
import Button from '../../containers/Button';
|
||||
import Avatar from '../../containers/Avatar';
|
||||
import { AvatarWithEdit } from '../../containers/Avatar';
|
||||
import { setUser } from '../../actions/login';
|
||||
import { CustomIcon } from '../../containers/CustomIcon';
|
||||
import * as HeaderButton from '../../containers/HeaderButton';
|
||||
import StatusBar from '../../containers/StatusBar';
|
||||
import { themes } from '../../lib/constants';
|
||||
|
@ -31,15 +29,7 @@ import SafeAreaView from '../../containers/SafeAreaView';
|
|||
import styles from './styles';
|
||||
import { ProfileStackParamList } from '../../stacks/types';
|
||||
import { Services } from '../../lib/services';
|
||||
import {
|
||||
IApplicationState,
|
||||
IAvatar,
|
||||
IAvatarButton,
|
||||
IAvatarSuggestion,
|
||||
IBaseScreen,
|
||||
IProfileParams,
|
||||
IUser
|
||||
} from '../../definitions';
|
||||
import { IApplicationState, IAvatarButton, IBaseScreen, IProfileParams, IUser } from '../../definitions';
|
||||
import { twoFactor } from '../../lib/services/twoFactor';
|
||||
import { TwoFactorMethods } from '../../definitions/ITotp';
|
||||
import { withActionSheet, IActionSheetProvider } from '../../containers/ActionSheet';
|
||||
|
@ -67,9 +57,6 @@ interface IProfileViewState {
|
|||
email: string | null;
|
||||
newPassword: string | null;
|
||||
currentPassword: string | null;
|
||||
avatarUrl: string | null;
|
||||
avatar: IAvatar;
|
||||
avatarSuggestions: IAvatarSuggestion;
|
||||
customFields: {
|
||||
[key: string | number]: string;
|
||||
};
|
||||
|
@ -113,25 +100,12 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
|||
email: '',
|
||||
newPassword: '',
|
||||
currentPassword: '',
|
||||
avatarUrl: '',
|
||||
avatar: {
|
||||
data: {},
|
||||
url: ''
|
||||
},
|
||||
avatarSuggestions: {},
|
||||
customFields: {},
|
||||
twoFactorCode: null
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
componentDidMount() {
|
||||
this.init();
|
||||
|
||||
try {
|
||||
const result = await Services.getAvatarSuggestion();
|
||||
this.setState({ avatarSuggestions: result });
|
||||
} catch (e) {
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps: IProfileViewProps) {
|
||||
|
@ -147,16 +121,6 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
|||
}
|
||||
}
|
||||
|
||||
setAvatar = (avatar: IAvatar) => {
|
||||
const { Accounts_AllowUserAvatarChange } = this.props;
|
||||
|
||||
if (!Accounts_AllowUserAvatarChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ avatar });
|
||||
};
|
||||
|
||||
init = (user?: IUser) => {
|
||||
const { user: userProps } = this.props;
|
||||
const { name, username, emails, customFields } = user || userProps;
|
||||
|
@ -167,17 +131,12 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
|||
email: emails ? emails[0].address : null,
|
||||
newPassword: null,
|
||||
currentPassword: null,
|
||||
avatarUrl: null,
|
||||
avatar: {
|
||||
data: {},
|
||||
url: ''
|
||||
},
|
||||
customFields: customFields || {}
|
||||
});
|
||||
};
|
||||
|
||||
formIsChanged = () => {
|
||||
const { name, username, email, newPassword, avatar, customFields } = this.state;
|
||||
const { name, username, email, newPassword, customFields } = this.state;
|
||||
const { user } = this.props;
|
||||
let customFieldsChanged = false;
|
||||
|
||||
|
@ -196,21 +155,10 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
|||
!newPassword &&
|
||||
user.emails &&
|
||||
user.emails[0].address === email &&
|
||||
!avatar.data &&
|
||||
!customFieldsChanged
|
||||
);
|
||||
};
|
||||
|
||||
handleError = (e: any, _func: string, action: string) => {
|
||||
if (e.data && e.data.error.includes('[error-too-many-requests]')) {
|
||||
return showErrorAlert(e.data.error);
|
||||
}
|
||||
if (I18n.isTranslated(e.error)) {
|
||||
return showErrorAlert(I18n.t(e.error));
|
||||
}
|
||||
showErrorAlert(I18n.t('There_was_an_error_while_action', { action: I18n.t(action) }));
|
||||
};
|
||||
|
||||
submit = async (): Promise<void> => {
|
||||
Keyboard.dismiss();
|
||||
|
||||
|
@ -220,7 +168,7 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
|||
|
||||
this.setState({ saving: true });
|
||||
|
||||
const { name, username, email, newPassword, currentPassword, avatar, customFields, twoFactorCode } = this.state;
|
||||
const { name, username, email, newPassword, currentPassword, customFields, twoFactorCode } = this.state;
|
||||
const { user, dispatch } = this.props;
|
||||
const params = {} as IProfileParams;
|
||||
|
||||
|
@ -273,17 +221,6 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
|||
}
|
||||
|
||||
try {
|
||||
if (avatar.url) {
|
||||
try {
|
||||
logEvent(events.PROFILE_SAVE_AVATAR);
|
||||
await Services.setAvatarFromService(avatar);
|
||||
} catch (e) {
|
||||
logEvent(events.PROFILE_SAVE_AVATAR_F);
|
||||
this.setState({ saving: false, currentPassword: null });
|
||||
return this.handleError(e, 'setAvatarFromService', 'changing_avatar');
|
||||
}
|
||||
}
|
||||
|
||||
const twoFactorOptions = params.currentPassword
|
||||
? {
|
||||
twoFactorCode: params.currentPassword,
|
||||
|
@ -317,7 +254,7 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
|||
}
|
||||
logEvent(events.PROFILE_SAVE_CHANGES_F);
|
||||
this.setState({ saving: false, currentPassword: null, twoFactorCode: null });
|
||||
this.handleError(e, 'saveUserProfile', 'saving_profile');
|
||||
this.handleError(e, 'saving_profile');
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -334,39 +271,23 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
|||
EventEmitter.emit(LISTENER, { message: I18n.t('Avatar_changed_successfully') });
|
||||
this.init();
|
||||
} catch (e) {
|
||||
this.handleError(e, 'resetAvatar', 'changing_avatar');
|
||||
this.handleError(e, 'changing_avatar');
|
||||
}
|
||||
};
|
||||
|
||||
pickImage = async () => {
|
||||
const { Accounts_AllowUserAvatarChange } = this.props;
|
||||
|
||||
if (!Accounts_AllowUserAvatarChange) {
|
||||
return;
|
||||
handleError = (e: any, action: string) => {
|
||||
if (e.data && e.data.error.includes('[error-too-many-requests]')) {
|
||||
return showErrorAlert(e.data.error);
|
||||
}
|
||||
|
||||
const options = {
|
||||
cropping: true,
|
||||
compressImageQuality: 0.8,
|
||||
freeStyleCropEnabled: true,
|
||||
cropperAvoidEmptySpaceAroundImage: false,
|
||||
cropperChooseText: I18n.t('Choose'),
|
||||
cropperCancelText: I18n.t('Cancel'),
|
||||
includeBase64: true
|
||||
};
|
||||
try {
|
||||
logEvent(events.PROFILE_PICK_AVATAR);
|
||||
const response: Image = await ImagePicker.openPicker(options);
|
||||
this.setAvatar({ url: response.path, data: `data:image/jpeg;base64,${response.data}`, service: 'upload' });
|
||||
} catch (error) {
|
||||
logEvent(events.PROFILE_PICK_AVATAR_F);
|
||||
console.warn(error);
|
||||
if (I18n.isTranslated(e.error)) {
|
||||
return showErrorAlert(I18n.t(e.error));
|
||||
}
|
||||
showErrorAlert(I18n.t('There_was_an_error_while_action', { action: I18n.t(action) }));
|
||||
};
|
||||
|
||||
pickImageWithURL = (avatarUrl: string) => {
|
||||
logEvent(events.PROFILE_PICK_AVATAR_WITH_URL);
|
||||
this.setAvatar({ url: avatarUrl, data: avatarUrl, service: 'url' });
|
||||
handleEditAvatar = () => {
|
||||
const { navigation } = this.props;
|
||||
navigation.navigate('ChangeAvatarView', { context: 'profile' });
|
||||
};
|
||||
|
||||
renderAvatarButton = ({ key, child, onPress, disabled = false }: IAvatarButton) => {
|
||||
|
@ -384,49 +305,6 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
|||
);
|
||||
};
|
||||
|
||||
renderAvatarButtons = () => {
|
||||
const { avatarUrl, avatarSuggestions } = this.state;
|
||||
const { user, theme, Accounts_AllowUserAvatarChange } = this.props;
|
||||
|
||||
return (
|
||||
<View style={styles.avatarButtons}>
|
||||
{this.renderAvatarButton({
|
||||
child: <Avatar text={`@${user.username}`} size={50} />,
|
||||
onPress: () => this.resetAvatar(),
|
||||
disabled: !Accounts_AllowUserAvatarChange,
|
||||
key: 'profile-view-reset-avatar'
|
||||
})}
|
||||
{this.renderAvatarButton({
|
||||
child: <CustomIcon name='upload' size={30} color={themes[theme].bodyText} />,
|
||||
onPress: () => this.pickImage(),
|
||||
disabled: !Accounts_AllowUserAvatarChange,
|
||||
key: 'profile-view-upload-avatar'
|
||||
})}
|
||||
{this.renderAvatarButton({
|
||||
child: <CustomIcon name='link' size={30} color={themes[theme].bodyText} />,
|
||||
onPress: () => (avatarUrl ? this.pickImageWithURL(avatarUrl) : null),
|
||||
disabled: !avatarUrl,
|
||||
key: 'profile-view-avatar-url-button'
|
||||
})}
|
||||
{Object.keys(avatarSuggestions).map(service => {
|
||||
const { url, blob, contentType } = avatarSuggestions[service];
|
||||
return this.renderAvatarButton({
|
||||
disabled: !Accounts_AllowUserAvatarChange,
|
||||
key: `profile-view-avatar-${service}`,
|
||||
child: <Avatar avatar={url} size={50} />,
|
||||
onPress: () =>
|
||||
this.setAvatar({
|
||||
url,
|
||||
data: blob,
|
||||
service,
|
||||
contentType
|
||||
})
|
||||
});
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
renderCustomFields = () => {
|
||||
const { customFields } = this.state;
|
||||
const { Accounts_CustomFields } = this.props;
|
||||
|
@ -520,7 +398,7 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
|||
};
|
||||
|
||||
render() {
|
||||
const { name, username, email, newPassword, avatarUrl, customFields, avatar, saving } = this.state;
|
||||
const { name, username, email, newPassword, customFields, saving } = this.state;
|
||||
const {
|
||||
user,
|
||||
theme,
|
||||
|
@ -543,7 +421,10 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
|||
<SafeAreaView testID='profile-view'>
|
||||
<ScrollView contentContainerStyle={sharedStyles.containerScrollView} testID='profile-view-list' {...scrollPersistTaps}>
|
||||
<View style={styles.avatarContainer} testID='profile-view-avatar'>
|
||||
<Avatar text={user.username} avatar={avatar?.url} isStatic={avatar?.url} size={100} />
|
||||
<AvatarWithEdit
|
||||
text={user.username}
|
||||
handleEdit={Accounts_AllowUserAvatarChange ? this.handleEditAvatar : undefined}
|
||||
/>
|
||||
</View>
|
||||
<FormTextInput
|
||||
editable={Accounts_AllowRealNameChange}
|
||||
|
@ -615,22 +496,6 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
|||
testID='profile-view-new-password'
|
||||
/>
|
||||
{this.renderCustomFields()}
|
||||
<FormTextInput
|
||||
editable={Accounts_AllowUserAvatarChange}
|
||||
inputStyle={[!Accounts_AllowUserAvatarChange && styles.disabled]}
|
||||
inputRef={e => {
|
||||
if (e) {
|
||||
this.avatarUrl = e;
|
||||
}
|
||||
}}
|
||||
label={I18n.t('Avatar_Url')}
|
||||
placeholder={I18n.t('Avatar_Url')}
|
||||
value={avatarUrl || undefined}
|
||||
onChangeText={value => this.setState({ avatarUrl: value })}
|
||||
onSubmitEditing={this.submit}
|
||||
testID='profile-view-avatar-url'
|
||||
/>
|
||||
{this.renderAvatarButtons()}
|
||||
<Button
|
||||
title={I18n.t('Save_Changes')}
|
||||
type='primary'
|
||||
|
|
|
@ -7,7 +7,7 @@ export default StyleSheet.create({
|
|||
avatarContainer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 10
|
||||
marginBottom: 24
|
||||
},
|
||||
avatarButtons: {
|
||||
flexWrap: 'wrap',
|
||||
|
|
|
@ -2,15 +2,13 @@ import React from 'react';
|
|||
import { Q } from '@nozbe/watermelondb';
|
||||
import { BlockContext } from '@rocket.chat/ui-kit';
|
||||
import { dequal } from 'dequal';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { Alert, Keyboard, ScrollView, Text, TextInput, TouchableOpacity, View, StyleSheet } from 'react-native';
|
||||
import ImagePicker, { Image } from 'react-native-image-crop-picker';
|
||||
import { connect } from 'react-redux';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { deleteRoom } from '../../actions/room';
|
||||
import { themes } from '../../lib/constants';
|
||||
import Avatar from '../../containers/Avatar';
|
||||
import { AvatarWithEdit } from '../../containers/Avatar';
|
||||
import { sendLoadingEvent } from '../../containers/Loading';
|
||||
import SafeAreaView from '../../containers/SafeAreaView';
|
||||
import StatusBar from '../../containers/StatusBar';
|
||||
|
@ -23,13 +21,11 @@ import {
|
|||
IRoomSettings,
|
||||
ISubscription,
|
||||
SubscriptionType,
|
||||
TSubscriptionModel,
|
||||
IAvatar
|
||||
TSubscriptionModel
|
||||
} from '../../definitions';
|
||||
import { ERoomType } from '../../definitions/ERoomType';
|
||||
import I18n from '../../i18n';
|
||||
import database from '../../lib/database';
|
||||
import { CustomIcon } from '../../containers/CustomIcon';
|
||||
import KeyboardView from '../../containers/KeyboardView';
|
||||
import { TSupportedPermissions } from '../../reducers/permissions';
|
||||
import { ModalStackParamList } from '../../stacks/MasterDetailStack/types';
|
||||
|
@ -54,7 +50,6 @@ import { Services } from '../../lib/services';
|
|||
|
||||
interface IRoomInfoEditViewState {
|
||||
room: ISubscription;
|
||||
avatar: IAvatar;
|
||||
permissions: { [key in TSupportedPermissions]?: boolean };
|
||||
name: string;
|
||||
description?: string;
|
||||
|
@ -102,7 +97,6 @@ class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfo
|
|||
this.room = {} as TSubscriptionModel;
|
||||
this.state = {
|
||||
room: {} as ISubscription,
|
||||
avatar: {} as IAvatar,
|
||||
permissions: {},
|
||||
name: '',
|
||||
description: '',
|
||||
|
@ -192,7 +186,6 @@ class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfo
|
|||
topic,
|
||||
announcement,
|
||||
t: t === 'p',
|
||||
avatar: {} as IAvatar,
|
||||
ro,
|
||||
reactWhenReadOnly,
|
||||
joinCode: joinCodeRequired ? this.randomValue : '',
|
||||
|
@ -228,8 +221,7 @@ class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfo
|
|||
joinCode,
|
||||
systemMessages,
|
||||
enableSysMes,
|
||||
encrypted,
|
||||
avatar
|
||||
encrypted
|
||||
} = this.state;
|
||||
const { joinCodeRequired } = room;
|
||||
const sysMes = room.sysMes as string[];
|
||||
|
@ -244,28 +236,15 @@ class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfo
|
|||
room.reactWhenReadOnly === reactWhenReadOnly &&
|
||||
dequal(sysMes, systemMessages) &&
|
||||
enableSysMes === (sysMes && sysMes.length > 0) &&
|
||||
room.encrypted === encrypted &&
|
||||
isEmpty(avatar)
|
||||
room.encrypted === encrypted
|
||||
);
|
||||
};
|
||||
|
||||
submit = async () => {
|
||||
logEvent(events.RI_EDIT_SAVE);
|
||||
Keyboard.dismiss();
|
||||
const {
|
||||
room,
|
||||
name,
|
||||
description,
|
||||
topic,
|
||||
announcement,
|
||||
t,
|
||||
ro,
|
||||
reactWhenReadOnly,
|
||||
joinCode,
|
||||
systemMessages,
|
||||
encrypted,
|
||||
avatar
|
||||
} = this.state;
|
||||
const { room, name, description, topic, announcement, t, ro, reactWhenReadOnly, joinCode, systemMessages, encrypted } =
|
||||
this.state;
|
||||
|
||||
sendLoadingEvent({ visible: true });
|
||||
let error = false;
|
||||
|
@ -284,10 +263,6 @@ class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfo
|
|||
if (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;
|
||||
|
|
|
@ -76,7 +76,8 @@ export default StyleSheet.create({
|
|||
avatarContainer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 10
|
||||
marginBottom: 32,
|
||||
marginTop: 16
|
||||
},
|
||||
resetButton: {
|
||||
padding: 4,
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -21,13 +21,14 @@ export default StyleSheet.create({
|
|||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 20,
|
||||
paddingVertical: 8
|
||||
paddingBottom: 8,
|
||||
paddingTop: 32
|
||||
},
|
||||
avatar: {
|
||||
marginHorizontal: 10
|
||||
},
|
||||
roomTitleContainer: {
|
||||
paddingTop: 20,
|
||||
paddingTop: 32,
|
||||
marginHorizontal: 16,
|
||||
alignItems: 'center'
|
||||
},
|
||||
|
|
|
@ -24,6 +24,7 @@ const styles = StyleSheet.create({
|
|||
});
|
||||
|
||||
interface ILeftButtonsProps {
|
||||
rid?: string;
|
||||
tmid?: string;
|
||||
unreadsCount: number | null;
|
||||
navigation: StackNavigationProp<ChatsStackParamList, 'RoomView'>;
|
||||
|
@ -38,6 +39,7 @@ interface ILeftButtonsProps {
|
|||
}
|
||||
|
||||
const LeftButtons = ({
|
||||
rid,
|
||||
tmid,
|
||||
unreadsCount,
|
||||
navigation,
|
||||
|
@ -78,7 +80,7 @@ const LeftButtons = ({
|
|||
}
|
||||
|
||||
if (baseUrl && userId && token) {
|
||||
return <Avatar text={title} size={30} type={t} style={styles.avatar} onPress={onPress} />;
|
||||
return <Avatar rid={rid} text={title} size={30} type={t} style={styles.avatar} onPress={onPress} />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
|
|
@ -589,6 +589,7 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
|
|||
headerRightContainerStyle: { flexGrow: undefined, flexBasis: undefined },
|
||||
headerLeft: () => (
|
||||
<LeftButtons
|
||||
rid={rid}
|
||||
tmid={tmid}
|
||||
unreadsCount={unreadsCount}
|
||||
navigation={navigation}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue