diff --git a/__tests__/__snapshots__/Storyshots.test.js.snap b/__tests__/__snapshots__/Storyshots.test.js.snap
index 3777a6ea..335bfb8b 100644
--- a/__tests__/__snapshots__/Storyshots.test.js.snap
+++ b/__tests__/__snapshots__/Storyshots.test.js.snap
@@ -9216,6 +9216,612 @@ exports[`Storyshots Message list 1`] = `
+
+ Message with read receipt
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diego.mello
+
+
+
+ 10:00 AM
+
+
+
+
+
+
+ I’m fine!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ I’m fine!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diego.mello
+
+
+
+ 10:00 AM
+
+
+
+
+
+
+ I’m fine!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ I’m fine!
+
+
+
+
+
+
+
+
+
+
+
+
({
@@ -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 1ba56ee4..d10a5262 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) => {
>
+
);
@@ -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 00000000..c407e021
--- /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 ;
+ }
+ 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 1f76d5ae..478055ad 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 4066b779..c08467ef 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 a426f0dd..b11c47cc 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 ef13d05c..b22686c1 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 7a08db17..65b001b2 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 ee082488..39fa9dae 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 fd14cda0..d750d7ab 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 737e58b8..4bf66959 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 00000000..9c90e8b5
--- /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 = () => (
+
+ {I18n.t('No_Read_Receipts')}
+
+ )
+
+ renderItem = ({ item }) => {
+ const {
+ Message_TimeFormat, userId, baseUrl, token
+ } = this.props;
+ const time = moment(item.ts).format(Message_TimeFormat);
+ return (
+
+
+
+
+
+ {item.user.name}
+
+
+ {time}
+
+
+
+ {`@${ item.user.username }`}
+
+
+
+ );
+ }
+
+ renderSeparator = () => ;
+
+ render() {
+ const { receipts, loading } = this.state;
+
+ if (!loading && receipts.length === 0) {
+ return this.renderEmpty();
+ }
+
+ return (
+
+
+
+ {loading
+ ?
+ : (
+ item._id}
+ />
+ )}
+
+
+ );
+ }
+}
diff --git a/app/views/ReadReceiptView/styles.js b/app/views/ReadReceiptView/styles.js
new file mode 100644
index 00000000..731fe8f1
--- /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 4bcecf40..3ed9a8e3 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 d79cbe33..87c82138 100644
--- a/storybook/stories/Message.js
+++ b/storybook/stories/Message.js
@@ -311,6 +311,30 @@ export default (
}]}
/>
+
+
+
+
+
+