From 0266cc2e0116fa38c1bfc2212809d4e0aca8d1fc Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Thu, 18 Apr 2019 17:57:35 -0300 Subject: [PATCH] Room item layout (#835) --- .../__snapshots__/Storyshots.test.js.snap | 4 + app/containers/Avatar.js | 116 ++++---- app/containers/MessageBox/index.js | 3 +- app/containers/message/Message.js | 3 +- app/presentation/RoomItem.js | 278 ------------------ app/presentation/RoomItem/LastMessage.js | 60 ++++ app/presentation/RoomItem/TypeIcon.js | 21 ++ app/presentation/RoomItem/UnreadBadge.js | 29 ++ app/presentation/RoomItem/index.js | 120 ++++++++ app/presentation/RoomItem/styles.js | 94 ++++++ app/presentation/UserItem.js | 2 +- app/views/ProfileView/index.js | 7 +- app/views/RoomActionsView/index.js | 3 +- app/views/RoomInfoView/index.js | 3 +- app/views/SidebarView/index.js | 3 +- storybook/stories/RoomItem.js | 4 + 16 files changed, 403 insertions(+), 347 deletions(-) delete mode 100644 app/presentation/RoomItem.js create mode 100644 app/presentation/RoomItem/LastMessage.js create mode 100644 app/presentation/RoomItem/TypeIcon.js create mode 100644 app/presentation/RoomItem/UnreadBadge.js create mode 100644 app/presentation/RoomItem/index.js create mode 100644 app/presentation/RoomItem/styles.js diff --git a/__tests__/__snapshots__/Storyshots.test.js.snap b/__tests__/__snapshots__/Storyshots.test.js.snap index 8975a82c..a7f48740 100644 --- a/__tests__/__snapshots__/Storyshots.test.js.snap +++ b/__tests__/__snapshots__/Storyshots.test.js.snap @@ -20808,6 +20808,10 @@ exports[`Storyshots RoomItem list 1`] = ` View View View + View + View + View + View { + 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 = ( + + ); - if (!text && !avatar) { - return null; - } + return ( + + {image} + {children} + + ); +}); - 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 = ( - - ); - - return ( - - {image} - {children} - - ); - } -} +export default Avatar; diff --git a/app/containers/MessageBox/index.js b/app/containers/MessageBox/index.js index a730bda8..298e89ed 100644 --- a/app/containers/MessageBox/index.js +++ b/app/containers/MessageBox/index.js @@ -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} />, { item.username || item.name } ] diff --git a/app/containers/message/Message.js b/app/containers/message/Message.js index 944fd99b..34df67fe 100644 --- a/app/containers/message/Message.js +++ b/app/containers/message/Message.js @@ -238,7 +238,8 @@ export default class Message extends PureComponent { borderRadius={4} avatar={avatar} baseUrl={baseUrl} - user={user} + userId={user.id} + token={user.token} /> ); } diff --git a/app/presentation/RoomItem.js b/app/presentation/RoomItem.js deleted file mode 100644 index 078b98bc..00000000 --- a/app/presentation/RoomItem.js +++ /dev/null @@ -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 ( - - { unread } - - ); -}; - -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 ; - } - - 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 ; - } - return ; - } - - 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 ( - - - {this.avatar} - - - {this.type} - { name } - {_updatedAt ? { date } : null} - - - - {this.lastMessage} - - {renderNumber(unread, userMentions)} - - - - - ); - } -} diff --git a/app/presentation/RoomItem/LastMessage.js b/app/presentation/RoomItem/LastMessage.js new file mode 100644 index 00000000..c89b391f --- /dev/null +++ b/app/presentation/RoomItem/LastMessage.js @@ -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 +}) => ( + + {formatMsg({ + lastMessage, type, showLastMessage, username + })} + +), arePropsEqual); + +LastMessage.propTypes = { + lastMessage: PropTypes.object, + type: PropTypes.string, + showLastMessage: PropTypes.bool, + username: PropTypes.string +}; + +export default LastMessage; diff --git a/app/presentation/RoomItem/TypeIcon.js b/app/presentation/RoomItem/TypeIcon.js new file mode 100644 index 00000000..8a8c3ef5 --- /dev/null +++ b/app/presentation/RoomItem/TypeIcon.js @@ -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 ; + } + return ; +}); + +TypeIcon.propTypes = { + type: PropTypes.string, + id: PropTypes.string, + prid: PropTypes.string +}; + +export default TypeIcon; diff --git a/app/presentation/RoomItem/UnreadBadge.js b/app/presentation/RoomItem/UnreadBadge.js new file mode 100644 index 00000000..d8ec84eb --- /dev/null +++ b/app/presentation/RoomItem/UnreadBadge.js @@ -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 ( + + { unread } + + ); +}); + +UnreadBadge.propTypes = { + unread: PropTypes.number, + userMentions: PropTypes.number, + type: PropTypes.string +}; + +export default UnreadBadge; diff --git a/app/presentation/RoomItem/index.js b/app/presentation/RoomItem/index.js new file mode 100644 index 00000000..38f50da3 --- /dev/null +++ b/app/presentation/RoomItem/index.js @@ -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 ( + + + + + + + { name } + {_updatedAt ? { date } : null} + + + + + + + + + ); + } +} diff --git a/app/presentation/RoomItem/styles.js b/app/presentation/RoomItem/styles.js new file mode 100644 index 00000000..9c941ec1 --- /dev/null +++ b/app/presentation/RoomItem/styles.js @@ -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 + } +}); diff --git a/app/presentation/UserItem.js b/app/presentation/UserItem.js index a3d91cfe..c767b446 100644 --- a/app/presentation/UserItem.js +++ b/app/presentation/UserItem.js @@ -49,7 +49,7 @@ const UserItem = ({ }) => ( - + {name} @{username} diff --git a/app/views/ProfileView/index.js b/app/views/ProfileView/index.js index 73e827fb..c7b98c87 100644 --- a/app/views/ProfileView/index.js +++ b/app/views/ProfileView/index.js @@ -286,7 +286,7 @@ export default class ProfileView extends LoggedView { return ( {this.renderAvatarButton({ - child: , + child: , 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: , + child: , 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} /> {t === 'd' ? : null } , diff --git a/app/views/RoomInfoView/index.js b/app/views/RoomInfoView/index.js index 3273fe2a..0ed9f148 100644 --- a/app/views/RoomInfoView/index.js +++ b/app/views/RoomInfoView/index.js @@ -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' ? : null} diff --git a/app/views/SidebarView/index.js b/app/views/SidebarView/index.js index 49e848f1..ffb234ed 100644 --- a/app/views/SidebarView/index.js +++ b/app/views/SidebarView/index.js @@ -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} /> diff --git a/storybook/stories/RoomItem.js b/storybook/stories/RoomItem.js index 6e4af5c3..48fddcb1 100644 --- a/storybook/stories/RoomItem.js +++ b/storybook/stories/RoomItem.js @@ -57,6 +57,10 @@ export default ( + + + +