Room item layout (#835)

This commit is contained in:
Diego Mello 2019-04-18 17:57:35 -03:00 committed by GitHub
parent 03adaa3f81
commit 0266cc2e01
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 403 additions and 347 deletions

View File

@ -20808,6 +20808,10 @@ exports[`Storyshots RoomItem list 1`] = `
View
View
View
View
View
View
View
<Text
style={
Array [

View File

@ -1,73 +1,69 @@
import React, { PureComponent } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import { View, ViewPropTypes } from 'react-native';
import FastImage from 'react-native-fast-image';
export default class Avatar extends PureComponent {
static propTypes = {
baseUrl: PropTypes.string.isRequired,
style: ViewPropTypes.style,
text: PropTypes.string,
avatar: PropTypes.string,
size: PropTypes.number,
borderRadius: PropTypes.number,
type: PropTypes.string,
children: PropTypes.object,
user: PropTypes.shape({
id: PropTypes.string,
token: PropTypes.string
})
const Avatar = React.memo(({
text, size, baseUrl, borderRadius, style, avatar, type, children, userId, token
}) => {
const avatarStyle = {
width: size,
height: size,
borderRadius
};
if (!text && !avatar) {
return null;
}
static defaultProps = {
text: '',
size: 25,
type: 'd',
borderRadius: 4
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 }`;
}
render() {
const {
text, size, baseUrl, borderRadius, style, avatar, type, children, user
} = this.props;
const uri = avatar || `${ baseUrl }/avatar/${ room }?format=png&width=${ uriSize }&height=${ uriSize }${ avatarAuthURLFragment }`;
const avatarStyle = {
width: size,
height: size,
borderRadius
};
const image = (
<FastImage
style={avatarStyle}
source={{
uri,
priority: FastImage.priority.high
}}
/>
);
if (!text && !avatar) {
return null;
}
return (
<View style={[avatarStyle, style]}>
{image}
{children}
</View>
);
});
const room = type === 'd' ? text : `@${ text }`;
Avatar.propTypes = {
baseUrl: PropTypes.string.isRequired,
style: ViewPropTypes.style,
text: PropTypes.string,
avatar: PropTypes.string,
size: PropTypes.number,
borderRadius: PropTypes.number,
type: PropTypes.string,
children: PropTypes.object,
userId: PropTypes.string,
token: PropTypes.string
};
// Avoid requesting several sizes by having only two sizes on cache
const uriSize = size === 100 ? 100 : 50;
Avatar.defaultProps = {
text: '',
size: 25,
type: 'd',
borderRadius: 4
};
let avatarAuthURLFragment = '';
if (user && user.id && user.token) {
avatarAuthURLFragment = `&rc_token=${ user.token }&rc_uid=${ user.id }`;
}
const uri = avatar || `${ baseUrl }/avatar/${ room }?format=png&width=${ uriSize }&height=${ uriSize }${ avatarAuthURLFragment }`;
const image = (
<FastImage
style={avatarStyle}
source={{
uri,
priority: FastImage.priority.high
}}
/>
);
return (
<View style={[avatarStyle, style]}>
{image}
{children}
</View>
);
}
}
export default Avatar;

View File

@ -706,7 +706,8 @@ class MessageBox extends Component {
size={30}
type={item.username ? 'd' : 'c'}
baseUrl={baseUrl}
user={user}
userId={user.id}
token={user.token}
/>,
<Text key='mention-item-name' style={styles.mentionText}>{ item.username || item.name }</Text>
]

View File

@ -238,7 +238,8 @@ export default class Message extends PureComponent {
borderRadius={4}
avatar={avatar}
baseUrl={baseUrl}
user={user}
userId={user.id}
token={user.token}
/>
);
}

View File

