[IMPROVEMENT] User status icons (#2991)

* Add status and teams

* Update icons, icon size and getUsersPresence

* Minor changes

* Refactor RoomTypeIcon

* Minor tweaks

* Update unit tests

* Minor fixes

* Fix styles

* Small refactor

* Update jest

Co-authored-by: Diego Mello <diegolmello@gmail.com>
This commit is contained in:
Gerzon Z 2021-03-31 13:47:17 -04:00 committed by GitHub
parent 8bc8a07e72
commit 25b71155e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1415 additions and 499 deletions

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,8 @@ export const STATUS_COLORS = {
online: '#2de0a5', online: '#2de0a5',
busy: '#f5455c', busy: '#f5455c',
away: '#ffd21f', away: '#ffd21f',
offline: '#cbced1' offline: '#cbced1',
loading: '#9ea2a8'
}; };
export const SWITCH_TRACK_COLOR = { export const SWITCH_TRACK_COLOR = {

View File

@ -3,10 +3,11 @@ import { StyleSheet } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { CustomIcon } from '../lib/Icons'; import { CustomIcon } from '../lib/Icons';
import { STATUS_COLORS, themes } from '../constants/colors'; import { STATUS_COLORS, themes } from '../constants/colors';
import Status from './Status/Status';
import { withTheme } from '../theme';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
icon: { icon: {
marginTop: 3,
marginRight: 4 marginRight: 4
} }
}); });
@ -18,7 +19,16 @@ const RoomTypeIcon = React.memo(({
return null; return null;
} }
const color = themes[theme].auxiliaryText; const color = themes[theme].titleText;
const iconStyle = [
styles.icon,
{ color },
style
];
if (type === 'd' && !isGroupChat) {
return <Status style={[iconStyle, { color: STATUS_COLORS[status] ?? STATUS_COLORS.offline }]} size={size} status={status} />;
}
let icon = 'channel-private'; let icon = 'channel-private';
if (type === 'discussion') { if (type === 'discussion') {
@ -27,7 +37,7 @@ const RoomTypeIcon = React.memo(({
icon = 'channel-public'; icon = 'channel-public';
} else if (type === 'd') { } else if (type === 'd') {
if (isGroupChat) { if (isGroupChat) {
icon = 'team'; icon = 'message';
} else { } else {
icon = 'mention'; icon = 'mention';
} }
@ -39,11 +49,7 @@ const RoomTypeIcon = React.memo(({
<CustomIcon <CustomIcon
name={icon} name={icon}
size={size} size={size}
style={[ style={iconStyle}
type === 'l' && status ? { color: STATUS_COLORS[status] } : { color },
styles.icon,
style
]}
/> />
); );
}); });
@ -61,4 +67,4 @@ RoomTypeIcon.defaultProps = {
size: 16 size: 16
}; };
export default RoomTypeIcon; export default withTheme(RoomTypeIcon);

View File

@ -1,36 +1,37 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { View } from 'react-native'; import { CustomIcon } from '../../lib/Icons';
import { STATUS_COLORS, themes } from '../../constants/colors'; import { STATUS_COLORS } from '../../constants/colors';
const Status = React.memo(({ const Status = React.memo(({
status, size, style, theme, ...props status, size, style, ...props
}) => ( }) => {
<View const name = `status-${ status }`;
style={ const isNameValid = CustomIcon.hasIcon(name);
[ const iconName = isNameValid ? name : 'status-offline';
style, const calculatedStyle = [{
{ width: size, height: size, textAlignVertical: 'center'
borderRadius: size, }, style];
width: size,
height: size, return (
backgroundColor: STATUS_COLORS[status] ?? STATUS_COLORS.offline, <CustomIcon
borderColor: themes[theme].backgroundColor style={calculatedStyle}
} size={size}
]} name={iconName}
color={STATUS_COLORS[status] ?? STATUS_COLORS.offline}
{...props} {...props}
/> />
)); );
});
Status.propTypes = { Status.propTypes = {
status: PropTypes.string, status: PropTypes.string,
size: PropTypes.number, size: PropTypes.number,
style: PropTypes.any, style: PropTypes.any
theme: PropTypes.string
}; };
Status.defaultProps = { Status.defaultProps = {
status: 'offline', status: 'offline',
size: 16, size: 32
theme: 'light'
}; };
export default Status; export default Status;

View File

@ -1,32 +1,19 @@
import React from 'react'; import React, { memo } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Status from './Status'; import Status from './Status';
import { withTheme } from '../../theme';
class StatusContainer extends React.PureComponent { const StatusContainer = memo(({ style, size = 32, status }) => <Status size={size} style={style} status={status} />);
static propTypes = {
StatusContainer.propTypes = {
style: PropTypes.any, style: PropTypes.any,
size: PropTypes.number, size: PropTypes.number,
status: PropTypes.string, status: PropTypes.string
theme: PropTypes.string
}; };
static defaultProps = {
size: 16
}
render() {
const {
style, size, status, theme
} = this.props;
return <Status size={size} style={style} status={status} theme={theme} />;
}
}
const mapStateToProps = (state, ownProps) => ({ const mapStateToProps = (state, ownProps) => ({
status: state.meteor.connected ? (state.activeUsers[ownProps.id] && state.activeUsers[ownProps.id].status) : 'offline' status: state.meteor.connected ? (state.activeUsers[ownProps.id] && state.activeUsers[ownProps.id].status) : 'loading'
}); });
export default connect(mapStateToProps)(withTheme(StatusContainer)); export default connect(mapStateToProps)(StatusContainer);

View File

@ -55,8 +55,9 @@ export default async function getUsersPresence() {
if (result.success) { if (result.success) {
const { users } = result; const { users } = result;
const activeUsers = users.reduce((ret, item) => { const activeUsers = ids.reduce((ret, id) => {
const { _id, status, statusText } = item; const user = users.find(u => u._id === id) ?? { _id: id, status: 'offline' };
const { _id, status, statusText } = user;
if (loggedUser && loggedUser.id === _id) { if (loggedUser && loggedUser.id === _id) {
reduxStore.dispatch(setUser({ status, statusText })); reduxStore.dispatch(setUser({ status, statusText }));

File diff suppressed because one or more lines are too long

View File

@ -78,7 +78,6 @@ const RoomItem = ({
prid={prid} prid={prid}
status={status} status={status}
isGroupChat={isGroupChat} isGroupChat={isGroupChat}
theme={theme}
/> />
<Title <Title
name={name} name={name}
@ -121,7 +120,6 @@ const RoomItem = ({
prid={prid} prid={prid}
status={status} status={status}
isGroupChat={isGroupChat} isGroupChat={isGroupChat}
theme={theme}
/> />
<Title <Title
name={name} name={name}

View File

@ -1,21 +1,13 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Status from '../../containers/Status/Status';
import RoomTypeIcon from '../../containers/RoomTypeIcon'; import RoomTypeIcon from '../../containers/RoomTypeIcon';
import styles from './styles';
const TypeIcon = React.memo(({ const TypeIcon = React.memo(({
theme, type, prid, status, isGroupChat type, prid, status, isGroupChat
}) => { }) => <RoomTypeIcon type={prid ? 'discussion' : type} isGroupChat={isGroupChat} status={status} />);
if (type === 'd' && !isGroupChat) {
return <Status style={styles.status} size={10} status={status} />;
}
return <RoomTypeIcon theme={theme} type={prid ? 'discussion' : type} isGroupChat={isGroupChat} status={status} />;
});
TypeIcon.propTypes = { TypeIcon.propTypes = {
theme: PropTypes.string,
type: PropTypes.string, type: PropTypes.string,
status: PropTypes.string, status: PropTypes.string,
prid: PropTypes.string, prid: PropTypes.string,

View File

@ -194,11 +194,11 @@ class RoomItemContainer extends React.Component {
} }
const mapStateToProps = (state, ownProps) => { const mapStateToProps = (state, ownProps) => {
let status = 'offline'; let status = 'loading';
const { id, type, visitor = {} } = ownProps; const { id, type, visitor = {} } = ownProps;
if (state.meteor.connected) { if (state.meteor.connected) {
if (type === 'd') { if (type === 'd') {
status = state.activeUsers[id]?.status || 'offline'; status = state.activeUsers[id]?.status || 'loading';
} else if (type === 'l' && visitor?.status) { } else if (type === 'l' && visitor?.status) {
({ status } = visitor); ({ status } = visitor);
} }

View File

@ -52,9 +52,7 @@ export default StyleSheet.create({
...sharedStyles.textSemibold ...sharedStyles.textSemibold
}, },
status: { status: {
marginLeft: 4, marginRight: 2
marginRight: 7,
marginTop: 3
}, },
markdownText: { markdownText: {
flex: 1, flex: 1,

View File

@ -448,14 +448,20 @@ class RoomActionsView extends React.Component {
type={t} type={t}
rid={rid} rid={rid}
> >
{t === 'd' && member._id ? <Status style={sharedStyles.status} id={member._id} /> : null } {t === 'd' && member._id
? (
<View style={[sharedStyles.status, { backgroundColor: themes[theme].backgroundColor }]}>
<Status size={16} id={member._id} />
</View>
) : null
}
</Avatar> </Avatar>
<View style={styles.roomTitleContainer}> <View style={styles.roomTitleContainer}>
{room.t === 'd' {room.t === 'd'
? <Text style={[styles.roomTitle, { color: themes[theme].titleText }]} numberOfLines={1}>{room.fname}</Text> ? <Text style={[styles.roomTitle, { color: themes[theme].titleText }]} numberOfLines={1}>{room.fname}</Text>
: ( : (
<View style={styles.roomTitleRow}> <View style={styles.roomTitleRow}>
<RoomTypeIcon type={room.prid ? 'discussion' : room.t} status={room.visitor?.status} theme={theme} /> <RoomTypeIcon type={room.prid ? 'discussion' : room.t} status={room.visitor?.status} />
<Text style={[styles.roomTitle, { color: themes[theme].titleText }]} numberOfLines={1}>{RocketChat.getRoomTitle(room)}</Text> <Text style={[styles.roomTitle, { color: themes[theme].titleText }]} numberOfLines={1}>{RocketChat.getRoomTitle(room)}</Text>
</View> </View>
) )

View File

@ -41,7 +41,7 @@ const getRoomTitle = (room, type, name, username, statusText, theme) => (type ==
) )
: ( : (
<View style={styles.roomTitleRow}> <View style={styles.roomTitleRow}>
<RoomTypeIcon type={room.prid ? 'discussion' : room.t} key='room-info-type' status={room.visitor?.status} theme={theme} /> <RoomTypeIcon type={room.prid ? 'discussion' : room.t} key='room-info-type' status={room.visitor?.status} />
<Text testID='room-info-view-name' style={[styles.roomTitle, { color: themes[theme].titleText }]} key='room-info-name'>{RocketChat.getRoomTitle(room)}</Text> <Text testID='room-info-view-name' style={[styles.roomTitle, { color: themes[theme].titleText }]} key='room-info-name'>{RocketChat.getRoomTitle(room)}</Text>
</View> </View>
) )
@ -290,7 +290,13 @@ class RoomInfoView extends React.Component {
size={100} size={100}
rid={room?.rid} rid={room?.rid}
> >
{this.t === 'd' && roomUser._id ? <Status style={[sharedStyles.status, styles.status]} theme={theme} size={24} id={roomUser._id} /> : null} {this.t === 'd' && roomUser._id
? (
<View style={[sharedStyles.status, { backgroundColor: themes[theme].auxiliaryBackground }]}>
<Status size={20} id={roomUser._id} />
</View>
)
: null}
</Avatar> </Avatar>
); );
} }

View File

@ -48,11 +48,6 @@ export default StyleSheet.create({
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center' alignItems: 'center'
}, },
status: {
borderWidth: 4,
bottom: -4,
right: -4
},
itemLabel: { itemLabel: {
marginBottom: 10, marginBottom: 10,
fontSize: 14, fontSize: 14,

View File

@ -6,9 +6,9 @@ import {
import I18n from '../../../i18n'; import I18n from '../../../i18n';
import sharedStyles from '../../Styles'; import sharedStyles from '../../Styles';
import Icon from './Icon';
import { themes } from '../../../constants/colors'; import { themes } from '../../../constants/colors';
import Markdown from '../../../containers/markdown'; import Markdown from '../../../containers/markdown';
import RoomTypeIcon from '../../../containers/RoomTypeIcon';
const HIT_SLOP = { const HIT_SLOP = {
top: 5, right: 5, bottom: 5, left: 5 top: 5, right: 5, bottom: 5, left: 5
@ -119,7 +119,7 @@ HeaderTitle.propTypes = {
}; };
const Header = React.memo(({ const Header = React.memo(({
title, subtitle, parentTitle, type, status, usersTyping, width, height, prid, tmid, connecting, goRoomActionsView, roomUserId, theme title, subtitle, parentTitle, type, status, usersTyping, width, height, prid, tmid, connecting, goRoomActionsView, theme, isGroupChat
}) => { }) => {
const portrait = height > width; const portrait = height > width;
let scale = 1; let scale = 1;
@ -136,13 +136,7 @@ const Header = React.memo(({
if (tmid) { if (tmid) {
renderFunc = () => ( renderFunc = () => (
<View style={styles.titleContainer}> <View style={styles.titleContainer}>
<Icon <RoomTypeIcon type={prid ? 'discussion' : type} isGroupChat={isGroupChat} status={status} />
type={prid ? 'discussion' : type}
tmid={tmid}
status={status}
roomUserId={roomUserId}
theme={theme}
/>
<Text style={[styles.subtitle, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>{parentTitle}</Text> <Text style={[styles.subtitle, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>{parentTitle}</Text>
</View> </View>
); );
@ -158,7 +152,7 @@ const Header = React.memo(({
hitSlop={HIT_SLOP} hitSlop={HIT_SLOP}
> >
<View style={styles.titleContainer}> <View style={styles.titleContainer}>
{tmid ? null : <Icon type={prid ? 'discussion' : type} status={status} roomUserId={roomUserId} theme={theme} />} {tmid ? null : <RoomTypeIcon type={prid ? 'discussion' : type} isGroupChat={isGroupChat} status={status} />}
<HeaderTitle <HeaderTitle
title={title} title={title}
tmid={tmid} tmid={tmid}
@ -185,7 +179,7 @@ Header.propTypes = {
theme: PropTypes.string, theme: PropTypes.string,
usersTyping: PropTypes.array, usersTyping: PropTypes.array,
connecting: PropTypes.bool, connecting: PropTypes.bool,
roomUserId: PropTypes.string, isGroupChat: PropTypes.bool,
parentTitle: PropTypes.string, parentTitle: PropTypes.string,
goRoomActionsView: PropTypes.func goRoomActionsView: PropTypes.func
}; };

View File

@ -1,72 +0,0 @@
import React from 'react';
import { StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import { STATUS_COLORS, themes } from '../../../constants/colors';
import { CustomIcon } from '../../../lib/Icons';
import Status from '../../../containers/Status/Status';
const ICON_SIZE = 15;
const styles = StyleSheet.create({
type: {
width: ICON_SIZE,
height: ICON_SIZE,
marginRight: 4,
marginLeft: -4
},
status: {
marginRight: 8
}
});
const Icon = React.memo(({
roomUserId, type, status, theme, tmid
}) => {
if ((type === 'd' || tmid) && roomUserId) {
return <Status size={10} style={styles.status} status={status} />;
}
let colorStyle = {};
if (type === 'l') {
colorStyle = { color: STATUS_COLORS[status] };
} else {
colorStyle = { color: themes[theme].auxiliaryText };
}
let icon;
if (type === 'discussion') {
icon = 'discussions';
} else if (type === 'c') {
icon = 'channel-public';
} else if (type === 'l') {
icon = 'omnichannel';
} else if (type === 'd') {
icon = 'team';
} else {
icon = 'channel-private';
}
return (
<CustomIcon
name={icon}
size={ICON_SIZE * 1}
style={[
styles.type,
{
width: ICON_SIZE * 1,
height: ICON_SIZE * 1
},
colorStyle
]}
/>
);
});
Icon.propTypes = {
roomUserId: PropTypes.string,
type: PropTypes.string,
status: PropTypes.string,
theme: PropTypes.string,
tmid: PropTypes.string
};
export default Icon;

View File

@ -28,7 +28,8 @@ class RoomHeaderView extends Component {
goRoomActionsView: PropTypes.func, goRoomActionsView: PropTypes.func,
width: PropTypes.number, width: PropTypes.number,
height: PropTypes.number, height: PropTypes.number,
parentTitle: PropTypes.string parentTitle: PropTypes.string,
isGroupChat: PropTypes.bool
}; };
shouldComponentUpdate(nextProps) { shouldComponentUpdate(nextProps) {
@ -76,7 +77,24 @@ class RoomHeaderView extends Component {
render() { render() {
const { const {
title, subtitle: subtitleProp, type, prid, tmid, widthOffset, status = 'offline', statusText, connecting, connected, usersTyping, goRoomActionsView, roomUserId, theme, width, height, parentTitle title,
subtitle: subtitleProp,
type,
prid,
tmid,
widthOffset,
status = 'offline',
statusText,
connecting,
connected,
usersTyping,
goRoomActionsView,
roomUserId,
theme,
width,
height,
parentTitle,
isGroupChat
} = this.props; } = this.props;
let subtitle; let subtitle;
@ -105,6 +123,7 @@ class RoomHeaderView extends Component {
goRoomActionsView={goRoomActionsView} goRoomActionsView={goRoomActionsView}
connecting={connecting} connecting={connecting}
parentTitle={parentTitle} parentTitle={parentTitle}
isGroupChat={isGroupChat}
/> />
); );
} }

View File

@ -306,6 +306,7 @@ class RoomView extends React.Component {
} = this.props; } = this.props;
const { rid, tmid } = this; const { rid, tmid } = this;
const prid = room?.prid; const prid = room?.prid;
const isGroupChat = RocketChat.isGroupChat(room);
let title = route.params?.name; let title = route.params?.name;
let parentTitle; let parentTitle;
if ((room.id || room.rid) && !tmid) { if ((room.id || room.rid) && !tmid) {
@ -356,6 +357,7 @@ class RoomView extends React.Component {
type={t} type={t}
roomUserId={roomUserId} roomUserId={roomUserId}
visitor={visitor} visitor={visitor}
isGroupChat={isGroupChat}
goRoomActionsView={this.goRoomActionsView} goRoomActionsView={this.goRoomActionsView}
/> />
), ),

View File

@ -196,7 +196,7 @@ class Sidebar extends Component {
return ( return (
<SidebarItem <SidebarItem
text={user.statusText || I18n.t('Edit_Status')} text={user.statusText || I18n.t('Edit_Status')}
left={<Status style={styles.status} size={12} status={user && user.status} />} left={<Status size={24} status={user?.status} />}
right={<CustomIcon name='edit' size={20} color={themes[theme].titleText} />} right={<CustomIcon name='edit' size={20} color={themes[theme].titleText} />}
onPress={() => this.sidebarNavigate('StatusView')} onPress={() => this.sidebarNavigate('StatusView')}
testID='sidebar-custom-status' testID='sidebar-custom-status'

View File

@ -51,9 +51,6 @@ export default StyleSheet.create({
avatar: { avatar: {
marginHorizontal: 10 marginHorizontal: 10
}, },
status: {
marginRight: 5
},
currentServerText: { currentServerText: {
fontSize: 14, fontSize: 14,
...sharedStyles.textSemibold ...sharedStyles.textSemibold

View File

@ -41,11 +41,11 @@ const styles = StyleSheet.create({
}, },
inputLeft: { inputLeft: {
position: 'absolute', position: 'absolute',
top: 18, top: 12,
left: 14 left: 12
}, },
inputStyle: { inputStyle: {
paddingLeft: 40 paddingLeft: 48
} }
}); });
@ -140,7 +140,7 @@ class StatusView extends React.Component {
testID={`status-view-current-${ user.status }`} testID={`status-view-current-${ user.status }`}
style={styles.inputLeft} style={styles.inputLeft}
status={user.status} status={user.status}
size={12} size={24}
/> />
)} )}
inputStyle={styles.inputStyle} inputStyle={styles.inputStyle}
@ -174,7 +174,7 @@ class StatusView extends React.Component {
} }
}} }}
testID={`status-view-${ id }`} testID={`status-view-${ id }`}
left={() => <Status size={12} status={item.id} />} left={() => <Status size={24} status={item.id} />}
/> />
); );
} }

View File

@ -26,9 +26,9 @@ export default StyleSheet.create({
}, },
status: { status: {
position: 'absolute', position: 'absolute',
bottom: -3, bottom: -2,
right: -3, right: -2,
borderWidth: 3 borderRadius: 10
}, },
textAlignCenter: { textAlignCenter: {
textAlign: 'center' textAlign: 'center'

Binary file not shown.

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { ScrollView, StyleSheet } from 'react-native'; import { ScrollView, StyleSheet, View } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { themes } from '../../app/constants/colors'; import { themes } from '../../app/constants/colors';
@ -9,11 +9,6 @@ import StoriesSeparator from './StoriesSeparator';
import sharedStyles from '../../app/views/Styles'; import sharedStyles from '../../app/views/Styles';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
status: {
borderWidth: 4,
bottom: -4,
right: -4
},
custom: { custom: {
padding: 16 padding: 16
} }
@ -117,11 +112,12 @@ const AvatarStories = ({ theme }) => (
server={server} server={server}
size={56} size={56}
> >
<View style={[sharedStyles.status, { backgroundColor: themes[theme].backgroundColor }]}>
<Status <Status
size={24} size={20}
style={[sharedStyles.status, styles.status]} status='online'
theme={theme}
/> />
</View>
</Avatar> </Avatar>
<Separator title='Wrong server' theme={theme} /> <Separator title='Wrong server' theme={theme} />
<Avatar <Avatar

View File

@ -65,6 +65,7 @@ export default ({ theme }) => {
<RoomItem status='away' /> <RoomItem status='away' />
<RoomItem status='busy' /> <RoomItem status='busy' />
<RoomItem status='offline' /> <RoomItem status='offline' />
<RoomItem status='loading' />
<RoomItem status='wrong' /> <RoomItem status='wrong' />
<Separator title='Alerts' /> <Separator title='Alerts' />