[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',
busy: '#f5455c',
away: '#ffd21f',
offline: '#cbced1'
offline: '#cbced1',
loading: '#9ea2a8'
};
export const SWITCH_TRACK_COLOR = {

View File

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

View File

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

View File

@ -1,32 +1,19 @@
import React from 'react';
import React, { memo } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import Status from './Status';
import { withTheme } from '../../theme';
class StatusContainer extends React.PureComponent {
static propTypes = {
style: PropTypes.any,
size: PropTypes.number,
status: PropTypes.string,
theme: PropTypes.string
};
const StatusContainer = memo(({ style, size = 32, status }) => <Status size={size} style={style} status={status} />);
static defaultProps = {
size: 16
}
render() {
const {
style, size, status, theme
} = this.props;
return <Status size={size} style={style} status={status} theme={theme} />;
}
}
StatusContainer.propTypes = {
style: PropTypes.any,
size: PropTypes.number,
status: PropTypes.string
};
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) {
const { users } = result;
const activeUsers = users.reduce((ret, item) => {
const { _id, status, statusText } = item;
const activeUsers = ids.reduce((ret, id) => {
const user = users.find(u => u._id === id) ?? { _id: id, status: 'offline' };
const { _id, status, statusText } = user;
if (loggedUser && loggedUser.id === _id) {
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}
status={status}
isGroupChat={isGroupChat}
theme={theme}
/>
<Title
name={name}
@ -121,7 +120,6 @@ const RoomItem = ({
prid={prid}
status={status}
isGroupChat={isGroupChat}
theme={theme}
/>
<Title
name={name}

View File

@ -1,21 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
import Status from '../../containers/Status/Status';
import RoomTypeIcon from '../../containers/RoomTypeIcon';
import styles from './styles';
const TypeIcon = React.memo(({
theme, type, prid, status, isGroupChat
}) => {
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} />;
});
type, prid, status, isGroupChat
}) => <RoomTypeIcon type={prid ? 'discussion' : type} isGroupChat={isGroupChat} status={status} />);
TypeIcon.propTypes = {
theme: PropTypes.string,
type: PropTypes.string,
status: PropTypes.string,
prid: PropTypes.string,

View File

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

View File

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

View File

@ -448,14 +448,20 @@ class RoomActionsView extends React.Component {
type={t}
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>
<View style={styles.roomTitleContainer}>
{room.t === 'd'
? <Text style={[styles.roomTitle, { color: themes[theme].titleText }]} numberOfLines={1}>{room.fname}</Text>
: (
<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>
</View>
)

View File

@ -41,7 +41,7 @@ const getRoomTitle = (room, type, name, username, statusText, theme) => (type ==
)
: (
<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>
</View>
)
@ -290,7 +290,13 @@ class RoomInfoView extends React.Component {
size={100}
rid={room?.rid}
>
{this.t === 'd' && roomUser._id ? <Status style={[sharedStyles.status, styles.status]} theme={theme} size={24} id={roomUser._id} /> : null}
{this.t === 'd' && roomUser._id
? (
<View style={[sharedStyles.status, { backgroundColor: themes[theme].auxiliaryBackground }]}>
<Status size={20} id={roomUser._id} />
</View>
)
: null}
</Avatar>
);
}

View File

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

View File

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

View File

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

View File

@ -196,7 +196,7 @@ class Sidebar extends Component {
return (
<SidebarItem
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} />}
onPress={() => this.sidebarNavigate('StatusView')}
testID='sidebar-custom-status'

View File

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

View File

@ -41,11 +41,11 @@ const styles = StyleSheet.create({
},
inputLeft: {
position: 'absolute',
top: 18,
left: 14
top: 12,
left: 12
},
inputStyle: {
paddingLeft: 40
paddingLeft: 48
}
});
@ -140,7 +140,7 @@ class StatusView extends React.Component {
testID={`status-view-current-${ user.status }`}
style={styles.inputLeft}
status={user.status}
size={12}
size={24}
/>
)}
inputStyle={styles.inputStyle}
@ -174,7 +174,7 @@ class StatusView extends React.Component {
}
}}
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: {
position: 'absolute',
bottom: -3,
right: -3,
borderWidth: 3
bottom: -2,
right: -2,
borderRadius: 10
},
textAlignCenter: {
textAlign: 'center'

Binary file not shown.

View File

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

View File

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