@ -1,278 +0,0 @@
import React from 'react';
import moment from 'moment';
import PropTypes from 'prop-types';
import {
View, Text, StyleSheet, PixelRatio
} from 'react-native';
import { connect } from 'react-redux';
import { emojify } from 'react-emojione';
import { RectButton } from 'react-native-gesture-handler';
import Avatar from '../containers/Avatar';
import Status from '../containers/Status';
import RoomTypeIcon from '../containers/RoomTypeIcon';
import I18n from '../i18n';
import sharedStyles from '../views/Styles';
import { COLOR_SEPARATOR, COLOR_PRIMARY, COLOR_WHITE } from '../constants/colors';
export const ROW_HEIGHT = 75 * PixelRatio.getFontScale();
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
marginLeft: 14,
height: ROW_HEIGHT
},
centerContainer: {
flex: 1,
paddingVertical: 10,
paddingRight: 14,
borderBottomWidth: StyleSheet.hairlineWidth,
borderColor: COLOR_SEPARATOR
},
title: {
flex: 1,
fontSize: 17,
lineHeight: 20,
...sharedStyles.textColorNormal,
...sharedStyles.textMedium
},
alert: {
...sharedStyles.textSemibold
},
row: {
flex: 1,
flexDirection: 'row',
alignItems: 'flex-start'
},
titleContainer: {
width: '100%',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center'
},
date: {
fontSize: 13,
marginLeft: 4,
...sharedStyles.textColorDescription,
...sharedStyles.textRegular
},
updateAlert: {
color: COLOR_PRIMARY,
...sharedStyles.textSemibold
},
unreadNumberContainer: {
minWidth: 23,
padding: 3,
borderRadius: 4,
backgroundColor: COLOR_PRIMARY,
alignItems: 'center',
justifyContent: 'center',
marginLeft: 10
},
unreadNumberText: {
color: COLOR_WHITE,
overflow: 'hidden',
fontSize: 13,
...sharedStyles.textRegular,
letterSpacing: 0.56
},
status: {
marginRight: 7,
marginTop: 3
},
markdownText: {
flex: 1,
fontSize: 14,
lineHeight: 17,
...sharedStyles.textRegular,
...sharedStyles.textColorDescription
},
markdownTextAlert: {
...sharedStyles.textColorNormal
},
avatar: {
marginRight: 10
}
});
const renderNumber = (unread, userMentions) => {
if (!unread || unread <= 0) {
return;
}
if (unread >= 1000) {
unread = '999+';
}
if (userMentions > 0) {
unread = `@ ${ unread }`;
}
return (
<View style={styles.unreadNumberContainer}>
<Text style={styles.unreadNumberText}>{ unread }</Text>
</View>
);
};
const attrs = ['name', 'unread', 'userMentions', 'showLastMessage', 'alert', 'type'];
@connect(state => ({
user: {
id: state.login.user && state.login.user.id,
username: state.login.user && state.login.user.username,
token: state.login.user && state.login.user.token
}
}))
export default class RoomItem extends React.Component {
static propTypes = {
type: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
baseUrl: PropTypes.string.isRequired,
showLastMessage: PropTypes.bool,
_updatedAt: PropTypes.string,
lastMessage: PropTypes.object,
alert: PropTypes.bool,
unread: PropTypes.number,
userMentions: PropTypes.number,
id: PropTypes.string,
prid: PropTypes.string,
onPress: PropTypes.func,
user: PropTypes.shape({
id: PropTypes.string,
username: PropTypes.string,
token: PropTypes.string
}),
avatarSize: PropTypes.number,
testID: PropTypes.string,
height: PropTypes.number
}
static defaultProps = {
avatarSize: 48
}
shouldComponentUpdate(nextProps) {
const { lastMessage, _updatedAt } = this.props;
const oldlastMessage = lastMessage;
const newLastmessage = nextProps.lastMessage;
if (oldlastMessage && newLastmessage && oldlastMessage.ts !== newLastmessage.ts) {
return true;
}
if (_updatedAt && nextProps._updatedAt && nextProps._updatedAt !== _updatedAt) {
return true;
}
// eslint-disable-next-line react/destructuring-assignment
return attrs.some(key => nextProps[key] !== this.props[key]);
}
get avatar() {
const {
type, name, avatarSize, baseUrl, user
} = this.props;
return <Avatar text={name} size={avatarSize} type={type} baseUrl={baseUrl} style={styles.avatar} user={user} />;
}
get lastMessage() {
const {
lastMessage, type, showLastMessage, user
} = this.props;
if (!showLastMessage) {
return '';
}
if (!lastMessage) {
return I18n.t('No_Message');
}
let prefix = '';
const me = lastMessage.u.username === user.username;
if (!lastMessage.msg && Object.keys(lastMessage.attachments).length > 0) {
if (me) {
return I18n.t('User_sent_an_attachment', { user: I18n.t('You') });
} else {
return I18n.t('User_sent_an_attachment', { user: lastMessage.u.username });
}
}
if (me) {
prefix = I18n.t('You_colon');
} else if (type !== 'd') {
prefix = `${ lastMessage.u.username }: `;
}
let msg = `${ prefix }${ lastMessage.msg.replace(/[\n\t\r]/igm, '') }`;
msg = emojify(msg, { output: 'unicode' });
return msg;
}
get type() {
const { type, id, prid } = this.props;
if (type === 'd') {
return <Status style={styles.status} size={10} id={id} />;
}
return <RoomTypeIcon type={prid ? 'discussion' : type} />;
}
formatDate = date => moment(date).calendar(null, {
lastDay: `[${ I18n.t('Yesterday') }]`,
sameDay: 'h:mm A',
lastWeek: 'dddd',
sameElse: 'MMM D'
})
render() {
const {
unread, userMentions, name, _updatedAt, alert, testID, height, onPress
} = this.props;
const date = this.formatDate(_updatedAt);
let accessibilityLabel = name;
if (unread === 1) {
accessibilityLabel += `, ${ unread } ${ I18n.t('alert') }`;
} else if (unread > 1) {
accessibilityLabel += `, ${ unread } ${ I18n.t('alerts') }`;
}
if (userMentions > 0) {
accessibilityLabel += `, ${ I18n.t('you_were_mentioned') }`;
}
if (date) {
accessibilityLabel += `, ${ I18n.t('last_message') } ${ date }`;
}
return (
<RectButton
onPress={onPress}
activeOpacity={0.8}
underlayColor='#e1e5e8'
testID={testID}
>
<View
style={[styles.container, height && { height }]}
accessibilityLabel={accessibilityLabel}
>
{this.avatar}
<View style={styles.centerContainer}>
<View style={styles.titleContainer}>
{this.type}
<Text style={[styles.title, alert && styles.alert]} ellipsizeMode='tail' numberOfLines={1}>{ name }</Text>
{_updatedAt ? <Text style={[styles.date, alert && styles.updateAlert]} ellipsizeMode='tail' numberOfLines={1}>{ date }</Text> : null}
</View>
<View style={styles.row}>
<Text style={[styles.markdownText, alert && styles.markdownTextAlert]} numberOfLines={2}>
{this.lastMessage}
</Text>
{renderNumber(unread, userMentions)}
</View>
</View>
</View>
</RectButton>
);
}
}

