[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 { Object {
"headers": undefined, "headers": undefined,
"priority": "high", "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={ style={
@ -189,7 +254,7 @@ exports[`Storyshots Avatar list Avatar 1`] = `
Object { Object {
"headers": undefined, "headers": undefined,
"priority": "high", "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={ style={
@ -254,7 +319,7 @@ exports[`Storyshots Avatar list Avatar 1`] = `
Object { Object {
"headers": undefined, "headers": undefined,
"priority": "high", "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={ style={
@ -319,7 +384,7 @@ exports[`Storyshots Avatar list Avatar 1`] = `
Object { Object {
"headers": undefined, "headers": undefined,
"priority": "high", "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={ style={
@ -454,7 +519,7 @@ exports[`Storyshots Avatar list Avatar 1`] = `
Object { Object {
"headers": undefined, "headers": undefined,
"priority": "high", "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={ style={
@ -519,7 +584,7 @@ exports[`Storyshots Avatar list Avatar 1`] = `
Object { Object {
"headers": undefined, "headers": undefined,
"priority": "high", "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={ style={
@ -679,7 +744,7 @@ exports[`Storyshots Avatar list Avatar 1`] = `
Object { Object {
"headers": undefined, "headers": undefined,
"priority": "high", "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={ style={
@ -744,7 +809,7 @@ exports[`Storyshots Avatar list Avatar 1`] = `
Object { Object {
"headers": undefined, "headers": undefined,
"priority": "high", "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={ style={
@ -835,7 +900,7 @@ exports[`Storyshots Avatar list Avatar 1`] = `
Object { Object {
"headers": undefined, "headers": undefined,
"priority": "high", "priority": "high",
"uri": "https://google.com/avatar/Avatar?format=png&size=50", "uri": "https://google.com/avatar/Avatar?format=png&size=100",
} }
} }
style={ style={
@ -902,7 +967,7 @@ exports[`Storyshots Avatar list Avatar 1`] = `
Object { Object {
"headers": undefined, "headers": undefined,
"priority": "high", "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={ style={

View File

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

View File

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

View File

@ -70,13 +70,13 @@ const NotifierComponent = React.memo(({ notification, isMasterDetail }) => {
const { isLandscape } = useOrientation(); const { isLandscape } = useOrientation();
const { text, payload } = notification; const { text, payload } = notification;
const { type } = payload; const { type, rid } = payload;
const name = type === 'd' ? payload.sender.username : payload.name; 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 // 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 { title = name, avatar = name } = notification;
const onPress = () => { const onPress = () => {
const { rid, prid } = payload; const { prid } = payload;
if (!rid) { if (!rid) {
return; return;
} }
@ -111,7 +111,7 @@ const NotifierComponent = React.memo(({ notification, isMasterDetail }) => {
background={Touchable.SelectableBackgroundBorderless()} 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}> <View style={styles.inner}>
<Text style={[styles.roomName, { color: themes[theme].titleText }]} numberOfLines={1}>{title}</Text> <Text style={[styles.roomName, { color: themes[theme].titleText }]} numberOfLines={1}>{title}</Text>
<Text style={[styles.message, { color: themes[theme].titleText }]} numberOfLines={1}>{text}</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_announcement',
'room_changed_topic', 'room_changed_topic',
'room_changed_privacy', 'room_changed_privacy',
'room_changed_avatar',
'message_snippeted', 'message_snippeted',
'thread-created' 'thread-created'
]; ];
@ -91,6 +92,8 @@ export const getInfoMessage = ({
return I18n.t('Room_changed_topic', { topic: msg, userBy: username }); return I18n.t('Room_changed_topic', { topic: msg, userBy: username });
} else if (type === 'room_changed_privacy') { } else if (type === 'room_changed_privacy') {
return I18n.t('Room_changed_privacy', { type: msg, userBy: username }); 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') { } else if (type === 'message_snippeted') {
return I18n.t('Created_snippet'); return I18n.t('Created_snippet');
} }

View File

@ -437,6 +437,7 @@ export default {
Roles: 'Roles', Roles: 'Roles',
Room_actions: 'Room actions', Room_actions: 'Room actions',
Room_changed_announcement: 'Room announcement changed to: {{announcement}} by {{userBy}}', 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_description: 'Room description changed to: {{description}} by {{userBy}}',
Room_changed_privacy: 'Room type changed to: {{type}} by {{userBy}}', Room_changed_privacy: 'Room type changed to: {{type}} by {{userBy}}',
Room_changed_topic: 'Room topic changed to: {{topic}} 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('livechat_data', sanitizer) livechatData;
@json('tags', sanitizer) tags; @json('tags', sanitizer) tags;
@field('avatar_etag') avatarETag;
} }

View File

@ -115,4 +115,6 @@ export default class Subscription extends Model {
@field('encrypted') encrypted; @field('encrypted') encrypted;
@field('e2e_key_id') e2eKeyId; @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: 'username', type: 'string', isIndexed: true },
{ name: 'avatar_etag', type: 'string', isOptional: 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: 'tags', type: 'string', isOptional: true },
{ name: 'e2e_key', type: 'string', isOptional: true }, { name: 'e2e_key', type: 'string', isOptional: true },
{ name: 'encrypted', type: 'boolean', 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({ tableSchema({
@ -67,7 +68,8 @@ export default appSchema({
{ name: 'served_by', type: 'string', isOptional: true }, { name: 'served_by', type: 'string', isOptional: true },
{ name: 'livechat_data', type: 'string', isOptional: true }, { name: 'livechat_data', type: 'string', isOptional: true },
{ name: 'tags', 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({ tableSchema({

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,12 +1,18 @@
const formatUrl = (url, size, query) => `${ url }?format=png&size=${ size }${ query }`; const formatUrl = (url, size, query) => `${ url }?format=png&size=${ size }${ query }`;
export const avatarURL = ({ 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 ? size : 100;
const uriSize = size === 100 ? 100 : 50;
const { id, token } = user; const { id, token } = user;
let query = ''; let query = '';

View File

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

View File

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

View File

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

View File

@ -6,7 +6,9 @@ import {
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import equal from 'deep-equal'; import equal from 'deep-equal';
import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit'; import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
import ImagePicker from 'react-native-image-crop-picker';
import isEqual from 'lodash/isEqual'; import isEqual from 'lodash/isEqual';
import isEmpty from 'lodash/isEmpty';
import semver from 'semver'; import semver from 'semver';
import database from '../../lib/database'; import database from '../../lib/database';
@ -31,6 +33,8 @@ import { withTheme } from '../../theme';
import { MultiSelect } from '../../containers/UIKit/MultiSelect'; import { MultiSelect } from '../../containers/UIKit/MultiSelect';
import { MessageTypeValues } from '../../utils/messageTypes'; import { MessageTypeValues } from '../../utils/messageTypes';
import SafeAreaView from '../../containers/SafeAreaView'; import SafeAreaView from '../../containers/SafeAreaView';
import Avatar from '../../containers/Avatar';
import { CustomIcon } from '../../lib/Icons';
const PERMISSION_SET_READONLY = 'set-readonly'; const PERMISSION_SET_READONLY = 'set-readonly';
const PERMISSION_SET_REACT_WHEN_READONLY = 'set-react-when-readonly'; const PERMISSION_SET_REACT_WHEN_READONLY = 'set-react-when-readonly';
@ -64,6 +68,7 @@ class RoomInfoEditView extends React.Component {
super(props); super(props);
this.state = { this.state = {
room: {}, room: {},
avatar: {},
permissions: {}, permissions: {},
name: '', name: '',
description: '', description: '',
@ -136,6 +141,7 @@ class RoomInfoEditView extends React.Component {
topic, topic,
announcement, announcement,
t: t === 'p', t: t === 'p',
avatar: {},
ro, ro,
reactWhenReadOnly, reactWhenReadOnly,
joinCode: joinCodeRequired ? this.randomValue : '', joinCode: joinCodeRequired ? this.randomValue : '',
@ -160,7 +166,7 @@ class RoomInfoEditView extends React.Component {
formIsChanged = () => { formIsChanged = () => {
const { 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; } = this.state;
const { joinCodeRequired } = room; const { joinCodeRequired } = room;
return !(room.name === name return !(room.name === name
@ -174,6 +180,7 @@ class RoomInfoEditView extends React.Component {
&& isEqual(room.sysMes, systemMessages) && isEqual(room.sysMes, systemMessages)
&& enableSysMes === (room.sysMes && room.sysMes.length > 0) && enableSysMes === (room.sysMes && room.sysMes.length > 0)
&& room.encrypted === encrypted && room.encrypted === encrypted
&& isEmpty(avatar)
); );
} }
@ -181,7 +188,7 @@ class RoomInfoEditView extends React.Component {
logEvent(events.RI_EDIT_SAVE); logEvent(events.RI_EDIT_SAVE);
Keyboard.dismiss(); Keyboard.dismiss();
const { const {
room, name, description, topic, announcement, t, ro, reactWhenReadOnly, joinCode, systemMessages, encrypted room, name, description, topic, announcement, t, ro, reactWhenReadOnly, joinCode, systemMessages, encrypted, avatar
} = this.state; } = this.state;
this.setState({ saving: true }); this.setState({ saving: true });
@ -201,6 +208,10 @@ class RoomInfoEditView extends React.Component {
if (room.name !== name) { if (room.name !== name) {
params.roomName = name; params.roomName = name;
} }
// Avatar
if (!isEmpty(avatar)) {
params.roomAvatar = avatar.data;
}
// Description // Description
if (room.description !== description) { if (room.description !== description) {
params.roomDescription = 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) => { toggleRoomType = (value) => {
logEvent(events.RI_EDIT_TOGGLE_ROOM_TYPE); logEvent(events.RI_EDIT_TOGGLE_ROOM_TYPE);
this.setState(({ encrypted }) => ({ t: value, encrypted: value && encrypted })); this.setState(({ encrypted }) => ({ t: value, encrypted: value && encrypted }));
@ -374,7 +407,7 @@ class RoomInfoEditView extends React.Component {
render() { render() {
const { 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; } = this.state;
const { serverVersion, e2eEnabled, theme } = this.props; const { serverVersion, e2eEnabled, theme } = this.props;
const { dangerColor } = themes[theme]; const { dangerColor } = themes[theme];
@ -396,6 +429,20 @@ class RoomInfoEditView extends React.Component {
testID='room-info-edit-view-list' testID='room-info-edit-view-list'
{...scrollPersistTaps} {...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 <RCTextInput
inputRef={(e) => { this.name = e; }} inputRef={(e) => { this.name = e; }}
label={I18n.t('Name')} label={I18n.t('Name')}

View File

@ -72,5 +72,17 @@ export default StyleSheet.create({
}, },
switchMargin: { switchMargin: {
marginBottom: 16 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} style={styles.avatar}
type={this.t} type={this.t}
size={100} size={100}
rid={room?.rid}
> >
{this.t === 'd' && roomUser._id ? <Status style={[sharedStyles.status, styles.status]} theme={theme} size={24} id={roomUser._id} /> : null} {this.t === 'd' && roomUser._id ? <Status style={[sharedStyles.status, styles.status]} theme={theme} size={24} id={roomUser._id} /> : null}
</Avatar> </Avatar>

View File

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

View File

@ -35,6 +35,13 @@ const AvatarStories = ({ theme }) => (
server={server} server={server}
size={56} 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} /> <Separator title='Avatar by url' theme={theme} />
<Avatar <Avatar
avatar='https://user-images.githubusercontent.com/29778115/89444446-14738480-d728-11ea-9412-75fd978d95fb.jpg' 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" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
reselect@^4.0.0: reselect@4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7" resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7"
integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA== integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==