[FIX] Avatar cache invalidation (#2311)

* [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

* get avatar etag on select users of create discussion

* invalidate ci cache

* Fix migration

* Fix sidebar avatar not updating

Co-authored-by: Prateek93a <prateek93a@gmail.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>
This commit is contained in:
Djorkaeff Alexandre 2020-10-30 10:12:02 -03:00 committed by GitHub
parent b8474286a8
commit 734039191f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 21840 additions and 20471 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,87 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View } from 'react-native';
import FastImage from '@rocket.chat/react-native-fast-image';
import Touchable from 'react-native-platform-touchable';
import { settings as RocketChatSettings } from '@rocket.chat/sdk';
import { avatarURL } from '../utils/avatar';
import Emoji from './markdown/Emoji';
const Avatar = React.memo(({
text, size, baseUrl, borderRadius, style, avatar, type, children, userId, token, onPress, theme, emoji, getCustomEmoji
}) => {
const avatarStyle = {
width: size,
height: size,
borderRadius
};
if (!text && !avatar) {
return null;
}
const uri = avatarURL({
type, text, size, userId, token, avatar, baseUrl
});
let image = emoji ? (
<Emoji
theme={theme}
baseUrl={baseUrl}
getCustomEmoji={getCustomEmoji}
isMessageContainsOnlyEmoji
literal={emoji}
/>
) : (
<FastImage
style={avatarStyle}
source={{
uri,
headers: RocketChatSettings.customHeaders,
priority: FastImage.priority.high
}}
/>
);
if (onPress) {
image = (
<Touchable onPress={onPress}>
{image}
</Touchable>
);
}
return (
<View style={[avatarStyle, style]}>
{image}
{children}
</View>
);
});
Avatar.propTypes = {
baseUrl: PropTypes.string.isRequired,
style: PropTypes.any,
text: PropTypes.string,
avatar: PropTypes.string,
emoji: PropTypes.string,
size: PropTypes.number,
borderRadius: PropTypes.number,
type: PropTypes.string,
children: PropTypes.object,
userId: PropTypes.string,
token: PropTypes.string,
theme: PropTypes.string,
onPress: PropTypes.func,
getCustomEmoji: PropTypes.func
};
Avatar.defaultProps = {
text: '',
size: 25,
type: 'd',
borderRadius: 4
};
export default Avatar;

View File

@ -0,0 +1,121 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View } from 'react-native';
import FastImage from '@rocket.chat/react-native-fast-image';
import Touchable from 'react-native-platform-touchable';
import { settings as RocketChatSettings } from '@rocket.chat/sdk';
import { avatarURL } from '../../utils/avatar';
import Emoji from '../markdown/Emoji';
const Avatar = React.memo(({
text,
size,
server,
borderRadius,
style,
avatar,
type,
children,
user,
onPress,
emoji,
theme,
getCustomEmoji,
avatarETag,
isStatic
}) => {
if ((!text && !avatar && !emoji) || !server) {
return null;
}
const avatarStyle = {
width: size,
height: size,
borderRadius
};
let image;
if (emoji) {
image = (
<Emoji
theme={theme}
baseUrl={server}
getCustomEmoji={getCustomEmoji}
isMessageContainsOnlyEmoji
literal={emoji}
style={avatarStyle}
/>
);
} else {
let uri = avatar;
if (!isStatic) {
uri = avatarURL({
type,
text,
size,
user,
avatar,
server,
avatarETag
});
}
image = (
<FastImage
style={avatarStyle}
source={{
uri,
headers: RocketChatSettings.customHeaders,
priority: FastImage.priority.high
}}
/>
);
}
if (onPress) {
image = (
<Touchable onPress={onPress}>
{image}
</Touchable>
);
}
return (
<View style={[avatarStyle, style]}>
{image}
{children}
</View>
);
});
Avatar.propTypes = {
server: PropTypes.string,
style: PropTypes.any,
text: PropTypes.string,
avatar: PropTypes.string,
emoji: PropTypes.string,
size: PropTypes.number,
borderRadius: PropTypes.number,
type: PropTypes.string,
children: PropTypes.object,
user: PropTypes.shape({
id: PropTypes.string,
token: PropTypes.string
}),
theme: PropTypes.string,
onPress: PropTypes.func,
getCustomEmoji: PropTypes.func,
avatarETag: PropTypes.string,
isStatic: PropTypes.bool
};
Avatar.defaultProps = {
text: '',
size: 25,
type: 'd',
borderRadius: 4
};
export default Avatar;

View File

@ -0,0 +1,82 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Q } from '@nozbe/watermelondb';
import database from '../../lib/database';
import { getUserSelector } from '../../selectors/login';
import Avatar from './Avatar';
class AvatarContainer extends React.Component {
static propTypes = {
text: PropTypes.string,
type: PropTypes.string
};
static defaultProps = {
text: '',
type: 'd'
};
constructor(props) {
super(props);
this.mounted = false;
this.state = { avatarETag: '' };
this.init();
}
componentDidMount() {
this.mounted = true;
}
componentWillUnmount() {
if (this.userSubscription?.unsubscribe) {
this.userSubscription.unsubscribe();
}
}
get isDirect() {
const { type } = this.props;
return type === 'd';
}
init = async() => {
if (this.isDirect) {
const { text } = this.props;
const db = database.active;
const usersCollection = db.collections.get('users');
try {
const [user] = await usersCollection.query(Q.where('username', text)).fetch();
if (user) {
const observable = user.observe();
this.userSubscription = observable.subscribe((u) => {
const { avatarETag } = u;
if (this.mounted) {
this.setState({ avatarETag });
} else {
this.state.avatarETag = avatarETag;
}
});
}
} catch {
// User was not found
}
}
}
render() {
const { avatarETag } = this.state;
return (
<Avatar
avatarETag={avatarETag}
{...this.props}
/>
);
}
}
const mapStateToProps = state => ({
user: getUserSelector(state),
server: state.share.server || state.server.server
});
export default connect(mapStateToProps)(AvatarContainer);

View File