View File

@ -0,0 +1,60 @@
import React from 'react';
import { Text } from 'react-native';
import { emojify } from 'react-emojione';
import PropTypes from 'prop-types';
import _ from 'lodash';
import I18n from '../../i18n';
import styles from './styles';
const formatMsg = ({
lastMessage, type, showLastMessage, username
}) => {
if (!showLastMessage) {
return '';
}
if (!lastMessage) {
return I18n.t('No_Message');
}
let prefix = '';
const isLastMessageSentByMe = lastMessage.u.username === username;
if (!lastMessage.msg && Object.keys(lastMessage.attachments).length) {
const user = isLastMessageSentByMe ? I18n.t('You') : lastMessage.u.username;
return I18n.t('User_sent_an_attachment', { user });
}
if (isLastMessageSentByMe) {
prefix = I18n.t('You_colon');
} else if (type !== 'd') {
prefix = `${ lastMessage.u.username }: `;
}
let msg = `${ prefix }${ lastMessage.msg.replace(/[\n\t\r]/igm, '') }`;
if (msg) {
msg = emojify(msg, { output: 'unicode' });
}
return msg;
};
const arePropsEqual = (oldProps, newProps) => _.isEqual(oldProps, newProps);
const LastMessage = React.memo(({
lastMessage, type, showLastMessage, username
}) => (
<Text style={[styles.markdownText, alert && styles.markdownTextAlert]} numberOfLines={2}>
{formatMsg({
lastMessage, type, showLastMessage, username
})}
</Text>
), arePropsEqual);
LastMessage.propTypes = {
lastMessage: PropTypes.object,
type: PropTypes.string,
showLastMessage: PropTypes.bool,
username: PropTypes.string
};
export default LastMessage;

View File

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

View File

@ -0,0 +1,29 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View, Text } from 'react-native';
import styles from './styles';
const UnreadBadge = React.memo(({ unread, userMentions, type }) => {
if (!unread || unread <= 0) {
return;
}
if (unread >= 1000) {
unread = '999+';
}
const mentioned = userMentions > 0 && type !== 'd';
return (
<View style={[styles.unreadNumberContainer, mentioned && styles.unreadMentioned]}>
<Text style={styles.unreadNumberText}>{ unread }</Text>
</View>
);
});
UnreadBadge.propTypes = {
unread: PropTypes.number,
userMentions: PropTypes.number,
type: PropTypes.string
};
export default UnreadBadge;

View File

