diff --git a/__tests__/__snapshots__/Storyshots.test.js.snap b/__tests__/__snapshots__/Storyshots.test.js.snap index 3777a6ea4f9223a6344a19dd44ff3610227983ea..335bfb8b39081fa9e13cc8af6c513d9a5563744f 100644 --- a/__tests__/__snapshots__/Storyshots.test.js.snap +++ b/__tests__/__snapshots__/Storyshots.test.js.snap @@ -9216,6 +9216,612 @@ exports[`Storyshots Message list 1`] = ` </View> </View> </View> + <Text + style={ + Array [ + Object { + "fontSize": 20, + "fontWeight": "300", + "marginLeft": 10, + "marginTop": 30, + }, + Object { + "marginBottom": 0, + "marginTop": 30, + }, + ] + } + > + Message with read receipt + </Text> + <View + accessible={true} + isTVSelectable={true} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + Object { + "opacity": 1, + } + } + > + <View> + <View + style={ + Array [ + Object { + "flexDirection": "column", + "paddingHorizontal": 14, + "paddingVertical": 4, + "width": "100%", + }, + undefined, + undefined, + ] + } + > + <View + style={ + Object { + "flexDirection": "row", + } + } + > + <View + style={ + Array [ + Object { + "borderRadius": 4, + "height": 36, + "width": 36, + }, + Object { + "marginTop": 4, + }, + ] + } + > + <View + style={ + Array [ + Object { + "overflow": "hidden", + }, + Object { + "borderRadius": 4, + "height": 36, + "width": 36, + }, + ] + } + > + <FastImageView + resizeMode="cover" + source={ + Object { + "priority": "high", + "uri": "https://open.rocket.chat/avatar/diego.mello?format=png&width=50&height=50&rc_token=79q6lH40W4ZRGLOshDiDiVlQaCc4f_lU9HNdHLAzuHz&rc_uid=y8bd77ptZswPj3EW8", + } + } + style={ + Object { + "bottom": 0, + "left": 0, + "position": "absolute", + "right": 0, + "top": 0, + } + } + /> + </View> + </View> + <View + style={ + Array [ + Object { + "flex": 1, + "marginLeft": 46, + }, + Object { + "marginLeft": 10, + }, + ] + } + > + <View + style={ + Object { + "alignItems": "center", + "flex": 1, + "flexDirection": "row", + } + } + > + <View + style={ + Object { + "alignItems": "center", + "flex": 1, + "flexDirection": "row", + } + } + > + <Text + numberOfLines={1} + style={ + Object { + "backgroundColor": "transparent", + "color": "#2F343D", + "fontFamily": "System", + "fontSize": 16, + "fontWeight": "500", + "lineHeight": 22, + } + } + > + diego.mello + </Text> + </View> + <Text + style={ + Object { + "backgroundColor": "transparent", + "color": "#9ca2a8", + "fontFamily": "System", + "fontSize": 12, + "fontWeight": "300", + "lineHeight": 22, + "paddingLeft": 10, + } + } + > + 10:00 AM + </Text> + </View> + <View + style={Object {}} + > + <Text + numberOfLines={0} + style={ + Object { + "alignItems": "flex-start", + "flexDirection": "row", + "flexWrap": "wrap", + "justifyContent": "flex-start", + "marginBottom": 0, + "marginTop": 0, + } + } + > + <Text + style={ + Object { + "backgroundColor": "transparent", + "color": "#2F343D", + "fontFamily": "System", + "fontSize": 16, + "fontWeight": "400", + } + } + > + <Text> + I’m fine! + </Text> + </Text> + </Text> + </View> + </View> + </View> + </View> + </View> + </View> + <View + accessible={true} + isTVSelectable={true} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + Object { + "opacity": 1, + } + } + > + <View> + <View + style={ + Array [ + Object { + "flexDirection": "column", + "paddingHorizontal": 14, + "paddingVertical": 4, + "width": "100%", + }, + undefined, + undefined, + ] + } + > + <View + style={ + Object { + "flexDirection": "row", + } + } + > + <View + style={ + Array [ + Object { + "flex": 1, + "marginLeft": 46, + }, + false, + ] + } + > + <View + style={Object {}} + > + <Text + numberOfLines={0} + style={ + Object { + "alignItems": "flex-start", + "flexDirection": "row", + "flexWrap": "wrap", + "justifyContent": "flex-start", + "marginBottom": 0, + "marginTop": 0, + } + } + > + <Text + style={ + Object { + "backgroundColor": "transparent", + "color": "#2F343D", + "fontFamily": "System", + "fontSize": 16, + "fontWeight": "400", + } + } + > + <Text> + I’m fine! + </Text> + </Text> + </Text> + </View> + </View> + </View> + </View> + </View> + </View> + <View + accessible={true} + isTVSelectable={true} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + Object { + "opacity": 1, + } + } + > + <View> + <View + style={ + Array [ + Object { + "flexDirection": "column", + "paddingHorizontal": 14, + "paddingVertical": 4, + "width": "100%", + }, + undefined, + undefined, + ] + } + > + <View + style={ + Object { + "flexDirection": "row", + } + } + > + <View + style={ + Array [ + Object { + "borderRadius": 4, + "height": 36, + "width": 36, + }, + Object { + "marginTop": 4, + }, + ] + } + > + <View + style={ + Array [ + Object { + "overflow": "hidden", + }, + Object { + "borderRadius": 4, + "height": 36, + "width": 36, + }, + ] + } + > + <FastImageView + resizeMode="cover" + source={ + Object { + "priority": "high", + "uri": "https://open.rocket.chat/avatar/diego.mello?format=png&width=50&height=50&rc_token=79q6lH40W4ZRGLOshDiDiVlQaCc4f_lU9HNdHLAzuHz&rc_uid=y8bd77ptZswPj3EW8", + } + } + style={ + Object { + "bottom": 0, + "left": 0, + "position": "absolute", + "right": 0, + "top": 0, + } + } + /> + </View> + </View> + <View + style={ + Array [ + Object { + "flex": 1, + "marginLeft": 46, + }, + Object { + "marginLeft": 10, + }, + ] + } + > + <View + style={ + Object { + "alignItems": "center", + "flex": 1, + "flexDirection": "row", + } + } + > + <View + style={ + Object { + "alignItems": "center", + "flex": 1, + "flexDirection": "row", + } + } + > + <Text + numberOfLines={1} + style={ + Object { + "backgroundColor": "transparent", + "color": "#2F343D", + "fontFamily": "System", + "fontSize": 16, + "fontWeight": "500", + "lineHeight": 22, + } + } + > + diego.mello + </Text> + </View> + <Text + style={ + Object { + "backgroundColor": "transparent", + "color": "#9ca2a8", + "fontFamily": "System", + "fontSize": 12, + "fontWeight": "300", + "lineHeight": 22, + "paddingLeft": 10, + } + } + > + 10:00 AM + </Text> + </View> + <View + style={Object {}} + > + <Text + numberOfLines={0} + style={ + Object { + "alignItems": "flex-start", + "flexDirection": "row", + "flexWrap": "wrap", + "justifyContent": "flex-start", + "marginBottom": 0, + "marginTop": 0, + } + } + > + <Text + style={ + Object { + "backgroundColor": "transparent", + "color": "#2F343D", + "fontFamily": "System", + "fontSize": 16, + "fontWeight": "400", + } + } + > + <Text> + I’m fine! + </Text> + </Text> + </Text> + </View> + </View> + <Text + allowFontScaling={false} + style={ + Array [ + Object { + "color": "#1d74f5", + "fontSize": 15, + }, + Object { + "lineHeight": 20, + }, + Object { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + Object {}, + ] + } + > +  + </Text> + </View> + </View> + </View> + </View> + <View + accessible={true} + isTVSelectable={true} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + Object { + "opacity": 1, + } + } + > + <View> + <View + style={ + Array [ + Object { + "flexDirection": "column", + "paddingHorizontal": 14, + "paddingVertical": 4, + "width": "100%", + }, + undefined, + undefined, + ] + } + > + <View + style={ + Object { + "flexDirection": "row", + } + } + > + <View + style={ + Array [ + Object { + "flex": 1, + "marginLeft": 46, + }, + false, + ] + } + > + <View + style={Object {}} + > + <Text + numberOfLines={0} + style={ + Object { + "alignItems": "flex-start", + "flexDirection": "row", + "flexWrap": "wrap", + "justifyContent": "flex-start", + "marginBottom": 0, + "marginTop": 0, + } + } + > + <Text + style={ + Object { + "backgroundColor": "transparent", + "color": "#2F343D", + "fontFamily": "System", + "fontSize": 16, + "fontWeight": "400", + } + } + > + <Text> + I’m fine! + </Text> + </Text> + </Text> + </View> + </View> + <Text + allowFontScaling={false} + style={ + Array [ + Object { + "color": "#1d74f5", + "fontSize": 15, + }, + Object { + "lineHeight": 20, + }, + Object { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + Object {}, + ] + } + > +  + </Text> + </View> + </View> + </View> + </View> <Text style={ Array [ diff --git a/app/constants/settings.js b/app/constants/settings.js index f22949ea0179076338b1086de6d534943f9d4cc5..1ae229a1823e308e844b611ee5da3a7d940d82a1 100644 --- a/app/constants/settings.js +++ b/app/constants/settings.js @@ -59,6 +59,12 @@ export default { Assets_favicon_512: { type: null }, + Message_Read_Receipt_Enabled: { + type: 'valueAsBoolean' + }, + Message_Read_Receipt_Store_Users: { + type: 'valueAsBoolean' + }, Threads_enabled: { type: null }, diff --git a/app/containers/MessageActions.js b/app/containers/MessageActions.js index 9667095b626c8b109f54b3d72fe9846da994e187..bc798c77201dd635bba504651861ab757147d4e1 100644 --- a/app/containers/MessageActions.js +++ b/app/containers/MessageActions.js @@ -17,6 +17,7 @@ import { vibrate } from '../utils/vibration'; import RocketChat from '../lib/rocketchat'; import I18n from '../i18n'; import log from '../utils/log'; +import Navigation from '../lib/Navigation'; @connect( state => ({ @@ -26,7 +27,8 @@ import log from '../utils/log'; Message_AllowEditing: state.settings.Message_AllowEditing, Message_AllowEditing_BlockEditInMinutes: state.settings.Message_AllowEditing_BlockEditInMinutes, Message_AllowPinning: state.settings.Message_AllowPinning, - Message_AllowStarring: state.settings.Message_AllowStarring + Message_AllowStarring: state.settings.Message_AllowStarring, + Message_Read_Receipt_Store_Users: state.settings.Message_Read_Receipt_Store_Users }), dispatch => ({ actionsHide: () => dispatch(actionsHideAction()), @@ -56,7 +58,8 @@ export default class MessageActions extends React.Component { Message_AllowEditing: PropTypes.bool, Message_AllowEditing_BlockEditInMinutes: PropTypes.number, Message_AllowPinning: PropTypes.bool, - Message_AllowStarring: PropTypes.bool + Message_AllowStarring: PropTypes.bool, + Message_Read_Receipt_Store_Users: PropTypes.bool }; constructor(props) { @@ -64,7 +67,7 @@ export default class MessageActions extends React.Component { this.handleActionPress = this.handleActionPress.bind(this); this.setPermissions(); - const { Message_AllowStarring, Message_AllowPinning } = this.props; + const { Message_AllowStarring, Message_AllowPinning, Message_Read_Receipt_Store_Users } = this.props; // Cancel this.options = [I18n.t('Cancel')]; @@ -118,6 +121,12 @@ export default class MessageActions extends React.Component { this.REACTION_INDEX = this.options.length - 1; } + // Read Receipts + if (Message_Read_Receipt_Store_Users) { + this.options.push(I18n.t('Read_Receipt')); + this.READ_RECEIPT_INDEX = this.options.length - 1; + } + // Report this.options.push(I18n.t('Report')); this.REPORT_INDEX = this.options.length - 1; @@ -302,6 +311,11 @@ export default class MessageActions extends React.Component { toggleReactionPicker(actionMessage); } + handleReadReceipt = () => { + const { actionMessage } = this.props; + Navigation.navigate('ReadReceiptsView', { messageId: actionMessage._id }); + } + handleReport = async() => { const { actionMessage } = this.props; try { @@ -348,6 +362,9 @@ export default class MessageActions extends React.Component { case this.DELETE_INDEX: this.handleDelete(); break; + case this.READ_RECEIPT_INDEX: + this.handleReadReceipt(); + break; default: break; } diff --git a/app/containers/message/Message.js b/app/containers/message/Message.js index 1ba56ee40de20ebc5ec14d7356acbca5050e557d..d10a5262f6f4e0d2273c3f9ad728bf677b0cab9a 100644 --- a/app/containers/message/Message.js +++ b/app/containers/message/Message.js @@ -16,6 +16,7 @@ import Reactions from './Reactions'; import Broadcast from './Broadcast'; import Discussion from './Discussion'; import Content from './Content'; +import ReadReceipt from './ReadReceipt'; const MessageInner = React.memo((props) => { if (props.type === 'discussion-created') { @@ -72,6 +73,10 @@ const Message = React.memo((props) => { > <MessageInner {...props} /> </View> + <ReadReceipt + isReadReceiptEnabled={props.isReadReceiptEnabled} + unread={props.unread} + /> </View> </View> ); @@ -119,7 +124,9 @@ Message.propTypes = { hasError: PropTypes.bool, style: PropTypes.any, onLongPress: PropTypes.func, - onPress: PropTypes.func + onPress: PropTypes.func, + isReadReceiptEnabled: PropTypes.bool, + unread: PropTypes.bool }; MessageInner.propTypes = { diff --git a/app/containers/message/ReadReceipt.js b/app/containers/message/ReadReceipt.js new file mode 100644 index 0000000000000000000000000000000000000000..c407e021d69e2ee24c903cc17cb3de1b31588dd4 --- /dev/null +++ b/app/containers/message/ReadReceipt.js @@ -0,0 +1,21 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { COLOR_PRIMARY } from '../../constants/colors'; +import { CustomIcon } from '../../lib/Icons'; +import styles from './styles'; + +const ReadReceipt = React.memo(({ isReadReceiptEnabled, unread }) => { + if (isReadReceiptEnabled && !unread && unread !== null) { + return <CustomIcon name='check' color={COLOR_PRIMARY} size={15} style={styles.readReceipt} />; + } + return null; +}); +ReadReceipt.displayName = 'MessageReadReceipt'; + +ReadReceipt.propTypes = { + isReadReceiptEnabled: PropTypes.bool, + unread: PropTypes.bool +}; + +export default ReadReceipt; diff --git a/app/containers/message/index.js b/app/containers/message/index.js index 1f76d5aeb389e56ceaf0aee93c35af795bf2029e..478055ad09a58e7799117a12977b0560d0b2feea 100644 --- a/app/containers/message/index.js +++ b/app/containers/message/index.js @@ -24,6 +24,7 @@ export default class MessageContainer extends React.Component { _updatedAt: PropTypes.instanceOf(Date), baseUrl: PropTypes.string, Message_GroupingPeriod: PropTypes.number, + isReadReceiptEnabled: PropTypes.bool, useRealName: PropTypes.bool, useMarkdown: PropTypes.bool, status: PropTypes.number, @@ -57,6 +58,9 @@ export default class MessageContainer extends React.Component { if (item.tmsg !== nextProps.item.tmsg) { return true; } + if (item.unread !== nextProps.item.unread) { + return true; + } return _updatedAt.toISOString() !== nextProps._updatedAt.toISOString(); } @@ -187,10 +191,10 @@ export default class MessageContainer extends React.Component { render() { const { - item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, customThreadTimeFormat, onOpenFileModal, timeFormat, useMarkdown + item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, customThreadTimeFormat, onOpenFileModal, timeFormat, useMarkdown, isReadReceiptEnabled } = this.props; const { - _id, msg, ts, attachments, urls, reactions, t, avatar, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg, mentions, channels + _id, msg, ts, attachments, urls, reactions, t, avatar, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg, mentions, channels, unread } = item; return ( @@ -213,6 +217,8 @@ export default class MessageContainer extends React.Component { broadcast={broadcast} baseUrl={baseUrl} useRealName={useRealName} + isReadReceiptEnabled={isReadReceiptEnabled} + unread={unread} role={role} drid={drid} dcount={dcount} diff --git a/app/containers/message/styles.js b/app/containers/message/styles.js index 4066b779a97d9df1f4635d610cf7e9044cd817d2..c08467ef094d2bf3b0bb2a8ae9938108ff84a8e8 100644 --- a/app/containers/message/styles.js +++ b/app/containers/message/styles.js @@ -234,5 +234,8 @@ export default StyleSheet.create({ flex: 1, color: COLOR_PRIMARY, ...sharedStyles.textRegular + }, + readReceipt: { + lineHeight: 20 } }); diff --git a/app/i18n/locales/en.js b/app/i18n/locales/en.js index a426f0ddb4ee6763c68a0a902aeea2931362a62b..b11c47cc508af96ddff17fd2574acaf04fef512b 100644 --- a/app/i18n/locales/en.js +++ b/app/i18n/locales/en.js @@ -233,6 +233,7 @@ export default { No_Message: 'No Message', No_messages_yet: 'No messages yet', No_Reactions: 'No Reactions', + No_Read_Receipts: 'No Read Receipts', Not_logged: 'Not logged', Nothing_to_save: 'Nothing to save!', Notify_active_in_this_room: 'Notify active users in this room', @@ -265,6 +266,7 @@ export default { Reactions: 'Reactions', Read_Only_Channel: 'Read Only Channel', Read_Only: 'Read Only', + Read_Receipt: 'Read Receipt', Register: 'Register', Repeat_Password: 'Repeat Password', Replied_on: 'Replied on:', diff --git a/app/i18n/locales/pt-BR.js b/app/i18n/locales/pt-BR.js index ef13d05cfec0803f080c77af1b32177e0ee927c9..b22686c10ec23873a67815c88d8f6aadc610f7a3 100644 --- a/app/i18n/locales/pt-BR.js +++ b/app/i18n/locales/pt-BR.js @@ -266,6 +266,7 @@ export default { Read_Only_Channel: 'Canal Somente Leitura', Read_Only: 'Somente Leitura', Register: 'Registrar', + Read_Receipt: 'Lida por', Repeat_Password: 'Repetir Senha', Replied_on: 'Respondido em:', replies: 'respostas', diff --git a/app/index.js b/app/index.js index 7a08db17ded0f7b7357941f07c119cc647a8fa0f..65b001b29f29a7a495d91b4360d22d542cdedf02 100644 --- a/app/index.js +++ b/app/index.js @@ -29,6 +29,7 @@ import RoomInfoView from './views/RoomInfoView'; import RoomInfoEditView from './views/RoomInfoEditView'; import RoomMembersView from './views/RoomMembersView'; import SearchMessagesView from './views/SearchMessagesView'; +import ReadReceiptsView from './views/ReadReceiptView'; import ThreadMessagesView from './views/ThreadMessagesView'; import MessagesView from './views/MessagesView'; import SelectedUsersView from './views/SelectedUsersView'; @@ -114,6 +115,7 @@ const ChatsStack = createStackNavigator({ SelectedUsersView, ThreadMessagesView, MessagesView, + ReadReceiptsView, DirectoryView }, { defaultNavigationOptions: defaultHeader diff --git a/app/lib/methods/helpers/normalizeMessage.js b/app/lib/methods/helpers/normalizeMessage.js index ee0824889afeea475ab71805467d0d6d37cf7dc1..39fa9dae0500bd62728bb6ba68b9e8164e899aa9 100644 --- a/app/lib/methods/helpers/normalizeMessage.js +++ b/app/lib/methods/helpers/normalizeMessage.js @@ -26,6 +26,7 @@ export default (msg) => { msg = normalizeAttachments(msg); msg.reactions = msg.reactions || []; + msg.unread = msg.unread || false; // TODO: api problems // if (Array.isArray(msg.reactions)) { // msg.reactions = msg.reactions.map((value, key) => ({ emoji: key, usernames: value.usernames.map(username => ({ value: username })) })); diff --git a/app/lib/realm.js b/app/lib/realm.js index fd14cda030c34905f4541b651147d87c7d7918f8..d750d7ab7e60e116f908bd2fda4f388cb08be224 100644 --- a/app/lib/realm.js +++ b/app/lib/realm.js @@ -197,7 +197,8 @@ const messagesSchema = { tlm: { type: 'date', optional: true }, replies: 'string[]', mentions: { type: 'list', objectType: 'users' }, - channels: { type: 'list', objectType: 'rooms' } + channels: { type: 'list', objectType: 'rooms' }, + unread: { type: 'bool', optional: true } } }; @@ -415,7 +416,7 @@ class DB { return this.databases.activeDB = new Realm({ path: `${ path }.realm`, schema, - schemaVersion: 11, + schemaVersion: 12, migration: (oldRealm, newRealm) => { if (oldRealm.schemaVersion >= 3 && newRealm.schemaVersion <= 11) { const newSubs = newRealm.objects('subscriptions'); diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index 737e58b8fdd2c70002e03729c8a11fc8f57a10af..4bf66959fb7f9890485d63714b278226988c9090 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -771,6 +771,12 @@ const RocketChat = { sort: { ts: -1 } }); }, + + getReadReceipts(messageId) { + return this.sdk.get('chat.getMessageReadReceipts', { + messageId + }); + }, searchMessages(roomId, searchText) { // RC 0.60.0 return this.sdk.get('chat.search', { diff --git a/app/views/ReadReceiptView/index.js b/app/views/ReadReceiptView/index.js new file mode 100644 index 0000000000000000000000000000000000000000..9c90e8b52b6251b95d7a717dfdeada124c3d2d4b --- /dev/null +++ b/app/views/ReadReceiptView/index.js @@ -0,0 +1,146 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { FlatList, View, Text } from 'react-native'; +import { SafeAreaView } from 'react-navigation'; +import equal from 'deep-equal'; +import moment from 'moment'; +import { connect } from 'react-redux'; + +import Avatar from '../../containers/Avatar'; +import styles from './styles'; +import RCActivityIndicator from '../../containers/ActivityIndicator'; +import I18n from '../../i18n'; +import RocketChat from '../../lib/rocketchat'; +import StatusBar from '../../containers/StatusBar'; + +@connect(state => ({ + Message_TimeFormat: state.settings.Message_TimeFormat, + baseUrl: state.settings.Site_Url || state.server ? state.server.server : '', + userId: state.login.user && state.login.user.id, + token: state.login.user && state.login.user.token +})) +export default class ReadReceiptsView extends React.Component { + static navigationOptions = { + title: I18n.t('Read_Receipt') + } + + static propTypes = { + navigation: PropTypes.object, + Message_TimeFormat: PropTypes.string, + baseUrl: PropTypes.string, + userId: PropTypes.string, + token: PropTypes.string + } + + constructor(props) { + super(props); + this.messageId = props.navigation.getParam('messageId'); + this.state = { + loading: false, + receipts: [] + }; + } + + componentDidMount() { + this.load(); + } + + shouldComponentUpdate(nextProps, nextState) { + const { loading, receipts } = this.state; + if (nextState.loading !== loading) { + return true; + } + if (!equal(nextState.receipts, receipts)) { + return true; + } + return false; + } + + load = async() => { + const { loading } = this.state; + if (loading) { + return; + } + + this.setState({ loading: true }); + + try { + const result = await RocketChat.getReadReceipts(this.messageId); + if (result.success) { + this.setState({ + receipts: result.receipts, + loading: false + }); + } + } catch (error) { + this.setState({ loading: false }); + console.log('err_fetch_read_receipts', error); + } + } + + renderEmpty = () => ( + <View style={styles.listEmptyContainer} testID='read-receipt-view'> + <Text>{I18n.t('No_Read_Receipts')}</Text> + </View> + ) + + renderItem = ({ item }) => { + const { + Message_TimeFormat, userId, baseUrl, token + } = this.props; + const time = moment(item.ts).format(Message_TimeFormat); + return ( + <View style={styles.itemContainer}> + <Avatar + text={item.user.username} + size={40} + baseUrl={baseUrl} + userId={userId} + token={token} + /> + <View style={styles.infoContainer}> + <View style={styles.item}> + <Text style={styles.name}> + {item.user.name} + </Text> + <Text> + {time} + </Text> + </View> + <Text> + {`@${ item.user.username }`} + </Text> + </View> + </View> + ); + } + + renderSeparator = () => <View style={styles.separator} />; + + render() { + const { receipts, loading } = this.state; + + if (!loading && receipts.length === 0) { + return this.renderEmpty(); + } + + return ( + <SafeAreaView style={styles.container} testID='read-receipt-view' forceInset={{ bottom: 'always' }}> + <StatusBar /> + <View> + {loading + ? <RCActivityIndicator /> + : ( + <FlatList + data={receipts} + renderItem={this.renderItem} + ItemSeparatorComponent={this.renderSeparator} + style={styles.list} + keyExtractor={item => item._id} + /> + )} + </View> + </SafeAreaView> + ); + } +} diff --git a/app/views/ReadReceiptView/styles.js b/app/views/ReadReceiptView/styles.js new file mode 100644 index 0000000000000000000000000000000000000000..731fe8f1dca67913907ef7113b99b4e99c95fc97 --- /dev/null +++ b/app/views/ReadReceiptView/styles.js @@ -0,0 +1,50 @@ +import { StyleSheet } from 'react-native'; +import { COLOR_SEPARATOR, COLOR_WHITE, COLOR_BACKGROUND_CONTAINER } from '../../constants/colors'; +import sharedStyles from '../Styles'; + +export default StyleSheet.create({ + listEmptyContainer: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: COLOR_BACKGROUND_CONTAINER + }, + item: { + flex: 1, + flexDirection: 'row', + justifyContent: 'space-between' + }, + separator: { + height: StyleSheet.hairlineWidth, + backgroundColor: COLOR_SEPARATOR + }, + name: { + ...sharedStyles.textRegular, + ...sharedStyles.textColorTitle, + fontSize: 17 + }, + username: { + flex: 1, + ...sharedStyles.textRegular, + ...sharedStyles.textColorDescription, + fontSize: 14 + }, + infoContainer: { + flex: 1, + marginLeft: 10 + }, + itemContainer: { + flex: 1, + flexDirection: 'row', + padding: 10, + backgroundColor: COLOR_WHITE + }, + container: { + flex: 1, + backgroundColor: COLOR_BACKGROUND_CONTAINER + }, + list: { + ...sharedStyles.separatorVertical, + marginVertical: 10 + } +}); diff --git a/app/views/RoomView/index.js b/app/views/RoomView/index.js index 4bcecf4099ccaa864a2543b01891d4f27ce31b86..3ed9a8e339440bb840ca1f892187c9a969c02962 100644 --- a/app/views/RoomView/index.js +++ b/app/views/RoomView/index.js @@ -60,7 +60,8 @@ import { Toast } from '../../utils/info'; Message_GroupingPeriod: state.settings.Message_GroupingPeriod, Message_TimeFormat: state.settings.Message_TimeFormat, useMarkdown: state.markdown.useMarkdown, - baseUrl: state.settings.baseUrl || state.server ? state.server.server : '' + baseUrl: state.settings.baseUrl || state.server ? state.server.server : '', + Message_Read_Receipt_Enabled: state.settings.Message_Read_Receipt_Enabled }), dispatch => ({ editCancel: () => dispatch(editCancelAction()), replyCancel: () => dispatch(replyCancelAction()), @@ -116,6 +117,7 @@ export default class RoomView extends React.Component { isAuthenticated: PropTypes.bool, Message_GroupingPeriod: PropTypes.number, Message_TimeFormat: PropTypes.string, + Message_Read_Receipt_Enabled: PropTypes.bool, editing: PropTypes.bool, replying: PropTypes.bool, baseUrl: PropTypes.string, @@ -499,7 +501,7 @@ export default class RoomView extends React.Component { renderItem = (item, previousItem) => { const { room, lastOpen } = this.state; const { - user, Message_GroupingPeriod, Message_TimeFormat, useRealName, baseUrl, useMarkdown + user, Message_GroupingPeriod, Message_TimeFormat, useRealName, baseUrl, useMarkdown, Message_Read_Receipt_Enabled } = this.props; let dateSeparator = null; let showUnreadSeparator = false; @@ -541,6 +543,7 @@ export default class RoomView extends React.Component { timeFormat={Message_TimeFormat} useRealName={useRealName} useMarkdown={useMarkdown} + isReadReceiptEnabled={Message_Read_Receipt_Enabled} /> ); diff --git a/storybook/stories/Message.js b/storybook/stories/Message.js index d79cbe33659638d1cb8b9f4d9471bb9fb18fea58..87c8213846ac1f1e276ca740aa27d17fedda0c68 100644 --- a/storybook/stories/Message.js +++ b/storybook/stories/Message.js @@ -311,6 +311,30 @@ export default ( }]} /> + <Separator title='Message with read receipt' /> + <Message + msg="I'm fine!" + isReadReceiptEnabled + unread + /> + <Message + msg="I'm fine!" + isReadReceiptEnabled + unread + isHeader={false} + /> + <Message + msg="I'm fine!" + isReadReceiptEnabled + read + /> + <Message + msg="I'm fine!" + isReadReceiptEnabled + read + isHeader={false} + /> + <Separator title='Message with thread' /> <Message msg='How are you?'