@ -11,7 +11,6 @@ import { CustomIcon } from '../../lib/Icons';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { useTheme } from '../../theme'; import { useTheme } from '../../theme';
import { getUserSelector } from '../../selectors/login';
import { ROW_HEIGHT } from '../../presentation/RoomItem'; import { ROW_HEIGHT } from '../../presentation/RoomItem';
import { goRoom } from '../../utils/goRoom'; import { goRoom } from '../../utils/goRoom';
import Navigation from '../../lib/Navigation'; import Navigation from '../../lib/Navigation';
@ -65,14 +64,11 @@ const styles = StyleSheet.create({
const hideNotification = () => Notifier.hideNotification(); const hideNotification = () => Notifier.hideNotification();
const NotifierComponent = React.memo(({ const NotifierComponent = React.memo(({ notification, isMasterDetail }) => {
baseUrl, user, notification, isMasterDetail
}) => {
const { theme } = useTheme(); const { theme } = useTheme();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const { isLandscape } = useOrientation(); const { isLandscape } = useOrientation();
const { id: userId, token } = user;
const { text, payload } = notification; const { text, payload } = notification;
const { type } = payload; const { type } = payload;
const name = type === 'd' ? payload.sender.username : payload.name; const name = type === 'd' ? payload.sender.username : payload.name;
@ -115,7 +111,7 @@ const NotifierComponent = React.memo(({
background={Touchable.SelectableBackgroundBorderless()} background={Touchable.SelectableBackgroundBorderless()}
> >
<> <>
<Avatar text={avatar} size={AVATAR_SIZE} type={type} baseUrl={baseUrl} style={styles.avatar} userId={userId} token={token} /> <Avatar text={avatar} size={AVATAR_SIZE} type={type} 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>
@ -134,15 +130,11 @@ const NotifierComponent = React.memo(({
}); });
NotifierComponent.propTypes = { NotifierComponent.propTypes = {
baseUrl: PropTypes.string,
user: PropTypes.object,
notification: PropTypes.object, notification: PropTypes.object,
isMasterDetail: PropTypes.bool isMasterDetail: PropTypes.bool
}; };
const mapStateToProps = state => ({ const mapStateToProps = state => ({
user: getUserSelector(state),
baseUrl: state.server.server,
isMasterDetail: state.app.isMasterDetail isMasterDetail: state.app.isMasterDetail
}); });

View File

@ -17,7 +17,7 @@ const MentionItem = ({
item, trackingType, theme item, trackingType, theme
}) => { }) => {
const context = useContext(MessageboxContext); const context = useContext(MessageboxContext);
const { baseUrl, user, onPressMention } = context; const { onPressMention } = context;
const defineTestID = (type) => { const defineTestID = (type) => {
switch (type) { switch (type) {
@ -43,9 +43,6 @@ const MentionItem = ({
text={item.username || item.name} text={item.username || item.name}
size={30} size={30}
type={item.t} type={item.t}
baseUrl={baseUrl}
userId={user.id}
token={user.token}
/> />
<Text style={[styles.mentionText, { color: themes[theme].titleText }]}>{ item.username || item.name || item }</Text> <Text style={[styles.mentionText, { color: themes[theme].titleText }]}>{ item.username || item.name || item }</Text>
</> </>

View File

@ -9,7 +9,7 @@ import { themes } from '../../constants/colors';
import styles from './styles'; import styles from './styles';
const Emoji = React.memo(({ const Emoji = React.memo(({
literal, isMessageContainsOnlyEmoji, getCustomEmoji, baseUrl, customEmojis = true, style = [], theme literal, isMessageContainsOnlyEmoji, getCustomEmoji, baseUrl, customEmojis = true, style = {}, theme
}) => { }) => {
const emojiUnicode = shortnameToUnicode(literal); const emojiUnicode = shortnameToUnicode(literal);
const emoji = getCustomEmoji && getCustomEmoji(literal.replace(/:/g, '')); const emoji = getCustomEmoji && getCustomEmoji(literal.replace(/:/g, ''));
@ -17,7 +17,10 @@ const Emoji = React.memo(({
return ( return (
<CustomEmoji <CustomEmoji
baseUrl={baseUrl} baseUrl={baseUrl}
style={isMessageContainsOnlyEmoji ? styles.customEmojiBig : styles.customEmoji} style={[
isMessageContainsOnlyEmoji ? styles.customEmojiBig : styles.customEmoji,
style
]}
emoji={emoji} emoji={emoji}
/> />
); );
@ -27,7 +30,7 @@ const Emoji = React.memo(({
style={[ style={[
{ color: themes[theme].bodyText }, { color: themes[theme].bodyText },
isMessageContainsOnlyEmoji ? styles.textBig : styles.text, isMessageContainsOnlyEmoji ? styles.textBig : styles.text,
...style style
]} ]}
> >
{emojiUnicode} {emojiUnicode}
@ -41,7 +44,7 @@ Emoji.propTypes = {
getCustomEmoji: PropTypes.func, getCustomEmoji: PropTypes.func,
baseUrl: PropTypes.string, baseUrl: PropTypes.string,
customEmojis: PropTypes.bool, customEmojis: PropTypes.bool,
style: PropTypes.array, style: PropTypes.object,
theme: PropTypes.string theme: PropTypes.string
}; };

View File

@ -1,7 +1,7 @@
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Avatar from '../Avatar'; import Avatar from '../Avatar/Avatar';
import styles from './styles'; import styles from './styles';
import MessageContext from './Context'; import MessageContext from './Context';
@ -22,11 +22,11 @@ const MessageAvatar = React.memo(({
borderRadius={small ? 2 : 4} borderRadius={small ? 2 : 4}
onPress={author._id === user.id ? undefined : () => navToRoomInfo(navParam)} onPress={author._id === user.id ? undefined : () => navToRoomInfo(navParam)}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
user={user}
server={baseUrl}
avatarETag={author.avatarETag}
avatar={avatar} avatar={avatar}
emoji={emoji} emoji={emoji}
baseUrl={baseUrl}
userId={user.id}
token={user.token}
theme={theme} theme={theme}
/> />
); );

View File

@ -6,9 +6,10 @@ import Message from './Message';
import MessageContext from './Context'; import MessageContext from './Context';
import debounce from '../../utils/debounce'; import debounce from '../../utils/debounce';
import { SYSTEM_MESSAGES, getMessageTranslation } from './utils'; import { SYSTEM_MESSAGES, getMessageTranslation } from './utils';
import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/encryption/constants';
import messagesStatus from '../../constants/messagesStatus'; import messagesStatus from '../../constants/messagesStatus';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/encryption/constants'; import database from '../../lib/database';
class MessageContainer extends React.Component { class MessageContainer extends React.Component {
static propTypes = { static propTypes = {
@ -72,7 +73,11 @@ class MessageContainer extends React.Component {
theme: 'light' theme: 'light'
} }
componentDidMount() { state = {
author: null
}
async componentDidMount() {
const { item } = this.props; const { item } = this.props;
if (item && item.observe) { if (item && item.observe) {
const observable = item.observe(); const observable = item.observe();
@ -80,6 +85,19 @@ class MessageContainer extends React.Component {
this.forceUpdate(); this.forceUpdate();
}); });
} }
const db = database.active;
const usersCollection = db.collections.get('users');
try {
const user = await usersCollection.find(item.u?._id);
const observable = user.observe();
this.userSubscription = observable.subscribe((author) => {
this.setState({ author });
this.forceUpdate();
});
} catch {
// Do nothing
}
} }
shouldComponentUpdate(nextProps) { shouldComponentUpdate(nextProps) {
@ -94,6 +112,9 @@ class MessageContainer extends React.Component {
if (this.subscription && this.subscription.unsubscribe) { if (this.subscription && this.subscription.unsubscribe) {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
} }
if (this.userSubscription && this.userSubscription.unsubscribe) {
this.userSubscription.unsubscribe();
}
} }
onPress = debounce(() => { onPress = debounce(() => {
@ -242,6 +263,7 @@ class MessageContainer extends React.Component {
} }
render() { render() {
const { author } = this.state;
const { const {
item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, customThreadTimeFormat, showAttachment, timeFormat, isReadReceiptEnabled, autoTranslateRoom, autoTranslateLanguage, navToRoomInfo, getCustomEmoji, isThreadRoom, callJitsi, blockAction, rid, theme item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, customThreadTimeFormat, showAttachment, timeFormat, isReadReceiptEnabled, autoTranslateRoom, autoTranslateLanguage, navToRoomInfo, getCustomEmoji, isThreadRoom, callJitsi, blockAction, rid, theme
} = this.props; } = this.props;
@ -276,7 +298,7 @@ class MessageContainer extends React.Component {
id={id} id={id}
msg={message} msg={message}
rid={rid} rid={rid}
author={u} author={author || u}
ts={ts} ts={ts}
type={t} type={t}
attachments={attachments} attachments={attachments}

View File

@ -15,15 +15,16 @@ import Role from './model/Role';
import Permission from './model/Permission'; import Permission from './model/Permission';
import SlashCommand from './model/SlashCommand'; import SlashCommand from './model/SlashCommand';
import User from './model/User'; import User from './model/User';
import Server from './model/Server';
import LoggedUser from './model/servers/User';
import Server from './model/servers/Server';
import ServersHistory from './model/ServersHistory'; import ServersHistory from './model/ServersHistory';
import serversSchema from './schema/servers'; import serversSchema from './schema/servers';
import appSchema from './schema/app'; import appSchema from './schema/app';
import migrations from './model/migrations'; import migrations from './model/migrations';
import serversMigrations from './model/servers/migrations';
import serversMigrations from './model/serversMigrations';
import { isIOS } from '../../utils/deviceInfo'; import { isIOS } from '../../utils/deviceInfo';
import appGroup from '../../utils/appGroup'; import appGroup from '../../utils/appGroup';
@ -58,7 +59,8 @@ export const getDatabase = (database = '') => {
Setting, Setting,
Role, Role,
Permission, Permission,
SlashCommand SlashCommand,
User
], ],
actionsEnabled: true actionsEnabled: true
}); });
@ -72,7 +74,7 @@ class DB {
schema: serversSchema, schema: serversSchema,
migrations: serversMigrations migrations: serversMigrations
}), }),
modelClasses: [Server, User, ServersHistory], modelClasses: [Server, LoggedUser, ServersHistory],
actionsEnabled: true actionsEnabled: true
}) })
} }
@ -113,7 +115,8 @@ class DB {
Upload, Upload,
Permission, Permission,
CustomEmoji, CustomEmoji,
FrequentlyUsedEmoji FrequentlyUsedEmoji,
User
], ],
actionsEnabled: true actionsEnabled: true
}); });

View File

@ -6,17 +6,13 @@ import { sanitizer } from '../utils';
export default class User extends Model { export default class User extends Model {
static table = 'users'; static table = 'users';
@field('token') token; @field('_id') _id;
@field('username') username;
@field('name') name; @field('name') name;
@field('language') language; @field('username') username;
@field('status') status; @field('avatar_etag') avatarETag;
@field('statusText') statusText;
@field('login_email_password') loginEmailPassword; @field('login_email_password') loginEmailPassword;

View File

@ -1,4 +1,4 @@
import { schemaMigrations, addColumns } from '@nozbe/watermelondb/Schema/migrations'; import { schemaMigrations, addColumns, createTable } from '@nozbe/watermelondb/Schema/migrations';
export default schemaMigrations({ export default schemaMigrations({
migrations: [ migrations: [
@ -166,6 +166,20 @@ export default schemaMigrations({
] ]
}) })
] ]
},
{
toVersion: 11,
steps: [
createTable({
name: 'users',
columns: [
{ name: '_id', type: 'string', isIndexed: true },
{ name: 'name', type: 'string', isOptional: true },
{ name: 'username', type: 'string', isIndexed: true },
{ name: 'avatar_etag', type: 'string', isOptional: true }
]
})
]
} }
] ]
}); });

View File

@ -0,0 +1,24 @@
import { Model } from '@nozbe/watermelondb';
import { field, json } from '@nozbe/watermelondb/decorators';
import { sanitizer } from '../../utils';
export default class User extends Model {
static table = 'users';
@field('token') token;
@field('username') username;
@field('name') name;
@field('language') language;
@field('status') status;
@field('statusText') statusText;
@json('roles', sanitizer) roles;
@field('avatar_etag') avatarETag;
}

View File

@ -83,6 +83,17 @@ export default schemaMigrations({
] ]
}) })
] ]
},
{
toVersion: 10,
steps: [
addColumns({
table: 'users',
columns: [
{ name: 'avatar_etag', type: 'string', isOptional: true }
]
})
]
} }
] ]
}); });