@ -0,0 +1,120 @@
import React from 'react';
import moment from 'moment';
import PropTypes from 'prop-types';
import { View, Text } from 'react-native';
import { connect } from 'react-redux';
import { RectButton } from 'react-native-gesture-handler';
import Avatar from '../../containers/Avatar';
import I18n from '../../i18n';
import styles, { ROW_HEIGHT } from './styles';
import UnreadBadge from './UnreadBadge';
import TypeIcon from './TypeIcon';
import LastMessage from './LastMessage';
export { ROW_HEIGHT };
const attrs = ['name', 'unread', 'userMentions', 'showLastMessage', 'alert', 'type'];
@connect(state => ({
userId: state.login.user && state.login.user.id,
username: state.login.user && state.login.user.username,
token: state.login.user && state.login.user.token
}))
export default class RoomItem extends React.Component {
static propTypes = {
type: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
baseUrl: PropTypes.string.isRequired,
showLastMessage: PropTypes.bool,
_updatedAt: PropTypes.string,
lastMessage: PropTypes.object,
alert: PropTypes.bool,
unread: PropTypes.number,
userMentions: PropTypes.number,
id: PropTypes.string,
prid: PropTypes.string,
onPress: PropTypes.func,
userId: PropTypes.string,
username: PropTypes.string,
token: PropTypes.string,
avatarSize: PropTypes.number,
testID: PropTypes.string,
height: PropTypes.number
}
static defaultProps = {
avatarSize: 48
}
shouldComponentUpdate(nextProps) {
const { lastMessage, _updatedAt } = this.props;
const oldlastMessage = lastMessage;
const newLastmessage = nextProps.lastMessage;
if (oldlastMessage && newLastmessage && oldlastMessage.ts !== newLastmessage.ts) {
return true;
}
if (_updatedAt && nextProps._updatedAt && nextProps._updatedAt !== _updatedAt) {
return true;
}
// eslint-disable-next-line react/destructuring-assignment
return attrs.some(key => nextProps[key] !== this.props[key]);
}
formatDate = date => moment(date).calendar(null, {
lastDay: `[${ I18n.t('Yesterday') }]`,
sameDay: 'h:mm A',
lastWeek: 'dddd',
sameElse: 'MMM D'
})
render() {
const {
unread, userMentions, name, _updatedAt, alert, testID, height, type, avatarSize, baseUrl, userId, username, token, onPress, id, prid, showLastMessage, lastMessage
} = this.props;
const date = this.formatDate(_updatedAt);
let accessibilityLabel = name;
if (unread === 1) {
accessibilityLabel += `, ${ unread } ${ I18n.t('alert') }`;
} else if (unread > 1) {
accessibilityLabel += `, ${ unread } ${ I18n.t('alerts') }`;
}
if (userMentions > 0) {
accessibilityLabel += `, ${ I18n.t('you_were_mentioned') }`;
}
if (date) {
accessibilityLabel += `, ${ I18n.t('last_message') } ${ date }`;
}
return (
<RectButton
onPress={onPress}
activeOpacity={0.8}
underlayColor='#e1e5e8'
testID={testID}
>
<View
style={[styles.container, height && { height }]}
accessibilityLabel={accessibilityLabel}
>
<Avatar text={name} size={avatarSize} type={type} baseUrl={baseUrl} style={styles.avatar} userId={userId} token={token} />
<View style={styles.centerContainer}>
<View style={styles.titleContainer}>
<TypeIcon type={type} id={id} prid={prid} />
<Text style={[styles.title, alert && styles.alert]} ellipsizeMode='tail' numberOfLines={1}>{ name }</Text>
{_updatedAt ? <Text style={[styles.date, alert && styles.updateAlert]} ellipsizeMode='tail' numberOfLines={1}>{ date }</Text> : null}
</View>
<View style={styles.row}>
<LastMessage lastMessage={lastMessage} type={type} showLastMessage={showLastMessage} username={username} />
<UnreadBadge unread={unread} userMentions={userMentions} type={type} />
</View>
</View>
</View>
</RectButton>
);
}
}

View File

