diff --git a/imports/message-read-receipt/server/dbIndexes.js b/imports/message-read-receipt/server/dbIndexes.js
new file mode 100644
index 00000000000..b88d4021a7b
--- /dev/null
+++ b/imports/message-read-receipt/server/dbIndexes.js
@@ -0,0 +1,5 @@
+RocketChat.models.Messages.tryEnsureIndex({
+ unread: 1
+}, {
+ sparse: true
+});
diff --git a/imports/message-read-receipt/server/index.js b/imports/message-read-receipt/server/index.js
new file mode 100644
index 00000000000..b0a36ad00ee
--- /dev/null
+++ b/imports/message-read-receipt/server/index.js
@@ -0,0 +1,2 @@
+import './dbIndexes';
+import './settings';
diff --git a/imports/message-read-receipt/server/lib/ReadReceipt.js b/imports/message-read-receipt/server/lib/ReadReceipt.js
new file mode 100644
index 00000000000..fe033a8e872
--- /dev/null
+++ b/imports/message-read-receipt/server/lib/ReadReceipt.js
@@ -0,0 +1,55 @@
+import { Random } from 'meteor/random';
+import ModelReadReceipts from '../models/ReadReceipts';
+
+const rawReadReceipts = ModelReadReceipts.model.rawCollection();
+
+// @TODO create a debounced function by roomId, so multiple calls to same roomId runs only once
+
+export const ReadReceipt = {
+ markMessagesAsRead(roomId, userId, userLastSeen) {
+ if (!RocketChat.settings.get('Message_Read_Receipt_Enabled')) {
+ return;
+ }
+
+ const room = RocketChat.models.Rooms.findOneById(roomId, { fields: { lm: 1 } });
+
+ // if users last seen is greater than room's last message, it means the user already have this room marked as read
+ if (userLastSeen > room.lm) {
+ return;
+ }
+
+ const firstSubscription = RocketChat.models.Subscriptions.getMinimumLastSeenByRoomId(roomId);
+ // console.log('userLastSeen ->', userLastSeen);
+ // console.log('firstSubscription ->', firstSubscription);
+ // console.log('room ->', room);
+
+ // last time room was read is already past room's last message, so does nothing everybody have this room already
+ // if (firstSubscription.ls > room.lm) {
+ // console.log('already read by everyone');
+
+ // return;
+ // }
+
+ // @TODO maybe store firstSubscription in room object so we don't need to call the above update method
+ // if firstSubscription on room didn't change
+
+ if (RocketChat.settings.get('Message_Read_Receipt_Store_Users')) {
+ const receipts = RocketChat.models.Messages.findUnreadMessagesByRoomAndDate(roomId, userLastSeen).map(message => {
+ return {
+ _id: Random.id(),
+ roomId,
+ userId,
+ messageId: message._id
+ };
+ });
+
+ try {
+ rawReadReceipts.insertMany(receipts);
+ } catch (e) {
+ console.error('Error inserting read receipts per user');
+ }
+ }
+
+ RocketChat.models.Messages.setAsRead(roomId, firstSubscription.ls);
+ }
+};
diff --git a/imports/message-read-receipt/server/models/ReadReceipts.js b/imports/message-read-receipt/server/models/ReadReceipts.js
new file mode 100644
index 00000000000..b5031aa3a8d
--- /dev/null
+++ b/imports/message-read-receipt/server/models/ReadReceipts.js
@@ -0,0 +1,15 @@
+class ModelReadReceipts extends RocketChat.models._Base {
+ constructor() {
+ super(...arguments);
+
+ this.tryEnsureIndex({
+ roomId: 1,
+ userId: 1,
+ messageId: 1
+ }, {
+ unique: 1
+ });
+ }
+}
+
+export default new ModelReadReceipts('message_read_receipt', true);
diff --git a/imports/message-read-receipt/server/settings.js b/imports/message-read-receipt/server/settings.js
new file mode 100644
index 00000000000..94f49ecfd73
--- /dev/null
+++ b/imports/message-read-receipt/server/settings.js
@@ -0,0 +1,12 @@
+RocketChat.settings.add('Message_Read_Receipt_Enabled', false, {
+ group: 'Message',
+ type: 'boolean',
+ public: true
+});
+
+RocketChat.settings.add('Message_Read_Receipt_Store_Users', false, {
+ group: 'Message',
+ type: 'boolean',
+ public: false,
+ enableQuery: { _id: 'Message_Read_Receipt_Enabled', value: true }
+});
diff --git a/imports/startup/server/index.js b/imports/startup/server/index.js
new file mode 100644
index 00000000000..261b86e752d
--- /dev/null
+++ b/imports/startup/server/index.js
@@ -0,0 +1 @@
+import '../../message-read-receipt/server';
diff --git a/packages/rocketchat-lib/server/functions/sendMessage.js b/packages/rocketchat-lib/server/functions/sendMessage.js
index 46b44edd90c..af100e1f547 100644
--- a/packages/rocketchat-lib/server/functions/sendMessage.js
+++ b/packages/rocketchat-lib/server/functions/sendMessage.js
@@ -31,6 +31,10 @@ RocketChat.sendMessage = function(user, message, room, upsert = false) {
});
}
}
+
+ // @TODO test if setting is enabled?
+ message.unread = true;
+
message = RocketChat.callbacks.run('beforeSaveMessage', message);
if (message) {
// Avoid saving sandstormSessionId to the database
diff --git a/packages/rocketchat-lib/server/models/Messages.js b/packages/rocketchat-lib/server/models/Messages.js
index 90f8bf06876..f74a0cefa1e 100644
--- a/packages/rocketchat-lib/server/models/Messages.js
+++ b/packages/rocketchat-lib/server/models/Messages.js
@@ -622,4 +622,30 @@ RocketChat.models.Messages = new class extends RocketChat.models._Base {
getMessageByFileId(fileID) {
return this.findOne({ 'file._id': fileID });
}
+
+ setAsRead(rid, until) {
+ return this.update({
+ rid,
+ unread: true,
+ ts: { $lt: until }
+ }, {
+ $unset: {
+ unread: 1
+ }
+ }, {
+ multi: true
+ });
+ }
+
+ findUnreadMessagesByRoomAndDate(rid, after) {
+ return this.find({
+ unread: true,
+ rid,
+ ts: { $gt: after }
+ }, {
+ fields: {
+ _id: 1
+ }
+ });
+ }
};
diff --git a/packages/rocketchat-lib/server/models/Subscriptions.js b/packages/rocketchat-lib/server/models/Subscriptions.js
index 4c59e26add7..a2c92b0aae7 100644
--- a/packages/rocketchat-lib/server/models/Subscriptions.js
+++ b/packages/rocketchat-lib/server/models/Subscriptions.js
@@ -182,6 +182,19 @@ class ModelSubscriptions extends RocketChat.models._Base {
return this.find(query, { fields: { unread: 1 } });
}
+ getMinimumLastSeenByRoomId(rid) {
+ return this.db.findOne({
+ rid
+ }, {
+ sort: {
+ ls: 1
+ },
+ fields: {
+ ls: 1
+ }
+ });
+ }
+
// UPDATE
archiveByRoomId(roomId) {
const query =
diff --git a/packages/rocketchat-theme/client/imports/components/messages.css b/packages/rocketchat-theme/client/imports/components/messages.css
index 16e1b298698..56228e89aa2 100644
--- a/packages/rocketchat-theme/client/imports/components/messages.css
+++ b/packages/rocketchat-theme/client/imports/components/messages.css
@@ -77,6 +77,14 @@
left: 0;
}
}
+
+ &.temp .read-receipt {
+ color: #999;
+ }
+
+ & .read-receipt.read {
+ color: blue;
+ }
}
.messages-box .rc-popover__list {
diff --git a/packages/rocketchat-theme/client/imports/general/base.css b/packages/rocketchat-theme/client/imports/general/base.css
index 1a62424e1c1..bb35fc58eac 100644
--- a/packages/rocketchat-theme/client/imports/general/base.css
+++ b/packages/rocketchat-theme/client/imports/general/base.css
@@ -123,6 +123,7 @@ button {
vertical-align: -0.15em;
+ fill: currentColor;
}
.ps-scrollbar-y-rail {
diff --git a/packages/rocketchat-ui-master/public/icons.svg b/packages/rocketchat-ui-master/public/icons.svg
index 3bf31b7d5ca..2105c2fb1df 100644
--- a/packages/rocketchat-ui-master/public/icons.svg
+++ b/packages/rocketchat-ui-master/public/icons.svg
@@ -80,6 +80,7 @@