View File

@ -1,7 +1,7 @@
import { appSchema, tableSchema } from '@nozbe/watermelondb'; import { appSchema, tableSchema } from '@nozbe/watermelondb';
export default appSchema({ export default appSchema({
version: 10, version: 11,
tables: [ tables: [
tableSchema({ tableSchema({
name: 'subscriptions', name: 'subscriptions',
@ -247,6 +247,15 @@ export default appSchema({
{ name: 'provides_preview', type: 'boolean', isOptional: true }, { name: 'provides_preview', type: 'boolean', isOptional: true },
{ name: 'app_id', type: 'string', isOptional: true } { name: 'app_id', type: 'string', isOptional: true }
] ]
}),
tableSchema({
name: 'users',
columns: [
{ name: '_id', type: 'string', isIndexed: true },
{ name: 'name', type: 'string', isOptional: true },
{ name: 'username', type: 'string', isIndexed: true },
{ name: 'avatar_etag', type: 'string', isOptional: true }
]
}) })
] ]
}); });

View File

@ -1,7 +1,7 @@
import { appSchema, tableSchema } from '@nozbe/watermelondb'; import { appSchema, tableSchema } from '@nozbe/watermelondb';
export default appSchema({ export default appSchema({
version: 9, version: 10,
tables: [ tables: [
tableSchema({ tableSchema({
name: 'users', name: 'users',
@ -13,7 +13,8 @@ export default appSchema({
{ name: 'status', type: 'string', isOptional: true }, { name: 'status', type: 'string', isOptional: true },
{ name: 'statusText', type: 'string', isOptional: true }, { name: 'statusText', type: 'string', isOptional: true },
{ name: 'roles', type: 'string', isOptional: true }, { name: 'roles', type: 'string', isOptional: true },
{ name: 'login_email_password', type: 'boolean', isOptional: true } { name: 'login_email_password', type: 'boolean', isOptional: true },
{ name: 'avatar_etag', type: 'string', isOptional: true }
] ]
}), }),
tableSchema({ tableSchema({

View File

@ -1,9 +1,11 @@
import { InteractionManager } from 'react-native'; import { InteractionManager } from 'react-native';
import semver from 'semver'; import semver from 'semver';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import reduxStore from '../createStore'; import reduxStore from '../createStore';
import { setActiveUsers } from '../../actions/activeUsers'; import { setActiveUsers } from '../../actions/activeUsers';
import { setUser } from '../../actions/login'; import { setUser } from '../../actions/login';
import database from '../database';
export function subscribeUsersPresence() { export function subscribeUsersPresence() {
const serverVersion = reduxStore.getState().server.version; const serverVersion = reduxStore.getState().server.version;
@ -20,6 +22,11 @@ export function subscribeUsersPresence() {
} else { } else {
this.sdk.subscribe('stream-notify-logged', 'user-status'); this.sdk.subscribe('stream-notify-logged', 'user-status');
} }
// RC 0.49.1
this.sdk.subscribe('stream-notify-logged', 'updateAvatar');
// RC 0.58.0
this.sdk.subscribe('stream-notify-logged', 'Users:NameChanged');
} }
let ids = []; let ids = [];
@ -46,7 +53,9 @@ export default async function getUsersPresence() {
// RC 1.1.0 // RC 1.1.0
const result = await this.sdk.get('users.presence', params); const result = await this.sdk.get('users.presence', params);
if (result.success) { if (result.success) {
const activeUsers = result.users.reduce((ret, item) => { const { users } = result;
const activeUsers = users.reduce((ret, item) => {
const { _id, status, statusText } = item; const { _id, status, statusText } = item;
if (loggedUser && loggedUser.id === _id) { if (loggedUser && loggedUser.id === _id) {
@ -60,6 +69,27 @@ export default async function getUsersPresence() {
reduxStore.dispatch(setActiveUsers(activeUsers)); reduxStore.dispatch(setActiveUsers(activeUsers));
}); });
ids = []; ids = [];
const db = database.active;
const userCollection = db.collections.get('users');
users.forEach(async(user) => {
try {
const userRecord = await userCollection.find(user._id);
await db.action(async() => {
await userRecord.update((u) => {
Object.assign(u, user);
});
});
} catch (e) {
// User not found
await db.action(async() => {
await userCollection.create((u) => {
u._raw = sanitizedRaw({ id: user._id }, userCollection.schema);
Object.assign(u, user);
});
});
}
});
} }
} catch { } catch {
// do nothing // do nothing
@ -80,5 +110,7 @@ export function getUserPresence(uid) {
}, 2000); }, 2000);
} }
ids.push(uid); if (uid) {
ids.push(uid);
}
} }

View File

@ -6,6 +6,7 @@ import {
} from '@rocket.chat/sdk'; } from '@rocket.chat/sdk';
import { Q } from '@nozbe/watermelondb'; import { Q } from '@nozbe/watermelondb';
import AsyncStorage from '@react-native-community/async-storage'; import AsyncStorage from '@react-native-community/async-storage';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import RNFetchBlob from 'rn-fetch-blob'; import RNFetchBlob from 'rn-fetch-blob';
import reduxStore from './createStore'; import reduxStore from './createStore';
@ -244,9 +245,9 @@ const RocketChat = {
this.usersListener = this.sdk.onStreamData('users', protectedFunction(ddpMessage => RocketChat._setUser(ddpMessage))); this.usersListener = this.sdk.onStreamData('users', protectedFunction(ddpMessage => RocketChat._setUser(ddpMessage)));
this.notifyLoggedListener = this.sdk.onStreamData('stream-notify-logged', protectedFunction((ddpMessage) => { this.notifyLoggedListener = this.sdk.onStreamData('stream-notify-logged', protectedFunction(async(ddpMessage) => {
const { eventName } = ddpMessage.fields; const { eventName } = ddpMessage.fields;
if (eventName === 'user-status') { if (/user-status/.test(eventName)) {
this.activeUsers = this.activeUsers || {}; this.activeUsers = this.activeUsers || {};
if (!this._setUserTimer) { if (!this._setUserTimer) {
this._setUserTimer = setTimeout(() => { this._setUserTimer = setTimeout(() => {
@ -266,6 +267,40 @@ const RocketChat = {
if (loggedUser && loggedUser.id === id) { if (loggedUser && loggedUser.id === id) {
reduxStore.dispatch(setUser({ status: STATUSES[status], statusText })); reduxStore.dispatch(setUser({ status: STATUSES[status], statusText }));
} }
} else if (/updateAvatar/.test(eventName)) {
const { username, etag } = ddpMessage.fields.args[0];
const db = database.active;
const userCollection = db.collections.get('users');
try {
const [userRecord] = await userCollection.query(Q.where('username', Q.eq(username))).fetch();
await db.action(async() => {
await userRecord.update((u) => {
u.avatarETag = etag;
});
});
} catch {
// We can't create a new record since we don't receive the user._id
}
} else if (/Users:NameChanged/.test(eventName)) {
const userNameChanged = ddpMessage.fields.args[0];
const db = database.active;
const userCollection = db.collections.get('users');
try {
const userRecord = await userCollection.find(userNameChanged._id);
await db.action(async() => {
await userRecord.update((u) => {
Object.assign(u, userNameChanged);
});
});
} catch {
// User not found
await db.action(async() => {
await userCollection.create((u) => {
u._raw = sanitizedRaw({ id: userNameChanged._id }, userCollection.schema);
Object.assign(u, userNameChanged);
});
});
}
} }
})); }));
@ -441,6 +476,7 @@ const RocketChat = {
statusLivechat: result.me.statusLivechat, statusLivechat: result.me.statusLivechat,
emails: result.me.emails, emails: result.me.emails,
roles: result.me.roles, roles: result.me.roles,
avatarETag: result.me.avatarETag,
loginEmailPassword loginEmailPassword
}; };
return user; return user;

View File

@ -18,7 +18,7 @@ const DirectoryItemLabel = React.memo(({ text, theme }) => {
}); });
const DirectoryItem = ({ const DirectoryItem = ({
title, description, avatar, onPress, testID, style, baseUrl, user, rightLabel, type, theme title, description, avatar, onPress, testID, style, rightLabel, type, theme
}) => ( }) => (
<Touch <Touch
onPress={onPress} onPress={onPress}
@ -32,9 +32,6 @@ const DirectoryItem = ({
size={30} size={30}
type={type} type={type}
style={styles.directoryItemAvatar} style={styles.directoryItemAvatar}
baseUrl={baseUrl}
userId={user.id}
token={user.token}
/> />
<View style={styles.directoryItemTextContainer}> <View style={styles.directoryItemTextContainer}>
<View style={styles.directoryItemTextTitle}> <View style={styles.directoryItemTextTitle}>
@ -53,11 +50,6 @@ DirectoryItem.propTypes = {
description: PropTypes.string, description: PropTypes.string,
avatar: PropTypes.string, avatar: PropTypes.string,
type: PropTypes.string, type: PropTypes.string,
user: PropTypes.shape({
id: PropTypes.string,
token: PropTypes.string
}),
baseUrl: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired, onPress: PropTypes.func.isRequired,
testID: PropTypes.string.isRequired, testID: PropTypes.string.isRequired,
style: PropTypes.any, style: PropTypes.any,

View File

@ -45,7 +45,8 @@ const RoomItem = ({
onPress, onPress,
toggleFav, toggleFav,
toggleRead, toggleRead,
hideChannel hideChannel,
avatarETag
}) => ( }) => (
<Touchable <Touchable
onPress={onPress} onPress={onPress}
@ -66,6 +67,7 @@ const RoomItem = ({
accessibilityLabel={accessibilityLabel} accessibilityLabel={accessibilityLabel}
avatar={avatar} avatar={avatar}
avatarSize={avatarSize} avatarSize={avatarSize}
avatarETag={avatarETag}
type={type} type={type}
baseUrl={baseUrl} baseUrl={baseUrl}
userId={userId} userId={userId}
@ -178,7 +180,8 @@ RoomItem.propTypes = {
toggleFav: PropTypes.func, toggleFav: PropTypes.func,
toggleRead: PropTypes.func, toggleRead: PropTypes.func,
onPress: PropTypes.func, onPress: PropTypes.func,
hideChannel: PropTypes.func hideChannel: PropTypes.func,
avatarETag: PropTypes.string
}; };
RoomItem.defaultProps = { RoomItem.defaultProps = {

View File

@ -4,12 +4,13 @@ import PropTypes from 'prop-types';
import styles from './styles'; import styles from './styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import Avatar from '../../containers/Avatar'; import Avatar from '../../containers/Avatar/Avatar';
const RoomItemInner = ({ const Wrapper = ({
accessibilityLabel, accessibilityLabel,
avatar, avatar,
avatarSize, avatarSize,
avatarETag,
type, type,
baseUrl, baseUrl,
userId, userId,
@ -25,10 +26,10 @@ const RoomItemInner = ({
text={avatar} text={avatar}
size={avatarSize} size={avatarSize}
type={type} type={type}
baseUrl={baseUrl}
style={styles.avatar} style={styles.avatar}
userId={userId} server={baseUrl}
token={token} user={{ id: userId, token }}
avatarETag={avatarETag}
/> />
<View <View
style={[ style={[
@ -43,10 +44,11 @@ const RoomItemInner = ({
</View> </View>
); );
RoomItemInner.propTypes = { Wrapper.propTypes = {
accessibilityLabel: PropTypes.string, accessibilityLabel: PropTypes.string,
avatar: PropTypes.string, avatar: PropTypes.string,
avatarSize: PropTypes.number, avatarSize: PropTypes.number,
avatarETag: PropTypes.string,
type: PropTypes.string, type: PropTypes.string,
baseUrl: PropTypes.string, baseUrl: PropTypes.string,
userId: PropTypes.string, userId: PropTypes.string,
@ -55,4 +57,4 @@ RoomItemInner.propTypes = {
children: PropTypes.element children: PropTypes.element
}; };
export default RoomItemInner; export default Wrapper;

View File

@ -1,10 +1,11 @@
import React, { useEffect, useState } from 'react'; 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 I18n from '../../i18n'; import I18n from '../../i18n';
import { ROW_HEIGHT } from './styles'; import { ROW_HEIGHT } from './styles';
import { formatDate } from '../../utils/room'; import { formatDate } from '../../utils/room';
import database from '../../lib/database';
import RoomItem from './RoomItem'; import RoomItem from './RoomItem';
export { ROW_HEIGHT }; export { ROW_HEIGHT };
@ -19,156 +20,212 @@ const attrs = [
'showLastMessage' 'showLastMessage'
]; ];
const arePropsEqual = (oldProps, newProps) => attrs.every(key => oldProps[key] === newProps[key]); class RoomItemContainer extends React.Component {
static propTypes = {
item: PropTypes.object.isRequired,
baseUrl: PropTypes.string.isRequired,
showLastMessage: PropTypes.bool,
id: PropTypes.string,
onPress: PropTypes.func,
userId: PropTypes.string,
username: PropTypes.string,
token: PropTypes.string,
avatarSize: PropTypes.number,
testID: PropTypes.string,
width: PropTypes.number,
status: PropTypes.string,
toggleFav: PropTypes.func,
toggleRead: PropTypes.func,
hideChannel: PropTypes.func,
useRealName: PropTypes.bool,
getUserPresence: PropTypes.func,
connected: PropTypes.bool,
theme: PropTypes.string,
isFocused: PropTypes.bool,
getRoomTitle: PropTypes.func,
getRoomAvatar: PropTypes.func,
getIsGroupChat: PropTypes.func,
getIsRead: PropTypes.func,
swipeEnabled: PropTypes.bool
};
const RoomItemContainer = React.memo(({ static defaultProps = {
item, avatarSize: 48,
onPress, status: 'offline',
width, getUserPresence: () => {},
toggleFav, getRoomTitle: () => 'title',
toggleRead, getRoomAvatar: () => '',
hideChannel, getIsGroupChat: () => false,
testID, getIsRead: () => false,
avatarSize, swipeEnabled: true
baseUrl, }
userId,
username,
token,
id,
showLastMessage,
status,
useRealName,
getUserPresence,
connected,
theme,
isFocused,
getRoomTitle,
getRoomAvatar,
getIsGroupChat,
getIsRead,
swipeEnabled
}) => {
const [, setForceUpdate] = useState(1);
useEffect(() => { constructor(props) {
if (connected && item.t === 'd' && id) { super(props);
this.mounted = false;
this.state = { avatarETag: '' };
this.init();
}
componentDidMount() {
this.mounted = true;
}
shouldComponentUpdate(nextProps, nextState) {
const { avatarETag } = this.state;
if (nextState.avatarETag !== avatarETag) {
return true;
}
const { props } = this;
return !attrs.every(key => props[key] === nextProps[key]);
}
componentDidUpdate(prevProps) {
const { connected, getUserPresence, id } = this.props;
if (prevProps.connected !== connected && connected && this.isDirect) {
getUserPresence(id); getUserPresence(id);
} }
}, [connected]); }
useEffect(() => { componentWillUnmount() {
if (this.userSubscription?.unsubscribe) {
this.userSubscription.unsubscribe();
}
if (this.roomSubscription?.unsubscribe) {
this.roomSubscription.unsubscribe();
}
}
get isGroupChat() {
const { item, getIsGroupChat } = this.props;
return getIsGroupChat(item);
}
get isDirect() {
const { item: { t }, id } = this.props;
return t === 'd' && id && !this.isGroupChat;
}
init = async() => {
const { item } = this.props;
if (item?.observe) { if (item?.observe) {
const observable = item.observe(); const observable = item.observe();
const subscription = observable?.subscribe?.(() => { this.roomSubscription = observable?.subscribe?.(() => {
setForceUpdate(prevForceUpdate => prevForceUpdate + 1); this.forceUpdate();
}); });
return () => {
subscription?.unsubscribe?.();
};
} }
}, []);
const name = getRoomTitle(item); if (this.isDirect) {
const avatar = getRoomAvatar(item); const { id } = this.props;
const isGroupChat = getIsGroupChat(item); const db = database.active;
const isRead = getIsRead(item); const usersCollection = db.collections.get('users');
const _onPress = () => onPress(item); try {
const date = item.lastMessage?.ts && formatDate(item.lastMessage.ts); const user = await usersCollection.find(id);
const observable = user.observe();
let accessibilityLabel = name; this.userSubscription = observable.subscribe((u) => {
if (item.unread === 1) { const { avatarETag } = u;
accessibilityLabel += `, ${ item.unread } ${ I18n.t('alert') }`; if (this.mounted) {
} else if (item.unread > 1) { this.setState({ avatarETag });
accessibilityLabel += `, ${ item.unread } ${ I18n.t('alerts') }`; } else {
this.state.avatarETag = avatarETag;
}
});
} catch {
// User not found
}
}
} }
if (item.userMentions > 0) { onPress = () => {
accessibilityLabel += `, ${ I18n.t('you_were_mentioned') }`; const { item, onPress } = this.props;
return onPress(item);
} }
if (date) { render() {
accessibilityLabel += `, ${ I18n.t('last_message') } ${ date }`; const { avatarETag } = this.state;
const {
item,
getRoomTitle,
getRoomAvatar,
getIsRead,
width,
toggleFav,
toggleRead,
hideChannel,
testID,
theme,
isFocused,
avatarSize,
baseUrl,
userId,
token,
status,
showLastMessage,
username,
useRealName,
swipeEnabled
} = this.props;
const name = getRoomTitle(item);
const avatar = getRoomAvatar(item);
const isRead = getIsRead(item);
const date = item.lastMessage?.ts && formatDate(item.lastMessage.ts);
let accessibilityLabel = name;
if (item.unread === 1) {
accessibilityLabel += `, ${ item.unread } ${ I18n.t('alert') }`;
} else if (item.unread > 1) {
accessibilityLabel += `, ${ item.unread } ${ I18n.t('alerts') }`;
}
if (item.userMentions > 0) {
accessibilityLabel += `, ${ I18n.t('you_were_mentioned') }`;
}
if (date) {
accessibilityLabel += `, ${ I18n.t('last_message') } ${ date }`;
}
return (
<RoomItem
name={name}
avatar={avatar}
isGroupChat={this.isGroupChat}
isRead={isRead}
onPress={this.onPress}
date={date}
accessibilityLabel={accessibilityLabel}
userMentions={item.userMentions}
width={width}
favorite={item.f}
toggleFav={toggleFav}
rid={item.rid}
toggleRead={toggleRead}
hideChannel={hideChannel}
testID={testID}
type={item.t}
theme={theme}
isFocused={isFocused}
size={avatarSize}
baseUrl={baseUrl}
userId={userId}
token={token}
prid={item.prid}
status={status}
hideUnreadStatus={item.hideUnreadStatus}
alert={item.alert}
roomUpdatedAt={item.roomUpdatedAt}
lastMessage={item.lastMessage}
showLastMessage={showLastMessage}
username={username}
useRealName={useRealName}
unread={item.unread}
groupMentions={item.groupMentions}
avatarETag={avatarETag}
swipeEnabled={swipeEnabled}
/>
);
} }
}
return (
<RoomItem
name={name}
avatar={avatar}
isGroupChat={isGroupChat}
isRead={isRead}
onPress={_onPress}
date={date}
accessibilityLabel={accessibilityLabel}
userMentions={item.userMentions}
width={width}
favorite={item.f}
toggleFav={toggleFav}
rid={item.rid}
toggleRead={toggleRead}
hideChannel={hideChannel}
testID={testID}
type={item.t}
theme={theme}
isFocused={isFocused}
size={avatarSize}
baseUrl={baseUrl}
userId={userId}
token={token}
prid={item.prid}
status={status}
hideUnreadStatus={item.hideUnreadStatus}
alert={item.alert}
roomUpdatedAt={item.roomUpdatedAt}
lastMessage={item.lastMessage}
showLastMessage={showLastMessage}
username={username}
useRealName={useRealName}
unread={item.unread}
groupMentions={item.groupMentions}
swipeEnabled={swipeEnabled}
/>
);
}, arePropsEqual);
RoomItemContainer.propTypes = {
item: PropTypes.object.isRequired,
baseUrl: PropTypes.string.isRequired,
showLastMessage: PropTypes.bool,
id: PropTypes.string,
onPress: PropTypes.func,
userId: PropTypes.string,
username: PropTypes.string,
token: PropTypes.string,
avatarSize: PropTypes.number,
testID: PropTypes.string,
width: PropTypes.number,
status: PropTypes.string,
toggleFav: PropTypes.func,
toggleRead: PropTypes.func,
hideChannel: PropTypes.func,
useRealName: PropTypes.bool,
getUserPresence: PropTypes.func,
connected: PropTypes.bool,
theme: PropTypes.string,
isFocused: PropTypes.bool,
getRoomTitle: PropTypes.func,
getRoomAvatar: PropTypes.func,
getIsGroupChat: PropTypes.func,
getIsRead: PropTypes.func,
swipeEnabled: PropTypes.bool
};
RoomItemContainer.defaultProps = {
avatarSize: 48,
status: 'offline',
getUserPresence: () => {},
getRoomTitle: () => 'title',
getRoomAvatar: () => '',
getIsGroupChat: () => false,
getIsRead: () => false,
swipeEnabled: true
};
const mapStateToProps = (state, ownProps) => { const mapStateToProps = (state, ownProps) => {
let status = 'offline'; let status = 'offline';

View File

@ -42,7 +42,7 @@ const styles = StyleSheet.create({
}); });
const UserItem = ({ const UserItem = ({
name, username, onPress, testID, onLongPress, style, icon, baseUrl, user, theme name, username, onPress, testID, onLongPress, style, icon, theme
}) => ( }) => (
<Pressable <Pressable
onPress={onPress} onPress={onPress}
@ -58,7 +58,7 @@ const UserItem = ({
})} })}
> >
<View style={[styles.container, styles.button, style]}> <View style={[styles.container, styles.button, style]}>
<Avatar text={username} size={30} type='d' style={styles.avatar} baseUrl={baseUrl} userId={user.id} token={user.token} /> <Avatar text={username} size={30} style={styles.avatar} />
<View style={styles.textContainer}> <View style={styles.textContainer}>
<Text style={[styles.name, { color: themes[theme].titleText }]} numberOfLines={1}>{name}</Text> <Text style={[styles.name, { color: themes[theme].titleText }]} numberOfLines={1}>{name}</Text>
<Text style={[styles.username, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>@{username}</Text> <Text style={[styles.username, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>@{username}</Text>
@ -71,11 +71,6 @@ const UserItem = ({
UserItem.propTypes = { UserItem.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
username: PropTypes.string.isRequired, username: PropTypes.string.isRequired,
user: PropTypes.shape({
id: PropTypes.string,
token: PropTypes.string
}),
baseUrl: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired, onPress: PropTypes.func.isRequired,
testID: PropTypes.string.isRequired, testID: PropTypes.string.isRequired,
onLongPress: PropTypes.func, onLongPress: PropTypes.func,

View File

@ -178,7 +178,8 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) {
status: user.status, status: user.status,
statusText: user.statusText, statusText: user.statusText,
roles: user.roles, roles: user.roles,
loginEmailPassword: user.loginEmailPassword loginEmailPassword: user.loginEmailPassword,
avatarETag: user.avatarETag
}; };
yield serversDB.action(async() => { yield serversDB.action(async() => {
try { try {

View File

@ -87,7 +87,8 @@ const handleSelectServer = function* handleSelectServer({ server, version, fetch
language: userRecord.language, language: userRecord.language,
status: userRecord.status, status: userRecord.status,
statusText: userRecord.statusText, statusText: userRecord.statusText,
roles: userRecord.roles roles: userRecord.roles,
avatarETag: userRecord.avatarETag
}; };
} catch { } catch {
// search credentials on shared credentials (Experimental/Official) // search credentials on shared credentials (Experimental/Official)

View File

@ -1,27 +1,29 @@
const formatUrl = (url, baseUrl, uriSize, avatarAuthURLFragment) => ( const formatUrl = (url, size, query) => `${ url }?format=png&size=${ size }${ query }`;
`${ baseUrl }${ url }?format=png&size=${ uriSize }&${ avatarAuthURLFragment }`
);
export const avatarURL = ({ export const avatarURL = ({
type, text, size, userId, token, avatar, baseUrl type, text, size, user = {}, avatar, server, avatarETag
}) => { }) => {
const room = type === 'd' ? text : `@${ text }`; const room = type === 'd' ? text : `@${ text }`;
// Avoid requesting several sizes by having only two sizes on cache // Avoid requesting several sizes by having only two sizes on cache
const uriSize = size === 100 ? 100 : 50; const uriSize = size === 100 ? 100 : 50;
let avatarAuthURLFragment = ''; const { id, token } = user;
if (userId && token) { let query = '';
avatarAuthURLFragment = `&rc_token=${ token }&rc_uid=${ userId }`; if (id && token) {
query += `&rc_token=${ token }&rc_uid=${ id }`;
}
if (avatarETag) {
query += `&etag=${ avatarETag }`;
} }
let uri;
if (avatar) { if (avatar) {
uri = avatar.includes('http') ? avatar : formatUrl(avatar, baseUrl, uriSize, avatarAuthURLFragment); if (avatar.startsWith('http')) {
} else { return avatar;
uri = formatUrl(`/avatar/${ room }`, baseUrl, uriSize, avatarAuthURLFragment); }
return formatUrl(`${ server }${ avatar }`, uriSize, query);
} }
return uri; return formatUrl(`${ server }/avatar/${ room }`, uriSize, query);
}; };

View File

@ -26,7 +26,7 @@ const SelectChannel = ({
}, 300); }, 300);
const getAvatar = (text, type) => avatarURL({ const getAvatar = (text, type) => avatarURL({
text, type, userId, token, baseUrl: server text, type, user: { id: userId, token }, server
}); });
return ( return (

View File

@ -2,10 +2,12 @@ import React, { useState } from 'react';
import { Text } from 'react-native'; import { Text } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit'; import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
import { Q } from '@nozbe/watermelondb';
import debounce from '../../utils/debounce'; import debounce from '../../utils/debounce';
import { avatarURL } from '../../utils/avatar'; import { avatarURL } from '../../utils/avatar';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import database from '../../lib/database';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { MultiSelect } from '../../containers/UIKit/MultiSelect'; import { MultiSelect } from '../../containers/UIKit/MultiSelect';
@ -19,15 +21,34 @@ const SelectUsers = ({
const getUsers = debounce(async(keyword = '') => { const getUsers = debounce(async(keyword = '') => {
try { try {
const db = database.active;
const usersCollection = db.collections.get('users');
const res = await RocketChat.search({ text: keyword, filterRooms: false }); const res = await RocketChat.search({ text: keyword, filterRooms: false });
setUsers([...users.filter(u => selected.includes(u.name)), ...res.filter(r => !users.find(u => u.name === r.name))]); let items = [...users.filter(u => selected.includes(u.name)), ...res.filter(r => !users.find(u => u.name === r.name))];
const records = await usersCollection.query(Q.where('username', Q.oneOf(items.map(u => u.name)))).fetch();
items = items.map((item) => {
const index = records.findIndex(r => r.username === item.name);
if (index > -1) {
const record = records[index];
return {
uids: item.uids,
usernames: item.usernames,
prid: item.prid,
fname: item.fname,
name: item.name,
avatarETag: record.avatarETag
};
}
return item;
});
setUsers(items);
} catch { } catch {
// do nothing // do nothing
} }
}, 300); }, 300);
const getAvatar = text => avatarURL({ const getAvatar = item => avatarURL({
text, type: 'd', userId, token, baseUrl: server text: RocketChat.getRoomAvatar(item), type: 'd', user: { id: userId, token }, server, avatarETag: item.avatarETag
}); });
return ( return (
@ -41,7 +62,7 @@ const SelectUsers = ({
options={users.map(user => ({ options={users.map(user => ({
value: user.name, value: user.name,
text: { text: RocketChat.getRoomTitle(user) }, text: { text: RocketChat.getRoomTitle(user) },
imageUrl: getAvatar(RocketChat.getRoomAvatar(user)) imageUrl: getAvatar(user)
}))} }))}
onClose={() => setUsers(users.filter(u => selected.includes(u.name)))} onClose={() => setUsers(users.filter(u => selected.includes(u.name)))}
placeholder={{ text: `${ I18n.t('Select_Users') }...` }} placeholder={{ text: `${ I18n.t('Select_Users') }...` }}

View File

@ -6,7 +6,7 @@ import prompt from 'react-native-prompt-android';
import SHA256 from 'js-sha256'; import SHA256 from 'js-sha256';
import ImagePicker from 'react-native-image-crop-picker'; import ImagePicker from 'react-native-image-crop-picker';
import RNPickerSelect from 'react-native-picker-select'; import RNPickerSelect from 'react-native-picker-select';
import equal from 'deep-equal'; import { isEqual, omit } from 'lodash';
import Touch from '../../utils/touch'; import Touch from '../../utils/touch';
import KeyboardView from '../../presentation/KeyboardView'; import KeyboardView from '../../presentation/KeyboardView';
@ -84,16 +84,22 @@ class ProfileView extends React.Component {
UNSAFE_componentWillReceiveProps(nextProps) { UNSAFE_componentWillReceiveProps(nextProps) {
const { user } = this.props; const { user } = this.props;
if (!equal(user, nextProps.user)) { /*
* We need to ignore status because on Android ImagePicker
* changes the activity, so, the user status changes and
* it's resetting the avatar right after
* select some image from gallery.
*/
if (!isEqual(omit(user, ['status']), omit(nextProps.user, ['status']))) {
this.init(nextProps.user); this.init(nextProps.user);
} }
} }
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
if (!equal(nextState, this.state)) { if (!isEqual(nextState, this.state)) {
return true; return true;
} }
if (!equal(nextProps, this.props)) { if (!isEqual(nextProps, this.props)) {
return true; return true;
} }
return false; return false;
@ -324,7 +330,6 @@ class ProfileView extends React.Component {
const { avatarUrl, avatarSuggestions } = this.state; const { avatarUrl, avatarSuggestions } = this.state;
const { const {
user, user,
baseUrl,
theme, theme,
Accounts_AllowUserAvatarChange Accounts_AllowUserAvatarChange
} = this.props; } = this.props;
@ -332,7 +337,7 @@ class ProfileView extends React.Component {
return ( return (
<View style={styles.avatarButtons}> <View style={styles.avatarButtons}>
{this.renderAvatarButton({ {this.renderAvatarButton({
child: <Avatar text={`@${ user.username }`} size={50} baseUrl={baseUrl} userId={user.id} token={user.token} />, child: <Avatar text={`@${ user.username }`} size={50} />,
onPress: () => this.resetAvatar(), onPress: () => this.resetAvatar(),
disabled: !Accounts_AllowUserAvatarChange, disabled: !Accounts_AllowUserAvatarChange,
key: 'profile-view-reset-avatar' key: 'profile-view-reset-avatar'
@ -354,7 +359,7 @@ class ProfileView extends React.Component {
return this.renderAvatarButton({ return this.renderAvatarButton({
disabled: !Accounts_AllowUserAvatarChange, disabled: !Accounts_AllowUserAvatarChange,
key: `profile-view-avatar-${ service }`, key: `profile-view-avatar-${ service }`,
child: <Avatar avatar={url} size={50} baseUrl={baseUrl} userId={user.id} token={user.token} />, child: <Avatar avatar={url} size={50} />,
onPress: () => this.setAvatar({ onPress: () => this.setAvatar({
url, data: blob, service, contentType url, data: blob, service, contentType
}) })
@ -448,7 +453,6 @@ class ProfileView extends React.Component {
name, username, email, newPassword, avatarUrl, customFields, avatar, saving name, username, email, newPassword, avatarUrl, customFields, avatar, saving
} = this.state; } = this.state;
const { const {
baseUrl,
user, user,
theme, theme,
Accounts_AllowEmailChange, Accounts_AllowEmailChange,
@ -474,12 +478,10 @@ class ProfileView extends React.Component {
> >
<View style={styles.avatarContainer} testID='profile-view-avatar'> <View style={styles.avatarContainer} testID='profile-view-avatar'>
<Avatar <Avatar
text={username} text={user.username}
avatar={avatar && avatar.url} avatar={avatar?.url}
isStatic={avatar?.url}
size={100} size={100}
baseUrl={baseUrl}
userId={user.id}
token={user.token}
/> />
</View> </View>
<RCTextInput <RCTextInput

View File

@ -14,7 +14,6 @@ import RocketChat from '../../lib/rocketchat';
import StatusBar from '../../containers/StatusBar'; import StatusBar from '../../containers/StatusBar';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { getUserSelector } from '../../selectors/login';
import SafeAreaView from '../../containers/SafeAreaView'; import SafeAreaView from '../../containers/SafeAreaView';
class ReadReceiptView extends React.Component { class ReadReceiptView extends React.Component {
@ -31,8 +30,6 @@ class ReadReceiptView extends React.Component {
static propTypes = { static propTypes = {
route: PropTypes.object, route: PropTypes.object,
Message_TimeFormat: PropTypes.string, Message_TimeFormat: PropTypes.string,
baseUrl: PropTypes.string,
user: PropTypes.object,
theme: PropTypes.string theme: PropTypes.string
} }
@ -96,9 +93,7 @@ class ReadReceiptView extends React.Component {
} }
renderItem = ({ item }) => { renderItem = ({ item }) => {
const { const { Message_TimeFormat, theme } = this.props;
Message_TimeFormat, user: { id: userId, token }, baseUrl, theme
} = this.props;
const time = moment(item.ts).format(Message_TimeFormat); const time = moment(item.ts).format(Message_TimeFormat);
if (!item?.user?.username) { if (!item?.user?.username) {
return null; return null;
@ -108,9 +103,6 @@ class ReadReceiptView extends React.Component {
<Avatar <Avatar
text={item.user.username} text={item.user.username}
size={40} size={40}
baseUrl={baseUrl}
userId={userId}
token={token}
/> />
<View style={styles.infoContainer}> <View style={styles.infoContainer}>
<View style={styles.item}> <View style={styles.item}>
@ -168,9 +160,7 @@ class ReadReceiptView extends React.Component {
} }
const mapStateToProps = state => ({ const mapStateToProps = state => ({
Message_TimeFormat: state.settings.Message_TimeFormat, Message_TimeFormat: state.settings.Message_TimeFormat
baseUrl: state.server.server,
user: getUserSelector(state)
}); });
export default connect(mapStateToProps)(withTheme(ReadReceiptView)); export default connect(mapStateToProps)(withTheme(ReadReceiptView));

View File

@ -670,7 +670,7 @@ 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, topic } = room;
const { baseUrl, user, theme } = this.props; const { theme } = this.props;
const avatar = RocketChat.getRoomAvatar(room); const avatar = RocketChat.getRoomAvatar(room);
@ -679,12 +679,9 @@ class RoomActionsView extends React.Component {
<> <>
<Avatar <Avatar
text={avatar} text={avatar}
size={50}
style={styles.avatar} style={styles.avatar}
size={50}
type={t} type={t}
baseUrl={baseUrl}
userId={user.id}
token={user.token}
> >
{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

@ -20,7 +20,6 @@ import StatusBar from '../../containers/StatusBar';
import log, { logEvent, events } from '../../utils/log'; import log, { logEvent, events } from '../../utils/log';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import { getUserSelector } from '../../selectors/login';
import Markdown from '../../containers/markdown'; import Markdown from '../../containers/markdown';
import { LISTENER } from '../../containers/Toast'; import { LISTENER } from '../../containers/Toast';
import EventEmitter from '../../utils/events'; import EventEmitter from '../../utils/events';
@ -54,11 +53,6 @@ class RoomInfoView extends React.Component {
static propTypes = { static propTypes = {
navigation: PropTypes.object, navigation: PropTypes.object,
route: PropTypes.object, route: PropTypes.object,
user: PropTypes.shape({
id: PropTypes.string,
token: PropTypes.string
}),
baseUrl: PropTypes.string,
rooms: PropTypes.array, rooms: PropTypes.array,
theme: PropTypes.string, theme: PropTypes.string,
isMasterDetail: PropTypes.bool, isMasterDetail: PropTypes.bool,
@ -287,17 +281,14 @@ class RoomInfoView extends React.Component {
} }
renderAvatar = (room, roomUser) => { renderAvatar = (room, roomUser) => {
const { baseUrl, user, theme } = this.props; const { theme } = this.props;
return ( return (
<Avatar <Avatar
text={room.name || roomUser.username} text={room.name || roomUser.username}
size={100}
style={styles.avatar} style={styles.avatar}
type={this.t} type={this.t}
baseUrl={baseUrl} size={100}
userId={user.id}
token={user.token}
> >
{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>
@ -377,8 +368,6 @@ class RoomInfoView extends React.Component {
} }
const mapStateToProps = state => ({ const mapStateToProps = state => ({
baseUrl: state.server.server,
user: getUserSelector(state),
rooms: state.room.rooms, rooms: state.room.rooms,
isMasterDetail: state.app.isMasterDetail, isMasterDetail: state.app.isMasterDetail,
jitsiEnabled: state.settings.Jitsi_Enabled || false jitsiEnabled: state.settings.Jitsi_Enabled || false

View File

@ -32,16 +32,14 @@ const LeftButtons = React.memo(({
); );
} }
const onPress = useCallback(() => goRoomActionsView(), []); const onPress = useCallback(() => goRoomActionsView(), []);
if (baseUrl && userId && token) { if (baseUrl && userId && token) {
return ( return (
<Avatar <Avatar
text={title} text={title}
size={30} size={30}
type={t} type={t}
baseUrl={baseUrl}
style={styles.avatar} style={styles.avatar}
userId={userId}
token={token}
onPress={onPress} onPress={onPress}
/> />
); );

View File

@ -5,6 +5,7 @@ import {
} from 'react-native'; } from 'react-native';
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 Avatar from '../../containers/Avatar'; import Avatar from '../../containers/Avatar';
import Status from '../../containers/Status/Status'; import Status from '../../containers/Status/Status';
@ -90,19 +91,8 @@ class Sidebar extends Component {
if (nextProps.theme !== theme) { if (nextProps.theme !== theme) {
return true; return true;
} }
if (nextProps.user && user) { if (!isEqual(nextProps.user, user)) {
if (nextProps.user.language !== user.language) { return true;
return true;
}
if (nextProps.user.status !== user.status) {
return true;
}
if (nextProps.user.username !== user.username) {
return true;
}
if (nextProps.user.statusText !== user.statusText) {
return true;
}
} }
if (nextProps.isMasterDetail !== isMasterDetail) { if (nextProps.isMasterDetail !== isMasterDetail) {
return true; return true;
@ -241,11 +231,8 @@ class Sidebar extends Component {
<View style={styles.header} theme={theme}> <View style={styles.header} theme={theme}>
<Avatar <Avatar
text={user.username} text={user.username}
size={30}
style={styles.avatar} style={styles.avatar}
baseUrl={baseUrl} size={30}
userId={user.id}
token={user.token}
/> />
<View style={styles.headerTextContainer}> <View style={styles.headerTextContainer}>
<View style={styles.headerUsername}> <View style={styles.headerUsername}>

File diff suppressed because it is too large Load Diff

View File

@ -95,7 +95,7 @@
"react-native-notifier": "1.3.1", "react-native-notifier": "1.3.1",
"react-native-orientation-locker": "1.1.8", "react-native-orientation-locker": "1.1.8",
"react-native-picker-select": "7.0.0", "react-native-picker-select": "7.0.0",
"react-native-platform-touchable": "^1.1.1", "react-native-platform-touchable": "1.1.1",
"react-native-popover-view": "3.0.3", "react-native-popover-view": "3.0.3",
"react-native-progress": "4.1.2", "react-native-progress": "4.1.2",
"react-native-prompt-android": "^1.1.0", "react-native-prompt-android": "^1.1.0",

137
storybook/stories/Avatar.js Normal file
View File

@ -0,0 +1,137 @@
import React from 'react';
import { ScrollView, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import { themes } from '../../app/constants/colors';
import Avatar from '../../app/containers/Avatar/Avatar';
import Status from '../../app/containers/Status/Status';
import StoriesSeparator from './StoriesSeparator';
import sharedStyles from '../../app/views/Styles';
const styles = StyleSheet.create({
status: {
borderWidth: 4,
bottom: -4,
right: -4
},
custom: {
padding: 16
}
});
const server = 'https://open.rocket.chat';
const Separator = ({ title, theme }) => <StoriesSeparator title={title} theme={theme} />;
Separator.propTypes = {
title: PropTypes.string,
theme: PropTypes.string
};
const AvatarStories = ({ theme }) => (
<ScrollView style={{ backgroundColor: themes[theme].backgroundColor }}>
<Separator title='Avatar by text' theme={theme} />
<Avatar
text='Avatar'
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'
server={server}
size={56}
/>
<Separator title='Avatar by path' theme={theme} />
<Avatar
avatar='/avatar/diego.mello'
server={server}
size={56}
/>
<Separator title='With ETag' theme={theme} />
<Avatar
type='d'
text='djorkaeff.alexandre'
avatarETag='5ag8KffJcZj9m5rCv'
server={server}
size={56}
/>
<Separator title='Without ETag' theme={theme} />
<Avatar
type='d'
text='djorkaeff.alexandre'
server={server}
size={56}
/>
<Separator title='Emoji' theme={theme} />
<Avatar
emoji='troll'
getCustomEmoji={() => ({ name: 'troll', extension: 'jpg' })}
server={server}
size={56}
/>
<Separator title='Direct' theme={theme} />
<Avatar
text='diego.mello'
server={server}
type='d'
size={56}
/>
<Separator title='Channel' theme={theme} />
<Avatar
text='general'
server={server}
type='c'
size={56}
/>
<Separator title='Touchable' theme={theme} />
<Avatar
text='Avatar'
server={server}
onPress={() => console.log('Pressed!')}
size={56}
/>
<Separator title='Static' theme={theme} />
<Avatar
avatar='https://user-images.githubusercontent.com/29778115/89444446-14738480-d728-11ea-9412-75fd978d95fb.jpg'
server={server}
isStatic
size={56}
/>
<Separator title='Custom borderRadius' theme={theme} />
<Avatar
text='Avatar'
server={server}
borderRadius={28}
size={56}
/>
<Separator title='Children' theme={theme} />
<Avatar
text='Avatar'
server={server}
size={56}
>
<Status
size={24}
style={[sharedStyles.status, styles.status]}
theme={theme}
/>
</Avatar>
<Separator title='Wrong server' theme={theme} />
<Avatar
text='Avatar'
server='https://google.com'
size={56}
/>
<Separator title='Custom style' theme={theme} />
<Avatar
text='Avatar'
server={server}
size={56}
style={styles.custom}
/>
</ScrollView>
);
AvatarStories.propTypes = {
theme: PropTypes.string
};
export default AvatarStories;

View File

@ -9,6 +9,7 @@ import Message from './Message';
import UiKitMessage from './UiKitMessage'; import UiKitMessage from './UiKitMessage';
import UiKitModal from './UiKitModal'; import UiKitModal from './UiKitModal';
import Markdown from './Markdown'; import Markdown from './Markdown';
import Avatar from './Avatar';
// import RoomViewHeader from './RoomViewHeader'; // import RoomViewHeader from './RoomViewHeader';
import MessageContext from '../../app/containers/message/Context'; import MessageContext from '../../app/containers/message/Context';
@ -70,6 +71,8 @@ storiesOf('UiKitModal', module)
.add('list UiKitModal', () => <UiKitModal theme={theme} />); .add('list UiKitModal', () => <UiKitModal theme={theme} />);
storiesOf('Markdown', module) storiesOf('Markdown', module)
.add('list Markdown', () => <Markdown theme={theme} />); .add('list Markdown', () => <Markdown theme={theme} />);
storiesOf('Avatar', module)
.add('list Avatar', () => <Avatar theme={theme} />);
// FIXME: I couldn't make these pass on jest :( // FIXME: I couldn't make these pass on jest :(
// storiesOf('RoomViewHeader', module) // storiesOf('RoomViewHeader', module)

View File

@ -12891,7 +12891,7 @@ react-native-picker-select@7.0.0:
dependencies: dependencies:
lodash.isequal "^4.5.0" lodash.isequal "^4.5.0"
react-native-platform-touchable@^1.1.1: react-native-platform-touchable@1.1.1, react-native-platform-touchable@^1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/react-native-platform-touchable/-/react-native-platform-touchable-1.1.1.tgz#fde4acc65eea585d28b164d0c3716a42129a68e4" resolved "https://registry.yarnpkg.com/react-native-platform-touchable/-/react-native-platform-touchable-1.1.1.tgz#fde4acc65eea585d28b164d0c3716a42129a68e4"
integrity sha1-/eSsxl7qWF0osWTQw3FqQhKaaOQ= integrity sha1-/eSsxl7qWF0osWTQw3FqQhKaaOQ=