@ -0,0 +1,94 @@
import { StyleSheet, PixelRatio } from 'react-native';
import sharedStyles from '../../views/Styles';
import {
COLOR_SEPARATOR, COLOR_PRIMARY, COLOR_WHITE, COLOR_TEXT
} from '../../constants/colors';
export const ROW_HEIGHT = 75 * PixelRatio.getFontScale();
export default StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
marginLeft: 14,
height: ROW_HEIGHT
},
centerContainer: {
flex: 1,
paddingVertical: 10,
paddingRight: 14,
borderBottomWidth: StyleSheet.hairlineWidth,
borderColor: COLOR_SEPARATOR
},
title: {
flex: 1,
fontSize: 17,
lineHeight: 20,
...sharedStyles.textColorNormal,
...sharedStyles.textMedium
},
alert: {
...sharedStyles.textSemibold
},
row: {
flex: 1,
flexDirection: 'row',
alignItems: 'flex-start'
},
titleContainer: {
width: '100%',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center'
},
date: {
fontSize: 13,
marginLeft: 4,
...sharedStyles.textColorDescription,
...sharedStyles.textRegular
},
updateAlert: {
color: COLOR_PRIMARY,
...sharedStyles.textSemibold
},
unreadNumberContainer: {
minWidth: 22,
height: 22,
paddingVertical: 3,
paddingHorizontal: 5,
borderRadius: 14,
backgroundColor: COLOR_TEXT,
alignItems: 'center',
justifyContent: 'center',
marginLeft: 10
},
unreadMentioned: {
backgroundColor: COLOR_PRIMARY
},
unreadNumberText: {
color: COLOR_WHITE,
overflow: 'hidden',
fontSize: 13,
...sharedStyles.textRegular,
letterSpacing: 0.56,
textAlign: 'center'
},
status: {
marginRight: 7,
marginTop: 3
},
markdownText: {
flex: 1,
fontSize: 14,
lineHeight: 17,
...sharedStyles.textRegular,
...sharedStyles.textColorDescription
},
markdownTextAlert: {
...sharedStyles.textColorNormal
},
avatar: {
marginRight: 10
}
});

View File

@ -49,7 +49,7 @@ const UserItem = ({
}) => (
<Touch onPress={onPress} onLongPress={onLongPress} style={styles.button} testID={testID}>
<View style={[styles.container, style]}>
<Avatar text={username} size={30} type='d' style={styles.avatar} baseUrl={baseUrl} user={user} />
<Avatar text={username} size={30} type='d' style={styles.avatar} baseUrl={baseUrl} userId={user.id} token={user.token} />
<View style={styles.textContainer}>
<Text style={styles.name}>{name}</Text>
<Text style={styles.username}>@{username}</Text>

View File

@ -286,7 +286,7 @@ export default class ProfileView extends LoggedView {
return (
<View style={styles.avatarButtons}>
{this.renderAvatarButton({
child: <Avatar text={`@${ user.username }`} size={50} baseUrl={baseUrl} user={user} />,
child: <Avatar text={`@${ user.username }`} size={50} baseUrl={baseUrl} userId={user.id} token={user.token} />,
onPress: () => this.resetAvatar(),
key: 'profile-view-reset-avatar'
})}
@ -305,7 +305,7 @@ export default class ProfileView extends LoggedView {
const { url, blob, contentType } = avatarSuggestions[service];
return this.renderAvatarButton({
key: `profile-view-avatar-${ service }`,
child: <Avatar avatar={url} size={50} baseUrl={baseUrl} user={user} />,
child: <Avatar avatar={url} size={50} baseUrl={baseUrl} userId={user.id} token={user.token} />,
onPress: () => this.setAvatar({
url, data: blob, service, contentType
})
@ -399,7 +399,8 @@ export default class ProfileView extends LoggedView {
avatar={avatar && avatar.url}
size={100}
baseUrl={baseUrl}
user={user}
userId={user.id}
token={user.token}
/>
</View>
<RCTextInput

View File

@ -397,7 +397,8 @@ export default class RoomActionsView extends LoggedView {
style={styles.avatar}
type={t}
baseUrl={baseUrl}
user={user}
userId={user.id}
token={user.token}
>
{t === 'd' ? <Status style={sharedStyles.status} id={member._id} /> : null }
</Avatar>,

View File

@ -234,7 +234,8 @@ export default class RoomInfoView extends LoggedView {
style={styles.avatar}
type={room.t}
baseUrl={baseUrl}
user={user}
userId={user.id}
token={user.token}
>
{room.t === 'd' ? <Status style={[sharedStyles.status, styles.status]} size={24} id={roomUser._id} /> : null}
</Avatar>

View File

@ -230,7 +230,8 @@ export default class Sidebar extends Component {
size={30}
style={styles.avatar}
baseUrl={baseUrl}
user={user}
userId={user.id}
token={user.token}
/>
<View style={styles.headerTextContainer}>
<View style={styles.headerUsername}>

View File

@ -57,6 +57,10 @@ export default (
<RoomItem alert unread={1000} />
<RoomItem alert unread={1} userMentions={1} />
<RoomItem alert unread={1000} userMentions={1} />
<RoomItem alert name='general' unread={1} type='c' />
<RoomItem alert name='general' unread={1000} type='c' />
<RoomItem alert name='general' unread={1} userMentions={1} type='c' />
<RoomItem alert name='general' unread={1000} userMentions={1} type='c' />
<StoriesSeparator title='Last Message' />
<RoomItem