[NEW] Channel avatars (#2504)

* [WIP] Avatar cache invalidation

* [WIP] Avatar container

* [IMPROVEMENT] Avatar container

* [CHORE] Improve code

* Allow static image on Avatar

* Fix avatar changing while change username (#1583)

Co-authored-by: Prateek93a <prateek93a@gmail.com>

* Add default props to properly update on Sidebar and ProfileView

* Fix subscribing on the wrong moment

* Storyshots update

* RoomItem using Avatar Component

* use iife to unsubscribe from user

* Use component on avatar container

* RoomItem as a React.Component

* Move servers models to servers folder

* Avatar -> AvatarContainer

* Users indexed fields

* Initialize author and check if u is present

* Not was found -> User not found (turn comments more relevant)

* RoomItemInner -> Wrapper

* Revert Avatar Touchable logic

* Revert responsability of LeftButton on Tablet Mode

* Prevent setState on constructor

* Run avatarURL only when its not static

* Add streams RC Version

* Move entire add user logic to result.success

* Reorder init on RoomItem

* onPress as a class function

* Fix roomItem using same username

* Add avatar Stories

* Fix pick an image from gallery on ProfileView

* Format Avatar URL to use RoomId.

Co-authored-by: Ezequiel De Oliveira <ezequiel1de1oliveira@gmail.com>

* edit room avatar

* invalidate cache of room images

* reinit avatar if something change

* read avatar cache on search

* room avatar changed system message

* add avatar by rid test

* update snapshot

* etag cache on select channel

* reset room avatar

* increase caching to have a better image quality

* fix lgtm warn

* invalidate ci cache

* get avatar etag on select users of create discussion

* invalidate ci cache

* Fix migration

* Fix sidebar avatar not updating

* Remove outdated comment

* Tests

Co-authored-by: Prateek93a <prateek93a@gmail.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>
Co-authored-by: Ezequiel De Oliveira <ezequiel1de1oliveira@gmail.com>
This commit is contained in:
Djorkaeff Alexandre 2020-10-30 10:51:04 -03:00 committed by GitHub
parent 734039191f
commit 46e3db97e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 282 additions and 70 deletions

View File

@ -59,7 +59,72 @@ exports[`Storyshots Avatar list Avatar 1`] = `
Object {
"headers": undefined,
"priority": "high",
"uri": "https://open.rocket.chat/avatar/Avatar?format=png&size=50",
"uri": "https://open.rocket.chat/avatar/Avatar?format=png&size=100",
}
}
style={
Object {
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
</View>
</View>
<Text
style={
Array [
Object {
"fontSize": 20,
"fontWeight": "300",
"marginLeft": 10,
"marginVertical": 30,
},
Object {
"color": "#0d0e12",
},
undefined,
]
}
>
Avatar by roomId
</Text>
<View
style={
Array [
Object {
"borderRadius": 4,
"height": 56,
"width": 56,
},
undefined,
]
}
>
<View
style={
Array [
Object {
"overflow": "hidden",
},
Object {
"borderRadius": 4,
"height": 56,
"width": 56,
},
]
}
>
<FastImageView
resizeMode="cover"
source={
Object {
"headers": undefined,
"priority": "high",
"uri": "https://open.rocket.chat/avatar/room/devWBbYr7inwupPqK?format=png&size=100",
}
}
style={
@ -189,7 +254,7 @@ exports[`Storyshots Avatar list Avatar 1`] = `
Object {
"headers": undefined,
"priority": "high",
"uri": "https://open.rocket.chat/avatar/diego.mello?format=png&size=50",
"uri": "https://open.rocket.chat/avatar/diego.mello?format=png&size=100",
}
}
style={
@ -254,7 +319,7 @@ exports[`Storyshots Avatar list Avatar 1`] = `
Object {
"headers": undefined,
"priority": "high",
"uri": "https://open.rocket.chat/avatar/djorkaeff.alexandre?format=png&size=50&etag=5ag8KffJcZj9m5rCv",
"uri": "https://open.rocket.chat/avatar/djorkaeff.alexandre?format=png&size=100&etag=5ag8KffJcZj9m5rCv",
}
}
style={
@ -319,7 +384,7 @@ exports[`Storyshots Avatar list Avatar 1`] = `
Object {
"headers": undefined,
"priority": "high",
"uri": "https://open.rocket.chat/avatar/djorkaeff.alexandre?format=png&size=50",
"uri": "https://open.rocket.chat/avatar/djorkaeff.alexandre?format=png&size=100",
}
}
style={
@ -454,7 +519,7 @@ exports[`Storyshots Avatar list Avatar 1`] = `
Object {
"headers": undefined,
"priority": "high",
"uri": "https://open.rocket.chat/avatar/diego.mello?format=png&size=50",
"uri": "https://open.rocket.chat/avatar/diego.mello?format=png&size=100",
}
}
style={
@ -519,7 +584,7 @@ exports[`Storyshots Avatar list Avatar 1`] = `
Object {
"headers": undefined,
"priority": "high",
"uri": "https://open.rocket.chat/avatar/@general?format=png&size=50",
"uri": "https://open.rocket.chat/avatar/@general?format=png&size=100",
}
}
style={
@ -679,7 +744,7 @@ exports[`Storyshots Avatar list Avatar 1`] = `
Object {
"headers": undefined,
"priority": "high",
"uri": "https://open.rocket.chat/avatar/Avatar?format=png&size=50",
"uri": "https://open.rocket.chat/avatar/Avatar?format=png&size=100",
}
}
style={
@ -744,7 +809,7 @@ exports[`Storyshots Avatar list Avatar 1`] = `
Object {
"headers": undefined,
"priority": "high",
"uri": "https://open.rocket.chat/avatar/Avatar?format=png&size=50",
"uri": "https://open.rocket.chat/avatar/Avatar?format=png&size=100",
}
}
style={
@ -835,7 +900,7 @@ exports[`Storyshots Avatar list Avatar 1`] = `
Object {
"headers": undefined,
"priority": "high",
"uri": "https://google.com/avatar/Avatar?format=png&size=50",
"uri": "https://google.com/avatar/Avatar?format=png&size=100",
}
}
style={
@ -902,7 +967,7 @@ exports[`Storyshots Avatar list Avatar 1`] = `
Object {
"headers": undefined,
"priority": "high",
"uri": "https://open.rocket.chat/avatar/Avatar?format=png&size=50",
"uri": "https://open.rocket.chat/avatar/Avatar?format=png&size=100",
}
}
style={

View File

@ -23,9 +23,10 @@ const Avatar = React.memo(({
theme,
getCustomEmoji,
avatarETag,
isStatic
isStatic,
rid
}) => {
if ((!text && !avatar && !emoji) || !server) {
if ((!text && !avatar && !emoji && !rid) || !server) {
return null;
}
@ -57,7 +58,8 @@ const Avatar = React.memo(({
user,
avatar,
server,
avatarETag
avatarETag,
rid
});
}
@ -108,7 +110,8 @@ Avatar.propTypes = {
onPress: PropTypes.func,
getCustomEmoji: PropTypes.func,
avatarETag: PropTypes.string,
isStatic: PropTypes.bool
isStatic: PropTypes.bool,
rid: PropTypes.string
};
Avatar.defaultProps = {

View File

@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Q } from '@nozbe/watermelondb';
import isEqual from 'react-fast-compare';
import database from '../../lib/database';
import { getUserSelector } from '../../selectors/login';
@ -9,6 +10,7 @@ import Avatar from './Avatar';
class AvatarContainer extends React.Component {
static propTypes = {
rid: PropTypes.string,
text: PropTypes.string,
type: PropTypes.string
};
@ -29,9 +31,15 @@ class AvatarContainer extends React.Component {
this.mounted = true;
}
componentDidUpdate(prevProps) {
if (!isEqual(prevProps, this.props)) {
this.init();
}
}
componentWillUnmount() {
if (this.userSubscription?.unsubscribe) {
this.userSubscription.unsubscribe();
if (this.subscription?.unsubscribe) {
this.subscription.unsubscribe();
}
}
@ -41,16 +49,28 @@ class AvatarContainer extends React.Component {
}
init = async() => {
if (this.isDirect) {
const { text } = this.props;
const db = database.active;
const usersCollection = db.collections.get('users');
const subsCollection = db.collections.get('subscriptions');
let record;
try {
if (this.isDirect) {
const { text } = this.props;
const [user] = await usersCollection.query(Q.where('username', text)).fetch();
if (user) {
const observable = user.observe();
this.userSubscription = observable.subscribe((u) => {
const { avatarETag } = u;
record = user;
} else {
const { rid } = this.props;
record = await subsCollection.find(rid);
}
} catch {
// Record not found
}
if (record) {
const observable = record.observe();
this.subscription = observable.subscribe((r) => {
const { avatarETag } = r;
if (this.mounted) {
this.setState({ avatarETag });
} else {
@ -58,10 +78,6 @@ class AvatarContainer extends React.Component {
}
});
}
} catch {
// User was not found
}
}
}
render() {

View File

@ -70,13 +70,13 @@ const NotifierComponent = React.memo(({ notification, isMasterDetail }) => {
const { isLandscape } = useOrientation();
const { text, payload } = notification;
const { type } = payload;
const { type, rid } = payload;
const name = type === 'd' ? payload.sender.username : payload.name;
// if sub is not on local database, title and avatar will be null, so we use payload from notification
const { title = name, avatar = name } = notification;
const onPress = () => {
const { rid, prid } = payload;
const { prid } = payload;
if (!rid) {
return;
}
@ -111,7 +111,7 @@ const NotifierComponent = React.memo(({ notification, isMasterDetail }) => {
background={Touchable.SelectableBackgroundBorderless()}
>
<>
<Avatar text={avatar} size={AVATAR_SIZE} type={type} style={styles.avatar} />
<Avatar text={avatar} size={AVATAR_SIZE} type={type} rid={rid} style={styles.avatar} />
<View style={styles.inner}>
<Text style={[styles.roomName, { color: themes[theme].titleText }]} numberOfLines={1}>{title}</Text>
<Text style={[styles.message, { color: themes[theme].titleText }]} numberOfLines={1}>{text}</Text>

View File

@ -49,6 +49,7 @@ export const SYSTEM_MESSAGES = [
'room_changed_announcement',
'room_changed_topic',
'room_changed_privacy',
'room_changed_avatar',
'message_snippeted',
'thread-created'
];
@ -91,6 +92,8 @@ export const getInfoMessage = ({
return I18n.t('Room_changed_topic', { topic: msg, userBy: username });
} else if (type === 'room_changed_privacy') {
return I18n.t('Room_changed_privacy', { type: msg, userBy: username });
} else if (type === 'room_changed_avatar') {
return I18n.t('Room_changed_avatar', { userBy: username });
} else if (type === 'message_snippeted') {
return I18n.t('Created_snippet');
}

View File

@ -437,6 +437,7 @@ export default {
Roles: 'Roles',
Room_actions: 'Room actions',
Room_changed_announcement: 'Room announcement changed to: {{announcement}} by {{userBy}}',
Room_changed_avatar: 'Room avatar changed by {{userBy}}',
Room_changed_description: 'Room description changed to: {{description}} by {{userBy}}',
Room_changed_privacy: 'Room type changed to: {{type}} by {{userBy}}',
Room_changed_topic: 'Room topic changed to: {{topic}} by {{userBy}}',

View File

@ -25,4 +25,6 @@ export default class Room extends Model {
@json('livechat_data', sanitizer) livechatData;
@json('tags', sanitizer) tags;
@field('avatar_etag') avatarETag;
}

View File

@ -115,4 +115,6 @@ export default class Subscription extends Model {
@field('encrypted') encrypted;
@field('e2e_key_id') e2eKeyId;
@field('avatar_etag') avatarETag;
}

View File

@ -178,6 +178,18 @@ export default schemaMigrations({
{ name: 'username', type: 'string', isIndexed: true },
{ name: 'avatar_etag', type: 'string', isOptional: true }
]
}),
addColumns({
table: 'subscriptions',
columns: [
{ name: 'avatar_etag', type: 'string', isOptional: true }
]
}),
addColumns({
table: 'rooms',
columns: [
{ name: 'avatar_etag', type: 'string', isOptional: true }
]
})
]
}

View File

@ -52,7 +52,8 @@ export default appSchema({
{ name: 'tags', type: 'string', isOptional: true },
{ name: 'e2e_key', type: 'string', isOptional: true },
{ name: 'encrypted', type: 'boolean', isOptional: true },
{ name: 'e2e_key_id', type: 'string', isOptional: true }
{ name: 'e2e_key_id', type: 'string', isOptional: true },
{ name: 'avatar_etag', type: 'string', isOptional: true }
]
}),
tableSchema({
@ -67,7 +68,8 @@ export default appSchema({
{ name: 'served_by', type: 'string', isOptional: true },
{ name: 'livechat_data', type: 'string', isOptional: true },
{ name: 'tags', type: 'string', isOptional: true },
{ name: 'e2e_key_id', type: 'string', isOptional: true }
{ name: 'e2e_key_id', type: 'string', isOptional: true },
{ name: 'avatar_etag', type: 'string', isOptional: true }
]
}),
tableSchema({

View File

@ -52,7 +52,8 @@ export default async(subscriptions = [], rooms = []) => {
tags: s.tags,
encrypted: s.encrypted,
e2eKeyId: s.e2eKeyId,
E2EKey: s.E2EKey
E2EKey: s.E2EKey,
avatarETag: s.avatarETag
}));
subscriptions = subscriptions.concat(existingSubs);
@ -80,7 +81,8 @@ export default async(subscriptions = [], rooms = []) => {
livechatData: r.livechatData,
tags: r.tags,
encrypted: r.encrypted,
e2eKeyId: r.e2eKeyId
e2eKeyId: r.e2eKeyId,
avatarETag: r.avatarETag
}));
rooms = rooms.concat(existingRooms);
} catch {

View File

@ -30,6 +30,7 @@ export const merge = (subscription, room) => {
subscription.broadcast = room.broadcast;
subscription.encrypted = room.encrypted;
subscription.e2eKeyId = room.e2eKeyId;
subscription.avatarETag = room.avatarETag;
if (!subscription.roles || !subscription.roles.length) {
subscription.roles = [];
}

View File

@ -84,7 +84,8 @@ const createOrUpdateSubscription = async(subscription, room) => {
tags: s.tags,
encrypted: s.encrypted,
e2eKeyId: s.e2eKeyId,
E2EKey: s.E2EKey
E2EKey: s.E2EKey,
avatarETag: s.avatarETag
};
} catch (error) {
try {
@ -116,7 +117,8 @@ const createOrUpdateSubscription = async(subscription, room) => {
broadcast: r.broadcast,
customFields: r.customFields,
departmentId: r.departmentId,
livechatData: r.livechatData
livechatData: r.livechatData,
avatarETag: r.avatarETag
};
} catch (error) {
// Do nothing

View File

@ -579,6 +579,7 @@ const RocketChat = {
rid: sub.rid,
name: sub.name,
fname: sub.fname,
avatarETag: sub.avatarETag,
t: sub.t,
search: true
});

View File

@ -18,7 +18,7 @@ const DirectoryItemLabel = React.memo(({ text, theme }) => {
});
const DirectoryItem = ({
title, description, avatar, onPress, testID, style, rightLabel, type, theme
title, description, avatar, onPress, testID, style, rightLabel, type, rid, theme
}) => (
<Touch
onPress={onPress}
@ -31,6 +31,7 @@ const DirectoryItem = ({
text={avatar}
size={30}
type={type}
rid={rid}
style={styles.directoryItemAvatar}
/>
<View style={styles.directoryItemTextContainer}>
@ -54,6 +55,7 @@ DirectoryItem.propTypes = {
testID: PropTypes.string.isRequired,
style: PropTypes.any,
rightLabel: PropTypes.string,
rid: PropTypes.string,
theme: PropTypes.string
};

View File

@ -73,6 +73,7 @@ const RoomItem = ({
userId={userId}
token={token}
theme={theme}
rid={rid}
>
{showLastMessage
? (

View File

@ -16,6 +16,7 @@ const Wrapper = ({
userId,
token,
theme,
rid,
children
}) => (
<View
@ -30,6 +31,7 @@ const Wrapper = ({
server={baseUrl}
user={{ id: userId, token }}
avatarETag={avatarETag}
rid={rid}
/>
<View
style={[
@ -54,6 +56,7 @@ Wrapper.propTypes = {
userId: PropTypes.string,
token: PropTypes.string,
theme: PropTypes.string,
rid: PropTypes.string,
children: PropTypes.element
};

View File

@ -88,8 +88,8 @@ class RoomItemContainer extends React.Component {
}
componentWillUnmount() {
if (this.userSubscription?.unsubscribe) {
this.userSubscription.unsubscribe();
if (this.avatarSubscription?.unsubscribe) {
this.avatarSubscription.unsubscribe();
}
if (this.roomSubscription?.unsubscribe) {
this.roomSubscription.unsubscribe();
@ -115,14 +115,23 @@ class RoomItemContainer extends React.Component {
});
}
if (this.isDirect) {
const { id } = this.props;
const db = database.active;
const usersCollection = db.collections.get('users');
const subsCollection = db.collections.get('subscriptions');
try {
const user = await usersCollection.find(id);
const observable = user.observe();
this.userSubscription = observable.subscribe((u) => {
const { id } = this.props;
const { rid } = item;
let record;
if (this.isDirect) {
record = await usersCollection.find(id);
} else {
record = await subsCollection.find(rid);
}
if (record) {
const observable = record.observe();
this.avatarSubscription = observable.subscribe((u) => {
const { avatarETag } = u;
if (this.mounted) {
this.setState({ avatarETag });
@ -130,9 +139,9 @@ class RoomItemContainer extends React.Component {
this.state.avatarETag = avatarETag;
}
});
} catch {
// User not found
}
} catch {
// Record not found
}
}
@ -220,7 +229,7 @@ class RoomItemContainer extends React.Component {
useRealName={useRealName}
unread={item.unread}
groupMentions={item.groupMentions}
avatarETag={avatarETag}
avatarETag={avatarETag || item.avatarETag}
swipeEnabled={swipeEnabled}
/>
);

View File

@ -1,12 +1,18 @@
const formatUrl = (url, size, query) => `${ url }?format=png&size=${ size }${ query }`;
export const avatarURL = ({
type, text, size, user = {}, avatar, server, avatarETag
type, text, size, user = {}, avatar, server, avatarETag, rid
}) => {
const room = type === 'd' ? text : `@${ text }`;
let room;
if (type === 'd') {
room = text;
} else if (rid) {
room = `room/${ rid }`;
} else {
room = `@${ text }`;
}
// Avoid requesting several sizes by having only two sizes on cache
const uriSize = size === 100 ? 100 : 50;
const uriSize = size > 100 ? size : 100;
const { id, token } = user;
let query = '';

View File

@ -25,8 +25,13 @@ const SelectChannel = ({
}
}, 300);
const getAvatar = (text, type) => avatarURL({
text, type, user: { id: userId, token }, server
const getAvatar = item => avatarURL({
text: RocketChat.getRoomAvatar(item),
type: item.t,
user: { id: userId, token },
server,
avatarETag: item.avatarETag,
rid: item.rid
});
return (
@ -42,7 +47,7 @@ const SelectChannel = ({
options={channels.map(channel => ({
value: channel.rid,
text: { text: RocketChat.getRoomTitle(channel) },
imageUrl: getAvatar(RocketChat.getRoomAvatar(channel), channel.t)
imageUrl: getAvatar(channel)
}))}
onClose={() => setChannels([])}
placeholder={{ text: `${ I18n.t('Select_a_Channel') }...` }}

View File

@ -205,7 +205,8 @@ class DirectoryView extends React.Component {
testID: `federation-view-item-${ item.name }`,
style,
user,
theme
theme,
rid: item._id
};
if (type === 'users') {

View File

@ -669,7 +669,12 @@ class RoomActionsView extends React.Component {
renderRoomInfo = ({ item }) => {
const { room, member } = this.state;
const { name, t, topic } = room;
const {
name,
t,
rid,
topic
} = room;
const { theme } = this.props;
const avatar = RocketChat.getRoomAvatar(room);
@ -682,6 +687,7 @@ class RoomActionsView extends React.Component {
style={styles.avatar}
size={50}
type={t}
rid={rid}
>
{t === 'd' && member._id ? <Status style={sharedStyles.status} id={member._id} /> : null }
</Avatar>

View File

@ -6,7 +6,9 @@ import {
import { connect } from 'react-redux';
import equal from 'deep-equal';
import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
import ImagePicker from 'react-native-image-crop-picker';
import isEqual from 'lodash/isEqual';
import isEmpty from 'lodash/isEmpty';
import semver from 'semver';
import database from '../../lib/database';
@ -31,6 +33,8 @@ import { withTheme } from '../../theme';
import { MultiSelect } from '../../containers/UIKit/MultiSelect';
import { MessageTypeValues } from '../../utils/messageTypes';
import SafeAreaView from '../../containers/SafeAreaView';
import Avatar from '../../containers/Avatar';
import { CustomIcon } from '../../lib/Icons';
const PERMISSION_SET_READONLY = 'set-readonly';
const PERMISSION_SET_REACT_WHEN_READONLY = 'set-react-when-readonly';
@ -64,6 +68,7 @@ class RoomInfoEditView extends React.Component {
super(props);
this.state = {
room: {},
avatar: {},
permissions: {},
name: '',
description: '',
@ -136,6 +141,7 @@ class RoomInfoEditView extends React.Component {
topic,
announcement,
t: t === 'p',
avatar: {},
ro,
reactWhenReadOnly,
joinCode: joinCodeRequired ? this.randomValue : '',
@ -160,7 +166,7 @@ class RoomInfoEditView extends React.Component {
formIsChanged = () => {
const {
room, name, description, topic, announcement, t, ro, reactWhenReadOnly, joinCode, systemMessages, enableSysMes, encrypted
room, name, description, topic, announcement, t, ro, reactWhenReadOnly, joinCode, systemMessages, enableSysMes, encrypted, avatar
} = this.state;
const { joinCodeRequired } = room;
return !(room.name === name
@ -174,6 +180,7 @@ class RoomInfoEditView extends React.Component {
&& isEqual(room.sysMes, systemMessages)
&& enableSysMes === (room.sysMes && room.sysMes.length > 0)
&& room.encrypted === encrypted
&& isEmpty(avatar)
);
}
@ -181,7 +188,7 @@ class RoomInfoEditView extends React.Component {
logEvent(events.RI_EDIT_SAVE);
Keyboard.dismiss();
const {
room, name, description, topic, announcement, t, ro, reactWhenReadOnly, joinCode, systemMessages, encrypted
room, name, description, topic, announcement, t, ro, reactWhenReadOnly, joinCode, systemMessages, encrypted, avatar
} = this.state;
this.setState({ saving: true });
@ -201,6 +208,10 @@ class RoomInfoEditView extends React.Component {
if (room.name !== name) {
params.roomName = name;
}
// Avatar
if (!isEmpty(avatar)) {
params.roomAvatar = avatar.data;
}
// Description
if (room.description !== description) {
params.roomDescription = description;
@ -347,6 +358,28 @@ class RoomInfoEditView extends React.Component {
);
}
changeAvatar = async() => {
const options = {
cropping: true,
compressImageQuality: 0.8,
cropperAvoidEmptySpaceAroundImage: false,
cropperChooseText: I18n.t('Choose'),
cropperCancelText: I18n.t('Cancel'),
includeBase64: true
};
try {
const response = await ImagePicker.openPicker(options);
this.setState({ avatar: { url: response.path, data: `data:image/jpeg;base64,${ response.data }`, service: 'upload' } });
} catch (e) {
console.log(e);
}
}
resetAvatar = () => {
this.setState({ avatar: { data: null } });
}
toggleRoomType = (value) => {
logEvent(events.RI_EDIT_TOGGLE_ROOM_TYPE);
this.setState(({ encrypted }) => ({ t: value, encrypted: value && encrypted }));
@ -374,7 +407,7 @@ class RoomInfoEditView extends React.Component {
render() {
const {
name, nameError, description, topic, announcement, t, ro, reactWhenReadOnly, room, joinCode, saving, permissions, archived, enableSysMes, encrypted
name, nameError, description, topic, announcement, t, ro, reactWhenReadOnly, room, joinCode, saving, permissions, archived, enableSysMes, encrypted, avatar
} = this.state;
const { serverVersion, e2eEnabled, theme } = this.props;
const { dangerColor } = themes[theme];
@ -396,6 +429,20 @@ class RoomInfoEditView extends React.Component {
testID='room-info-edit-view-list'
{...scrollPersistTaps}
>
<TouchableOpacity style={styles.avatarContainer} onPress={this.changeAvatar}>
<Avatar
type={room.t}
text={room.name}
avatar={avatar?.url}
isStatic={avatar?.url}
rid={isEmpty(avatar) && room.rid}
size={100}
>
<TouchableOpacity style={[styles.resetButton, { backgroundColor: themes[theme].dangerColor }]} onPress={this.resetAvatar}>
<CustomIcon name='delete' color={themes[theme].backgroundColor} size={24} />
</TouchableOpacity>
</Avatar>
</TouchableOpacity>
<RCTextInput
inputRef={(e) => { this.name = e; }}
label={I18n.t('Name')}

View File

@ -72,5 +72,17 @@ export default StyleSheet.create({
},
switchMargin: {
marginBottom: 16
},
avatarContainer: {
alignItems: 'center',
justifyContent: 'center',
marginBottom: 10
},
resetButton: {
padding: 4,
borderRadius: 4,
position: 'absolute',
bottom: -8,
right: -8
}
});

View File

@ -289,6 +289,7 @@ class RoomInfoView extends React.Component {
style={styles.avatar}
type={this.t}
size={100}
rid={room?.rid}
>
{this.t === 'd' && roomUser._id ? <Status style={[sharedStyles.status, styles.status]} theme={theme} size={24} id={roomUser._id} /> : null}
</Avatar>

View File

@ -116,7 +116,7 @@
"redux-immutable-state-invariant": "2.1.0",
"redux-saga": "1.1.3",
"remove-markdown": "^0.3.0",
"reselect": "^4.0.0",
"reselect": "4.0.0",
"rn-extensions-share": "^2.4.0",
"rn-fetch-blob": "0.12.0",
"rn-root-view": "^1.0.3",

View File

@ -35,6 +35,13 @@ const AvatarStories = ({ theme }) => (
server={server}
size={56}
/>
<Separator title='Avatar by roomId' theme={theme} />
<Avatar
type='p'
rid='devWBbYr7inwupPqK'
server={server}
size={56}
/>
<Separator title='Avatar by url' theme={theme} />
<Avatar
avatar='https://user-images.githubusercontent.com/29778115/89444446-14738480-d728-11ea-9412-75fd978d95fb.jpg'

View File

@ -13560,7 +13560,7 @@ requires-port@^1.0.0:
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
reselect@^4.0.0:
reselect@4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7"
integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==