[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

View File

@ -1,5 +1,926 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Avatar list Avatar 1`] = `
<RCTScrollView
style={
Object {
"backgroundColor": "#ffffff",
}
}
>
<View>
<Text
style={
Array [
Object {
"fontSize": 20,
"fontWeight": "300",
"marginLeft": 10,
"marginVertical": 30,
},
Object {
"color": "#0d0e12",
},
undefined,
]
}
>
Avatar by text
</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/Avatar?format=png&size=50",
}
}
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 url
</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://user-images.githubusercontent.com/29778115/89444446-14738480-d728-11ea-9412-75fd978d95fb.jpg",
}
}
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 path
</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/diego.mello?format=png&size=50",
}
}
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,
]
}
>
With ETag
</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/djorkaeff.alexandre?format=png&size=50&etag=5ag8KffJcZj9m5rCv",
}
}
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,
]
}
>
Without ETag
</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/djorkaeff.alexandre?format=png&size=50",
}
}
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,
]
}
>
Emoji
</Text>
<View
style={
Array [
Object {
"borderRadius": 4,
"height": 56,
"width": 56,
},
undefined,
]
}
>
<View
style={
Array [
Object {
"overflow": "hidden",
},
Array [
Object {
"height": 30,
"width": 30,
},
Object {
"borderRadius": 4,
"height": 56,
"width": 56,
},
],
]
}
>
<FastImageView
resizeMode="contain"
source={
Object {
"priority": "high",
"uri": "https://open.rocket.chat/emoji-custom/troll.jpg",
}
}
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,
]
}
>
Direct
</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/diego.mello?format=png&size=50",
}
}
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,
]
}
>
Channel
</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/@general?format=png&size=50",
}
}
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,
]
}
>
Touchable
</Text>
<View
style={
Array [
Object {
"borderRadius": 4,
"height": 56,
"width": 56,
},
undefined,
]
}
/>
<Text
style={
Array [
Object {
"fontSize": 20,
"fontWeight": "300",
"marginLeft": 10,
"marginVertical": 30,
},
Object {
"color": "#0d0e12",
},
undefined,
]
}
>
Static
</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://user-images.githubusercontent.com/29778115/89444446-14738480-d728-11ea-9412-75fd978d95fb.jpg",
}
}
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,
]
}
>
Custom borderRadius
</Text>
<View
style={
Array [
Object {
"borderRadius": 28,
"height": 56,
"width": 56,
},
undefined,
]
}
>
<View
style={
Array [
Object {
"overflow": "hidden",
},
Object {
"borderRadius": 28,
"height": 56,
"width": 56,
},
]
}
>
<FastImageView
resizeMode="cover"
source={
Object {
"headers": undefined,
"priority": "high",
"uri": "https://open.rocket.chat/avatar/Avatar?format=png&size=50",
}
}
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,
]
}
>
Children
</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/Avatar?format=png&size=50",
}
}
style={
Object {
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
</View>
<View
style={
Array [
Array [
Object {
"borderWidth": 3,
"bottom": -3,
"position": "absolute",
"right": -3,
},
Object {
"borderWidth": 4,
"bottom": -4,
"right": -4,
},
],
Object {
"backgroundColor": "#cbced1",
"borderColor": "#ffffff",
"borderRadius": 24,
"height": 24,
"width": 24,
},
]
}
/>
</View>
<Text
style={
Array [
Object {
"fontSize": 20,
"fontWeight": "300",
"marginLeft": 10,
"marginVertical": 30,
},
Object {
"color": "#0d0e12",
},
undefined,
]
}
>
Wrong server
</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://google.com/avatar/Avatar?format=png&size=50",
}
}
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,
]
}
>
Custom style
</Text>
<View
style={
Array [
Object {
"borderRadius": 4,
"height": 56,
"width": 56,
},
Object {
"padding": 16,
},
]
}
>
<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/Avatar?format=png&size=50",
}
}
style={
Object {
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
</View>
</View>
</View>
</RCTScrollView>
`;
exports[`Storyshots Markdown list Markdown 1`] = `
<RCTScrollView
contentContainerStyle={
@ -1238,6 +2159,7 @@ exports[`Storyshots Markdown list Markdown 1`] = `
"fontSize": 16,
"fontWeight": "400",
},
Object {},
]
}
>
@ -1255,6 +2177,7 @@ exports[`Storyshots Markdown list Markdown 1`] = `
"fontSize": 16,
"fontWeight": "400",
},
Object {},
]
}
>
@ -1303,10 +2226,13 @@ exports[`Storyshots Markdown list Markdown 1`] = `
Object {
"overflow": "hidden",
},
Array [
Object {
"height": 20,
"width": 20,
},
Object {},
],
]
}
>
@ -1361,10 +2287,13 @@ exports[`Storyshots Markdown list Markdown 1`] = `
Object {
"overflow": "hidden",
},
Array [
Object {
"height": 20,
"width": 20,
},
Object {},
],
]
}
>
@ -1419,10 +2348,13 @@ exports[`Storyshots Markdown list Markdown 1`] = `
Object {
"overflow": "hidden",
},
Array [
Object {
"height": 20,
"width": 20,
},
Object {},
],
]
}
>
@ -1499,6 +2431,7 @@ exports[`Storyshots Markdown list Markdown 1`] = `
"fontSize": 30,
"fontWeight": "400",
},
Object {},
]
}
>
@ -1541,10 +2474,13 @@ exports[`Storyshots Markdown list Markdown 1`] = `
Object {
"overflow": "hidden",
},
Array [
Object {
"height": 30,
"width": 30,
},
Object {},
],
]
}
>

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 { themes } from '../../constants/colors';
import { useTheme } from '../../theme';
import { getUserSelector } from '../../selectors/login';
import { ROW_HEIGHT } from '../../presentation/RoomItem';
import { goRoom } from '../../utils/goRoom';
import Navigation from '../../lib/Navigation';
@ -65,14 +64,11 @@ const styles = StyleSheet.create({
const hideNotification = () => Notifier.hideNotification();
const NotifierComponent = React.memo(({
baseUrl, user, notification, isMasterDetail
}) => {
const NotifierComponent = React.memo(({ notification, isMasterDetail }) => {
const { theme } = useTheme();
const insets = useSafeAreaInsets();
const { isLandscape } = useOrientation();
const { id: userId, token } = user;
const { text, payload } = notification;
const { type } = payload;
const name = type === 'd' ? payload.sender.username : payload.name;
@ -115,7 +111,7 @@ const NotifierComponent = React.memo(({
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}>
<Text style={[styles.roomName, { color: themes[theme].titleText }]} numberOfLines={1}>{title}</Text>
<Text style={[styles.message, { color: themes[theme].titleText }]} numberOfLines={1}>{text}</Text>
@ -134,15 +130,11 @@ const NotifierComponent = React.memo(({
});
NotifierComponent.propTypes = {
baseUrl: PropTypes.string,
user: PropTypes.object,
notification: PropTypes.object,
isMasterDetail: PropTypes.bool
};
const mapStateToProps = state => ({
user: getUserSelector(state),
baseUrl: state.server.server,
isMasterDetail: state.app.isMasterDetail
});

View File

@ -17,7 +17,7 @@ const MentionItem = ({
item, trackingType, theme
}) => {
const context = useContext(MessageboxContext);
const { baseUrl, user, onPressMention } = context;
const { onPressMention } = context;
const defineTestID = (type) => {
switch (type) {
@ -43,9 +43,6 @@ const MentionItem = ({
text={item.username || item.name}
size={30}
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>
</>

View File

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

View File

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

View File

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

View File

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

View File

@ -6,17 +6,13 @@ import { sanitizer } from '../utils';
export default class User extends Model {
static table = 'users';
@field('token') token;
@field('username') username;
@field('_id') _id;
@field('name') name;
@field('language') language;
@field('username') username;
@field('status') status;
@field('statusText') statusText;
@field('avatar_etag') avatarETag;
@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({
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';
export default appSchema({
version: 10,
version: 11,
tables: [
tableSchema({
name: 'subscriptions',
@ -247,6 +247,15 @@ export default appSchema({
{ name: 'provides_preview', type: 'boolean', 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';
export default appSchema({
version: 9,
version: 10,
tables: [
tableSchema({
name: 'users',
@ -13,7 +13,8 @@ export default appSchema({
{ name: 'status', type: 'string', isOptional: true },
{ name: 'statusText', 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({

View File

@ -1,9 +1,11 @@
import { InteractionManager } from 'react-native';
import semver from 'semver';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import reduxStore from '../createStore';
import { setActiveUsers } from '../../actions/activeUsers';
import { setUser } from '../../actions/login';
import database from '../database';
export function subscribeUsersPresence() {
const serverVersion = reduxStore.getState().server.version;
@ -20,6 +22,11 @@ export function subscribeUsersPresence() {
} else {
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 = [];
@ -46,7 +53,9 @@ export default async function getUsersPresence() {
// RC 1.1.0
const result = await this.sdk.get('users.presence', params);
if (result.success) {
const activeUsers = result.users.reduce((ret, item) => {
const { users } = result;
const activeUsers = users.reduce((ret, item) => {
const { _id, status, statusText } = item;
if (loggedUser && loggedUser.id === _id) {
@ -60,6 +69,27 @@ export default async function getUsersPresence() {
reduxStore.dispatch(setActiveUsers(activeUsers));
});
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 {
// do nothing
@ -80,5 +110,7 @@ export function getUserPresence(uid) {
}, 2000);
}
if (uid) {
ids.push(uid);
}
}

View File

@ -6,6 +6,7 @@ import {
} from '@rocket.chat/sdk';
import { Q } from '@nozbe/watermelondb';
import AsyncStorage from '@react-native-community/async-storage';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import RNFetchBlob from 'rn-fetch-blob';
import reduxStore from './createStore';
@ -244,9 +245,9 @@ const RocketChat = {
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;
if (eventName === 'user-status') {
if (/user-status/.test(eventName)) {
this.activeUsers = this.activeUsers || {};
if (!this._setUserTimer) {
this._setUserTimer = setTimeout(() => {
@ -266,6 +267,40 @@ const RocketChat = {
if (loggedUser && loggedUser.id === id) {
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,
emails: result.me.emails,
roles: result.me.roles,
avatarETag: result.me.avatarETag,
loginEmailPassword
};
return user;

View File

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

View File

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

View File

@ -4,12 +4,13 @@ import PropTypes from 'prop-types';
import styles from './styles';
import { themes } from '../../constants/colors';
import Avatar from '../../containers/Avatar';
import Avatar from '../../containers/Avatar/Avatar';
const RoomItemInner = ({
const Wrapper = ({
accessibilityLabel,
avatar,
avatarSize,
avatarETag,
type,
baseUrl,
userId,
@ -25,10 +26,10 @@ const RoomItemInner = ({
text={avatar}
size={avatarSize}
type={type}
baseUrl={baseUrl}
style={styles.avatar}
userId={userId}
token={token}
server={baseUrl}
user={{ id: userId, token }}
avatarETag={avatarETag}
/>
<View
style={[
@ -43,10 +44,11 @@ const RoomItemInner = ({
</View>
);
RoomItemInner.propTypes = {
Wrapper.propTypes = {
accessibilityLabel: PropTypes.string,
avatar: PropTypes.string,
avatarSize: PropTypes.number,
avatarETag: PropTypes.string,
type: PropTypes.string,
baseUrl: PropTypes.string,
userId: PropTypes.string,
@ -55,4 +57,4 @@ RoomItemInner.propTypes = {
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 { connect } from 'react-redux';
import I18n from '../../i18n';
import { ROW_HEIGHT } from './styles';
import { formatDate } from '../../utils/room';
import database from '../../lib/database';
import RoomItem from './RoomItem';
export { ROW_HEIGHT };
@ -19,61 +20,154 @@ const attrs = [
'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 = {
avatarSize: 48,
status: 'offline',
getUserPresence: () => {},
getRoomTitle: () => 'title',
getRoomAvatar: () => '',
getIsGroupChat: () => false,
getIsRead: () => false,
swipeEnabled: true
}
constructor(props) {
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);
}
}
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) {
const observable = item.observe();
this.roomSubscription = observable?.subscribe?.(() => {
this.forceUpdate();
});
}
if (this.isDirect) {
const { id } = this.props;
const db = database.active;
const usersCollection = db.collections.get('users');
try {
const user = await usersCollection.find(id);
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 not found
}
}
}
onPress = () => {
const { item, onPress } = this.props;
return onPress(item);
}
render() {
const { avatarETag } = this.state;
const {
item,
onPress,
getRoomTitle,
getRoomAvatar,
getIsRead,
width,
toggleFav,
toggleRead,
hideChannel,
testID,
theme,
isFocused,
avatarSize,
baseUrl,
userId,
username,
token,
id,
showLastMessage,
status,
showLastMessage,
username,
useRealName,
getUserPresence,
connected,
theme,
isFocused,
getRoomTitle,
getRoomAvatar,
getIsGroupChat,
getIsRead,
swipeEnabled
}) => {
const [, setForceUpdate] = useState(1);
useEffect(() => {
if (connected && item.t === 'd' && id) {
getUserPresence(id);
}
}, [connected]);
useEffect(() => {
if (item?.observe) {
const observable = item.observe();
const subscription = observable?.subscribe?.(() => {
setForceUpdate(prevForceUpdate => prevForceUpdate + 1);
});
return () => {
subscription?.unsubscribe?.();
};
}
}, []);
} = this.props;
const name = getRoomTitle(item);
const avatar = getRoomAvatar(item);
const isGroupChat = getIsGroupChat(item);
const isRead = getIsRead(item);
const _onPress = () => onPress(item);
const date = item.lastMessage?.ts && formatDate(item.lastMessage.ts);
let accessibilityLabel = name;
@ -95,9 +189,9 @@ const RoomItemContainer = React.memo(({
<RoomItem
name={name}
avatar={avatar}
isGroupChat={isGroupChat}
isGroupChat={this.isGroupChat}
isRead={isRead}
onPress={_onPress}
onPress={this.onPress}
date={date}
accessibilityLabel={accessibilityLabel}
userMentions={item.userMentions}
@ -126,49 +220,12 @@ const RoomItemContainer = React.memo(({
useRealName={useRealName}
unread={item.unread}
groupMentions={item.groupMentions}
avatarETag={avatarETag}
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) => {
let status = 'offline';

View File

@ -42,7 +42,7 @@ const styles = StyleSheet.create({
});
const UserItem = ({
name, username, onPress, testID, onLongPress, style, icon, baseUrl, user, theme
name, username, onPress, testID, onLongPress, style, icon, theme
}) => (
<Pressable
onPress={onPress}
@ -58,7 +58,7 @@ const UserItem = ({
})}
>
<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}>
<Text style={[styles.name, { color: themes[theme].titleText }]} numberOfLines={1}>{name}</Text>
<Text style={[styles.username, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>@{username}</Text>
@ -71,11 +71,6 @@ const UserItem = ({
UserItem.propTypes = {
name: PropTypes.string.isRequired,
username: PropTypes.string.isRequired,
user: PropTypes.shape({
id: PropTypes.string,
token: PropTypes.string
}),
baseUrl: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired,
testID: PropTypes.string.isRequired,
onLongPress: PropTypes.func,

View File

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

View File

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

View File

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

View File

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

View File

@ -2,10 +2,12 @@ import React, { useState } from 'react';
import { Text } from 'react-native';
import PropTypes from 'prop-types';
import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
import { Q } from '@nozbe/watermelondb';
import debounce from '../../utils/debounce';
import { avatarURL } from '../../utils/avatar';
import RocketChat from '../../lib/rocketchat';
import database from '../../lib/database';
import I18n from '../../i18n';
import { MultiSelect } from '../../containers/UIKit/MultiSelect';
@ -19,15 +21,34 @@ const SelectUsers = ({
const getUsers = debounce(async(keyword = '') => {
try {
const db = database.active;
const usersCollection = db.collections.get('users');
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 {
// do nothing
}
}, 300);
const getAvatar = text => avatarURL({
text, type: 'd', userId, token, baseUrl: server
const getAvatar = item => avatarURL({
text: RocketChat.getRoomAvatar(item), type: 'd', user: { id: userId, token }, server, avatarETag: item.avatarETag
});
return (
@ -41,7 +62,7 @@ const SelectUsers = ({
options={users.map(user => ({
value: user.name,
text: { text: RocketChat.getRoomTitle(user) },
imageUrl: getAvatar(RocketChat.getRoomAvatar(user))
imageUrl: getAvatar(user)
}))}
onClose={() => setUsers(users.filter(u => selected.includes(u.name)))}
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 ImagePicker from 'react-native-image-crop-picker';
import RNPickerSelect from 'react-native-picker-select';
import equal from 'deep-equal';
import { isEqual, omit } from 'lodash';
import Touch from '../../utils/touch';
import KeyboardView from '../../presentation/KeyboardView';
@ -84,16 +84,22 @@ class ProfileView extends React.Component {
UNSAFE_componentWillReceiveProps(nextProps) {
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);
}
}
shouldComponentUpdate(nextProps, nextState) {
if (!equal(nextState, this.state)) {
if (!isEqual(nextState, this.state)) {
return true;
}
if (!equal(nextProps, this.props)) {
if (!isEqual(nextProps, this.props)) {
return true;
}
return false;
@ -324,7 +330,6 @@ class ProfileView extends React.Component {
const { avatarUrl, avatarSuggestions } = this.state;
const {
user,
baseUrl,
theme,
Accounts_AllowUserAvatarChange
} = this.props;
@ -332,7 +337,7 @@ class ProfileView extends React.Component {
return (
<View style={styles.avatarButtons}>
{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(),
disabled: !Accounts_AllowUserAvatarChange,
key: 'profile-view-reset-avatar'
@ -354,7 +359,7 @@ class ProfileView extends React.Component {
return this.renderAvatarButton({
disabled: !Accounts_AllowUserAvatarChange,
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({
url, data: blob, service, contentType
})
@ -448,7 +453,6 @@ class ProfileView extends React.Component {
name, username, email, newPassword, avatarUrl, customFields, avatar, saving
} = this.state;
const {
baseUrl,
user,
theme,
Accounts_AllowEmailChange,
@ -474,12 +478,10 @@ class ProfileView extends React.Component {
>
<View style={styles.avatarContainer} testID='profile-view-avatar'>
<Avatar
text={username}
avatar={avatar && avatar.url}
text={user.username}
avatar={avatar?.url}
isStatic={avatar?.url}
size={100}
baseUrl={baseUrl}
userId={user.id}
token={user.token}
/>
</View>
<RCTextInput

View File

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

View File

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@ import {
} from 'react-native';
import { connect } from 'react-redux';
import { Q } from '@nozbe/watermelondb';
import isEqual from 'react-fast-compare';
import Avatar from '../../containers/Avatar';
import Status from '../../containers/Status/Status';
@ -90,20 +91,9 @@ class Sidebar extends Component {
if (nextProps.theme !== theme) {
return true;
}
if (nextProps.user && user) {
if (nextProps.user.language !== user.language) {
if (!isEqual(nextProps.user, user)) {
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) {
return true;
}
@ -241,11 +231,8 @@ class Sidebar extends Component {
<View style={styles.header} theme={theme}>
<Avatar
text={user.username}
size={30}
style={styles.avatar}
baseUrl={baseUrl}
userId={user.id}
token={user.token}
size={30}
/>
<View style={styles.headerTextContainer}>
<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-orientation-locker": "1.1.8",
"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-progress": "4.1.2",
"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 UiKitModal from './UiKitModal';
import Markdown from './Markdown';
import Avatar from './Avatar';
// import RoomViewHeader from './RoomViewHeader';
import MessageContext from '../../app/containers/message/Context';
@ -70,6 +71,8 @@ storiesOf('UiKitModal', module)
.add('list UiKitModal', () => <UiKitModal theme={theme} />);
storiesOf('Markdown', module)
.add('list Markdown', () => <Markdown theme={theme} />);
storiesOf('Avatar', module)
.add('list Avatar', () => <Avatar theme={theme} />);
// FIXME: I couldn't make these pass on jest :(
// storiesOf('RoomViewHeader', module)

View File

@ -12891,7 +12891,7 @@ react-native-picker-select@7.0.0:
dependencies:
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"
resolved "https://registry.yarnpkg.com/react-native-platform-touchable/-/react-native-platform-touchable-1.1.1.tgz#fde4acc65eea585d28b164d0c3716a42129a68e4"
integrity sha1-/eSsxl7qWF0osWTQw3FqQhKaaOQ=