From 09b52a47e7fb06701b46e08fcd3b67f36a67ed28 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Thu, 22 Sep 2022 10:26:30 -0300 Subject: [PATCH 01/29] first commit --- apps/meteor/app/threads/server/functions.js | 3 ++ apps/meteor/server/sdk/index.ts | 2 + apps/meteor/server/sdk/types/IReadsService.ts | 5 ++ apps/meteor/server/services/reads/service.ts | 46 +++++++++++++++++++ apps/meteor/server/services/startup.ts | 2 + 5 files changed, 58 insertions(+) create mode 100644 apps/meteor/server/sdk/types/IReadsService.ts create mode 100644 apps/meteor/server/services/reads/service.ts diff --git a/apps/meteor/app/threads/server/functions.js b/apps/meteor/app/threads/server/functions.js index 29a6eb199ad5..82ce9feee610 100644 --- a/apps/meteor/app/threads/server/functions.js +++ b/apps/meteor/app/threads/server/functions.js @@ -1,5 +1,6 @@ import { Messages, Subscriptions } from '../../models/server'; import { getMentions } from '../../lib/server/lib/notifyUsersOnMessage'; +import { Reads } from '../../../server/sdk'; export const reply = ({ tmid }, message, parentMessage, followers) => { const { rid, ts, u, editedAt } = message; @@ -75,6 +76,8 @@ export const readThread = ({ userId, rid, tmid }) => { const clearAlert = sub.tunread?.length <= 1 && sub.tunread.includes(tmid); Subscriptions.removeUnreadThreadByRoomIdAndUserId(rid, userId, tmid, clearAlert); + + Reads.readThread(userId, tmid); }; export const readAllThreads = (rid, userId) => Subscriptions.removeAllUnreadThreadsByRoomIdAndUserId(rid, userId); diff --git a/apps/meteor/server/sdk/index.ts b/apps/meteor/server/sdk/index.ts index 42724b8bc3ef..e7f75d74d851 100644 --- a/apps/meteor/server/sdk/index.ts +++ b/apps/meteor/server/sdk/index.ts @@ -20,6 +20,7 @@ import type { IVideoConfService } from './types/IVideoConfService'; import type { ISAUMonitorService } from './types/ISAUMonitorService'; import type { IDeviceManagementService } from './types/IDeviceManagementService'; import { FibersContextStore } from './lib/ContextStore'; +import type { IReadsService } from './types/IReadsService'; // TODO think in a way to not have to pass the service name to proxify here as well export const Authorization = proxifyWithWait('authorization'); @@ -40,6 +41,7 @@ export const LDAP = proxifyWithWait('ldap'); export const SAUMonitor = proxifyWithWait('sau-monitor'); export const DeviceManagement = proxifyWithWait('device-management'); export const VideoConf = proxifyWithWait('video-conference'); +export const Reads = proxifyWithWait('reads'); // Calls without wait. Means that the service is optional and the result may be an error // of service/method not available diff --git a/apps/meteor/server/sdk/types/IReadsService.ts b/apps/meteor/server/sdk/types/IReadsService.ts new file mode 100644 index 000000000000..b04762487b8d --- /dev/null +++ b/apps/meteor/server/sdk/types/IReadsService.ts @@ -0,0 +1,5 @@ +import type { IServiceClass } from './ServiceClass'; + +export interface IReadsService extends IServiceClass { + readThread(userId: string, threadId: string): Promise; +} diff --git a/apps/meteor/server/services/reads/service.ts b/apps/meteor/server/services/reads/service.ts new file mode 100644 index 000000000000..29040ef5efc9 --- /dev/null +++ b/apps/meteor/server/services/reads/service.ts @@ -0,0 +1,46 @@ +import { ServiceClassInternal } from '../../sdk/types/ServiceClass'; +import type { IReadsService } from '../../sdk/types/IReadsService'; +// import { ReadReceipt } from '/imports/message-read-receipt/server/lib/ReadReceipt'; + +type IRead = { + _id: string; + // rid: string; + tmid: string; + ls: Date; + userId: string; + // following: boolean; +}; + +export class ReadsService extends ServiceClassInternal implements IReadsService { + protected name = 'reads'; + + readThread(userId: string, tmid: string): Promise { + console.log('received readThread', userId, tmid); + + // Reads.updateOne({ + // userId, + // tmid, + // }, { + // $set: { + // ls: new Date(), + // }, + // }, { + // upsert: true, + // }); + + // const firstSubscription = Subscriptions.getMinimumLastSeenByRoomId(_id); + // if (!firstSubscription || !firstSubscription.ls) { + // return; + // } + + // Messages.setAsRead(_id, firstSubscription.ls); + + // if (lm <= firstSubscription.ls) { + // Rooms.setLastMessageAsRead(_id); + // } + + // ReadReceipt.storeReadReceipts(userId, [tmid]); + + return Promise.resolve(!!tmid); + } +} diff --git a/apps/meteor/server/services/startup.ts b/apps/meteor/server/services/startup.ts index 6f389cf593d5..00e258485f26 100644 --- a/apps/meteor/server/services/startup.ts +++ b/apps/meteor/server/services/startup.ts @@ -20,6 +20,7 @@ import { VideoConfService } from './video-conference/service'; import { isRunningMs } from '../lib/isRunningMs'; import { PushService } from './push/service'; import { DeviceManagementService } from './device-management/service'; +import { ReadsService } from './reads/service'; const { db } = MongoInternals.defaultRemoteCollectionDriver().mongo; @@ -41,6 +42,7 @@ api.registerService(new UiKitCoreApp()); api.registerService(new PushService()); api.registerService(new DeviceManagementService()); api.registerService(new VideoConfService()); +api.registerService(new ReadsService()); // if the process is running in micro services mode we don't need to register services that will run separately if (!isRunningMs()) { From 8dda41f2ce3041c1e1f2a9df9aa5593b4e828f23 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Fri, 14 Oct 2022 16:07:12 -0300 Subject: [PATCH 02/29] Add Reads model --- apps/meteor/server/models/Reads.ts | 6 ++++ apps/meteor/server/models/raw/Reads.ts | 32 +++++++++++++++++++ packages/core-typings/src/Reads.ts | 8 +++++ packages/core-typings/src/index.ts | 1 + packages/model-typings/src/index.ts | 1 + .../model-typings/src/models/IReadsModel.ts | 9 ++++++ packages/models/src/index.ts | 2 ++ 7 files changed, 59 insertions(+) create mode 100644 apps/meteor/server/models/Reads.ts create mode 100644 apps/meteor/server/models/raw/Reads.ts create mode 100644 packages/core-typings/src/Reads.ts create mode 100644 packages/model-typings/src/models/IReadsModel.ts diff --git a/apps/meteor/server/models/Reads.ts b/apps/meteor/server/models/Reads.ts new file mode 100644 index 000000000000..ded578974e02 --- /dev/null +++ b/apps/meteor/server/models/Reads.ts @@ -0,0 +1,6 @@ +import { registerModel } from '@rocket.chat/models'; + +import { db } from '../database/utils'; +import { ReadsRaw } from './raw/Reads'; + +registerModel('IReadsModel', new ReadsRaw(db)); diff --git a/apps/meteor/server/models/raw/Reads.ts b/apps/meteor/server/models/raw/Reads.ts new file mode 100644 index 000000000000..04b7b544e9ba --- /dev/null +++ b/apps/meteor/server/models/raw/Reads.ts @@ -0,0 +1,32 @@ +import type { Reads, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; +import type { IReadsModel } from '@rocket.chat/model-typings'; +import type { Collection, FindCursor, Db, IndexDescription } from 'mongodb'; + +import { BaseRaw } from './BaseRaw'; + +export class ReadsRaw extends BaseRaw implements IReadsModel { + constructor(db: Db, trash?: Collection>) { + super(db, 'reads', trash); + } + + protected modelIndexes(): IndexDescription[] { + return [{ key: { tmid: 1, userId: 1 }, unique: true }, { key: { tmid: 1 } }]; + } + + findByThreadId(tmid: string): FindCursor { + return this.find({ tmid }); + } + + getMinimumLastSeenByThreadId(tmid: string): Promise { + return this.findOne( + { + tmid, + }, + { + sort: { + ls: 1, + }, + }, + ); + } +} diff --git a/packages/core-typings/src/Reads.ts b/packages/core-typings/src/Reads.ts new file mode 100644 index 000000000000..d8492106e226 --- /dev/null +++ b/packages/core-typings/src/Reads.ts @@ -0,0 +1,8 @@ +import type { IMessage } from './IMessage/IMessage'; +import type { IUser } from './IUser'; + +export type Reads = { + tmid: IMessage['_id']; + ls: Date; + userId: IUser['_id']; +}; diff --git a/packages/core-typings/src/index.ts b/packages/core-typings/src/index.ts index cf1d2babedd6..1f4050406ed9 100644 --- a/packages/core-typings/src/index.ts +++ b/packages/core-typings/src/index.ts @@ -57,6 +57,7 @@ export * from './ICustomUserStatus'; export * from './IEmailMessageHistory'; export * from './ReadReceipt'; +export * from './Reads'; export * from './IUpload'; export * from './IOEmbedCache'; export * from './IOembed'; diff --git a/packages/model-typings/src/index.ts b/packages/model-typings/src/index.ts index 222df26698c3..6f19c60de874 100644 --- a/packages/model-typings/src/index.ts +++ b/packages/model-typings/src/index.ts @@ -42,6 +42,7 @@ export * from './models/IPbxEventsModel'; export * from './models/IPushTokenModel'; export * from './models/IPermissionsModel'; export * from './models/IReadReceiptsModel'; +export * from './models/IReadsModel'; export * from './models/IReportsModel'; export * from './models/IRolesModel'; export * from './models/IRoomsModel'; diff --git a/packages/model-typings/src/models/IReadsModel.ts b/packages/model-typings/src/models/IReadsModel.ts new file mode 100644 index 000000000000..faf56db481d3 --- /dev/null +++ b/packages/model-typings/src/models/IReadsModel.ts @@ -0,0 +1,9 @@ +import type { FindCursor } from 'mongodb'; +import type { Reads } from '@rocket.chat/core-typings'; + +import type { IBaseModel } from './IBaseModel'; + +export interface IReadsModel extends IBaseModel { + findByThreadId(tmid: string): FindCursor; + getMinimumLastSeenByThreadId(tmid: string): Promise; +} diff --git a/packages/models/src/index.ts b/packages/models/src/index.ts index 64d16afba0db..85699e7403eb 100644 --- a/packages/models/src/index.ts +++ b/packages/models/src/index.ts @@ -42,6 +42,7 @@ import type { IPushTokenModel, IPermissionsModel, IReadReceiptsModel, + IReadsModel, IReportsModel, IRolesModel, IRoomsModel, @@ -116,6 +117,7 @@ export const PbxEvents = proxify('IPbxEventsModel'); export const PushToken = proxify('IPushTokenModel'); export const Permissions = proxify('IPermissionsModel'); export const ReadReceipts = proxify('IReadReceiptsModel'); +export const Reads = proxify('IReadsModel'); export const Reports = proxify('IReportsModel'); export const Roles = proxify('IRolesModel'); export const Rooms = proxify('IRoomsModel'); From 4df0834037c7bbe9826c7db8c18bb375583b2e51 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Tue, 20 Dec 2022 14:04:56 -0300 Subject: [PATCH 03/29] Add readThread method --- .../app/models/server/models/Messages.js | 56 +++++++++++++++- apps/meteor/app/threads/server/functions.js | 12 +++- .../server/lib/ReadReceipt.js | 19 +++++- apps/meteor/server/models/raw/Reads.ts | 6 +- apps/meteor/server/models/startup.ts | 1 + apps/meteor/server/sdk/types/IReadsService.ts | 2 +- apps/meteor/server/services/reads/service.ts | 65 ++++++++----------- 7 files changed, 113 insertions(+), 48 deletions(-) diff --git a/apps/meteor/app/models/server/models/Messages.js b/apps/meteor/app/models/server/models/Messages.js index e570c67ed779..57e44b4a5611 100644 --- a/apps/meteor/app/models/server/models/Messages.js +++ b/apps/meteor/app/models/server/models/Messages.js @@ -1084,12 +1084,38 @@ export class Messages extends Base { return this.findOne(query, options); } - setAsRead(rid, until) { + setVisibleMessagesAsRead(rid, until) { return this.update( { rid, unread: true, ts: { $lt: until }, + $or: [ + { + tmid: { $exists: false }, + }, + { + tshow: true, + }, + ], + }, + { + $unset: { + unread: 1, + }, + }, + { + multi: true, + }, + ); + } + + setThreadMessagesAsRead(tmid, until) { + return this.update( + { + tmid, + unread: true, + ts: { $lt: until }, }, { $unset: { @@ -1115,10 +1141,36 @@ export class Messages extends Base { ); } - findUnreadMessagesByRoomAndDate(rid, after) { + findVisibleUnreadMessagesByRoomAndDate(rid, after) { const query = { unread: true, rid, + $or: [ + { + tmid: { $exists: false }, + }, + { + tshow: true, + }, + ], + }; + + if (after) { + query.ts = { $gt: after }; + } + + return this.find(query, { + fields: { + _id: 1, + }, + }); + } + + findUnreadThreadMessagesByDate(tmid, after) { + const query = { + unread: true, + tmid, + tshow: { $exists: false }, }; if (after) { diff --git a/apps/meteor/app/threads/server/functions.js b/apps/meteor/app/threads/server/functions.js index 82ce9feee610..3201404e70e5 100644 --- a/apps/meteor/app/threads/server/functions.js +++ b/apps/meteor/app/threads/server/functions.js @@ -1,6 +1,7 @@ import { Messages, Subscriptions } from '../../models/server'; import { getMentions } from '../../lib/server/lib/notifyUsersOnMessage'; import { Reads } from '../../../server/sdk'; +import { settings } from '../../settings/server'; export const reply = ({ tmid }, message, parentMessage, followers) => { const { rid, ts, u, editedAt } = message; @@ -21,8 +22,14 @@ export const reply = ({ tmid }, message, parentMessage, followers) => { Messages.updateRepliesByThreadId(tmid, addToReplies, ts); const replies = Messages.getThreadFollowsByThreadId(tmid); + console.log('replies', replies); + + if (replies && replies.length === 1) { + console.log('Readng thread') + } const repliesFiltered = replies.filter((userId) => userId !== u._id).filter((userId) => !mentionIds.includes(userId)); + console.log('repliesFiltered', repliesFiltered); if (toAll || toHere) { Subscriptions.addUnreadThreadByRoomIdAndUserIds(rid, repliesFiltered, tmid, { @@ -76,8 +83,9 @@ export const readThread = ({ userId, rid, tmid }) => { const clearAlert = sub.tunread?.length <= 1 && sub.tunread.includes(tmid); Subscriptions.removeUnreadThreadByRoomIdAndUserId(rid, userId, tmid, clearAlert); - - Reads.readThread(userId, tmid); + if (settings.get('Message_Read_Receipt_Enabled')) { + Reads.readThread(userId, tmid); + } }; export const readAllThreads = (rid, userId) => Subscriptions.removeAllUnreadThreadsByRoomIdAndUserId(rid, userId); diff --git a/apps/meteor/imports/message-read-receipt/server/lib/ReadReceipt.js b/apps/meteor/imports/message-read-receipt/server/lib/ReadReceipt.js index 158d3ddc3365..34b41ef7f21a 100644 --- a/apps/meteor/imports/message-read-receipt/server/lib/ReadReceipt.js +++ b/apps/meteor/imports/message-read-receipt/server/lib/ReadReceipt.js @@ -26,7 +26,7 @@ const updateMessages = debounceByRoomId( return; } - Messages.setAsRead(_id, firstSubscription.ls); + Messages.setVisibleMessagesAsRead(_id, firstSubscription.ls); if (lm <= firstSubscription.ls) { Rooms.setLastMessageAsRead(_id); @@ -47,7 +47,7 @@ export const ReadReceipt = { return; } - this.storeReadReceipts(Messages.findUnreadMessagesByRoomAndDate(roomId, userLastSeen), roomId, userId); + this.storeReadReceipts(Messages.findVisibleUnreadMessagesByRoomAndDate(roomId, userLastSeen), roomId, userId); updateMessages(room); }, @@ -71,6 +71,21 @@ export const ReadReceipt = { this.storeReadReceipts([{ _id: message._id }], roomId, userId, extraData); }, + storeThreadMessagesReadReceipts(tmid, userId, userLastSeen) { + if (!settings.get('Message_Read_Receipt_Enabled')) { + return; + } + + const message = Messages.findOneById(tmid, { fields: { tlm: 1, rid: 1 } }); + + // if users last seen is greater than thread's last message, it means the user already have this thread marked as read + if (!message || userLastSeen > message.tlm) { + return; + } + + this.storeReadReceipts(Messages.findUnreadThreadMessagesByDate(tmid, userLastSeen), message.rid, userId); + }, + async storeReadReceipts(messages, roomId, userId, extraData = {}) { if (settings.get('Message_Read_Receipt_Store_Users')) { const ts = new Date(); diff --git a/apps/meteor/server/models/raw/Reads.ts b/apps/meteor/server/models/raw/Reads.ts index 04b7b544e9ba..257c21286a98 100644 --- a/apps/meteor/server/models/raw/Reads.ts +++ b/apps/meteor/server/models/raw/Reads.ts @@ -1,6 +1,6 @@ import type { Reads, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; import type { IReadsModel } from '@rocket.chat/model-typings'; -import type { Collection, FindCursor, Db, IndexDescription } from 'mongodb'; +import type { Collection, Db, IndexDescription } from 'mongodb'; import { BaseRaw } from './BaseRaw'; @@ -13,8 +13,8 @@ export class ReadsRaw extends BaseRaw implements IReadsModel { return [{ key: { tmid: 1, userId: 1 }, unique: true }, { key: { tmid: 1 } }]; } - findByThreadId(tmid: string): FindCursor { - return this.find({ tmid }); + findOneByUserIdAndThreadId(userId: string, tmid: string): Promise { + return this.findOne({ userId, tmid }); } getMinimumLastSeenByThreadId(tmid: string): Promise { diff --git a/apps/meteor/server/models/startup.ts b/apps/meteor/server/models/startup.ts index 32af7c43cd03..10995488fb57 100644 --- a/apps/meteor/server/models/startup.ts +++ b/apps/meteor/server/models/startup.ts @@ -38,6 +38,7 @@ import './PbxEvents'; import './PushToken'; import './Permissions'; import './ReadReceipts'; +import './Reads'; import './Reports'; import './Roles'; import './Rooms'; diff --git a/apps/meteor/server/sdk/types/IReadsService.ts b/apps/meteor/server/sdk/types/IReadsService.ts index b04762487b8d..58755d443548 100644 --- a/apps/meteor/server/sdk/types/IReadsService.ts +++ b/apps/meteor/server/sdk/types/IReadsService.ts @@ -1,5 +1,5 @@ import type { IServiceClass } from './ServiceClass'; export interface IReadsService extends IServiceClass { - readThread(userId: string, threadId: string): Promise; + readThread(userId: string, threadId: string): Promise; } diff --git a/apps/meteor/server/services/reads/service.ts b/apps/meteor/server/services/reads/service.ts index 29040ef5efc9..ca169424b948 100644 --- a/apps/meteor/server/services/reads/service.ts +++ b/apps/meteor/server/services/reads/service.ts @@ -1,46 +1,35 @@ import { ServiceClassInternal } from '../../sdk/types/ServiceClass'; import type { IReadsService } from '../../sdk/types/IReadsService'; -// import { ReadReceipt } from '/imports/message-read-receipt/server/lib/ReadReceipt'; - -type IRead = { - _id: string; - // rid: string; - tmid: string; - ls: Date; - userId: string; - // following: boolean; -}; +import { Reads } from '@rocket.chat/models'; +import { Messages } from '../../../app/models/server'; +import { ReadReceipt } from '../../../imports/message-read-receipt/server/lib/ReadReceipt'; export class ReadsService extends ServiceClassInternal implements IReadsService { protected name = 'reads'; - readThread(userId: string, tmid: string): Promise { - console.log('received readThread', userId, tmid); - - // Reads.updateOne({ - // userId, - // tmid, - // }, { - // $set: { - // ls: new Date(), - // }, - // }, { - // upsert: true, - // }); - - // const firstSubscription = Subscriptions.getMinimumLastSeenByRoomId(_id); - // if (!firstSubscription || !firstSubscription.ls) { - // return; - // } - - // Messages.setAsRead(_id, firstSubscription.ls); - - // if (lm <= firstSubscription.ls) { - // Rooms.setLastMessageAsRead(_id); - // } - - // ReadReceipt.storeReadReceipts(userId, [tmid]); - - return Promise.resolve(!!tmid); + async readThread(userId: string, tmid: string): Promise { + const read = await Reads.findOneByUserIdAndThreadId(userId, tmid); + Reads.updateOne({ + userId, + tmid, + }, { + $set: { + ls: new Date(), + }, + }, { + upsert: true, + }); + + const threadMessage = Messages.findOneById(tmid); + if (!threadMessage || !threadMessage.tlm) { + return; + } + + ReadReceipt.storeThreadMessagesReadReceipts(tmid, userId, read?.ls || threadMessage.ts); + + const firstRead = await Reads.getMinimumLastSeenByThreadId(tmid); + if (firstRead && firstRead.ls) { + Messages.setThreadMessagesAsRead(tmid, firstRead.ls); + } } } From a8bf7034957efaa91522e8389385f01a977ca561 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Tue, 20 Dec 2022 15:58:52 -0300 Subject: [PATCH 04/29] fix first thread message as read --- .../server/methods/getThreadMessages.js | 4 ++ apps/meteor/server/services/reads/service.ts | 46 +++++++++++++------ packages/core-typings/src/Reads.ts | 3 +- 3 files changed, 39 insertions(+), 14 deletions(-) diff --git a/apps/meteor/app/threads/server/methods/getThreadMessages.js b/apps/meteor/app/threads/server/methods/getThreadMessages.js index 54d5f32b7691..233fdae18209 100644 --- a/apps/meteor/app/threads/server/methods/getThreadMessages.js +++ b/apps/meteor/app/threads/server/methods/getThreadMessages.js @@ -33,6 +33,10 @@ Meteor.methods({ throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'getThreadMessages' }); } + if (!thread.tcount) { + return []; + } + readThread({ userId: user._id, rid: thread.rid, tmid }); const result = Messages.findVisibleThreadByThreadId(tmid, { diff --git a/apps/meteor/server/services/reads/service.ts b/apps/meteor/server/services/reads/service.ts index ca169424b948..f01c0ca9e2f2 100644 --- a/apps/meteor/server/services/reads/service.ts +++ b/apps/meteor/server/services/reads/service.ts @@ -1,6 +1,7 @@ +import { Reads } from '@rocket.chat/models'; + import { ServiceClassInternal } from '../../sdk/types/ServiceClass'; import type { IReadsService } from '../../sdk/types/IReadsService'; -import { Reads } from '@rocket.chat/models'; import { Messages } from '../../../app/models/server'; import { ReadReceipt } from '../../../imports/message-read-receipt/server/lib/ReadReceipt'; @@ -9,25 +10,44 @@ export class ReadsService extends ServiceClassInternal implements IReadsService async readThread(userId: string, tmid: string): Promise { const read = await Reads.findOneByUserIdAndThreadId(userId, tmid); - Reads.updateOne({ - userId, - tmid, - }, { - $set: { - ls: new Date(), - }, - }, { - upsert: true, - }); - - const threadMessage = Messages.findOneById(tmid); + console.log('read', read); + + const now = new Date(); + console.log('now', now); + + await Reads.updateOne( + { + userId, + tmid, + }, + { + $set: { + ls: now, + }, + }, + { + upsert: true, + }, + ); + + const threadMessage = Messages.findOneById(tmid, { projection: { ts: 1, tlm: 1, replies: 1 } }); + console.log('threadMessage', threadMessage); if (!threadMessage || !threadMessage.tlm) { return; } ReadReceipt.storeThreadMessagesReadReceipts(tmid, userId, read?.ls || threadMessage.ts); + // doesn't mark as read if not all followers have read the thread + const totalReads = await Reads.col.countDocuments({ tmid, userId: { $in: threadMessage.replies } }); + console.log('totalReads ->', totalReads); + console.log('threadMessage.replies.length ->', threadMessage.replies.length); + if (totalReads < threadMessage.replies.length) { + return; + } + const firstRead = await Reads.getMinimumLastSeenByThreadId(tmid); + console.log('firstRead', firstRead); if (firstRead && firstRead.ls) { Messages.setThreadMessagesAsRead(tmid, firstRead.ls); } diff --git a/packages/core-typings/src/Reads.ts b/packages/core-typings/src/Reads.ts index d8492106e226..6468500eaaa4 100644 --- a/packages/core-typings/src/Reads.ts +++ b/packages/core-typings/src/Reads.ts @@ -2,7 +2,8 @@ import type { IMessage } from './IMessage/IMessage'; import type { IUser } from './IUser'; export type Reads = { - tmid: IMessage['_id']; + _id: string; + tmid: IMessage['_id']; ls: Date; userId: IUser['_id']; }; From 8f9b674405f0c9a42db01751eb5bdc9e732c927a Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Tue, 3 Jan 2023 13:01:51 -0300 Subject: [PATCH 05/29] Fix typecheck error --- packages/core-services/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core-services/src/index.ts b/packages/core-services/src/index.ts index 5d87cd4e3600..d162b90b8210 100644 --- a/packages/core-services/src/index.ts +++ b/packages/core-services/src/index.ts @@ -72,6 +72,7 @@ export { IOmnichannelVoipService, IPresence, IPushService, + IReadsService, IRoomService, ISAUMonitorService, ISubscriptionExtraData, From 62b866cac1c53e928405acfa18bd23f6a750261f Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Tue, 17 Jan 2023 09:01:31 -0300 Subject: [PATCH 06/29] Move read receipts to EE --- apps/meteor/app/api/server/v1/chat.js | 25 -------------- apps/meteor/app/threads/server/functions.js | 6 ---- .../server/methods/getThreadMessages.js | 3 ++ apps/meteor/client/startup/index.ts | 2 +- .../client/views/admin/info/LicenseCard.tsx | 2 ++ .../ReadReceiptsModal/ReadReceiptsModal.tsx | 9 +++-- apps/meteor/ee/app/license/server/bundles.ts | 4 ++- .../server/hooks}/hooks.js | 18 +++++++--- .../app/message-read-receipt/server/index.js | 9 +++++ .../server}/methods/getReadReceipts.js | 2 +- .../message-read-receipt/server/settings.ts | 22 ++++++++++++ apps/meteor/ee/client/startup/index.ts | 1 + .../{ => ee}/client/startup/readReceipt.ts | 10 +++--- apps/meteor/ee/definition/rest/index.ts | 1 + apps/meteor/ee/definition/rest/v1/chat.ts | 34 +++++++++++++++++++ apps/meteor/ee/server/api/chat.ts | 33 ++++++++++++++++++ apps/meteor/ee/server/api/index.ts | 1 + .../lib/message-read-receipt}/ReadReceipt.js | 0 .../server/local-services}/reads/service.ts | 8 ++--- .../{ => ee}/server/models/ReadReceipts.ts | 2 +- .../server/models/raw/ReadReceipts.ts | 2 +- apps/meteor/ee/server/models/startup.ts | 1 + apps/meteor/ee/server/sdk/index.ts | 3 +- .../ee/server/sdk/types/IReadsService.ts | 3 ++ apps/meteor/ee/server/sdk/types/index.ts | 2 ++ apps/meteor/ee/server/startup/services.ts | 2 ++ .../message-read-receipt/server/index.js | 4 --- .../message-read-receipt/server/settings.ts | 14 -------- apps/meteor/imports/startup/server/index.ts | 1 - apps/meteor/lib/callbacks.ts | 2 +- .../rocketchat-i18n/i18n/en.i18n.json | 1 + apps/meteor/server/models/startup.ts | 1 - apps/meteor/server/services/startup.ts | 2 -- packages/core-services/src/index.ts | 3 -- 34 files changed, 154 insertions(+), 79 deletions(-) rename apps/meteor/{imports/message-read-receipt/server => ee/app/message-read-receipt/server/hooks}/hooks.js (59%) create mode 100644 apps/meteor/ee/app/message-read-receipt/server/index.js rename apps/meteor/{imports/message-read-receipt/server/api => ee/app/message-read-receipt/server}/methods/getReadReceipts.js (91%) create mode 100644 apps/meteor/ee/app/message-read-receipt/server/settings.ts rename apps/meteor/{ => ee}/client/startup/readReceipt.ts (67%) create mode 100644 apps/meteor/ee/definition/rest/v1/chat.ts create mode 100644 apps/meteor/ee/server/api/chat.ts rename apps/meteor/{imports/message-read-receipt/server/lib => ee/server/lib/message-read-receipt}/ReadReceipt.js (100%) rename apps/meteor/{server/services => ee/server/local-services}/reads/service.ts (82%) rename apps/meteor/{ => ee}/server/models/ReadReceipts.ts (76%) rename apps/meteor/{ => ee}/server/models/raw/ReadReceipts.ts (91%) create mode 100644 apps/meteor/ee/server/sdk/types/IReadsService.ts create mode 100644 apps/meteor/ee/server/sdk/types/index.ts delete mode 100644 apps/meteor/imports/message-read-receipt/server/index.js delete mode 100644 apps/meteor/imports/message-read-receipt/server/settings.ts diff --git a/apps/meteor/app/api/server/v1/chat.js b/apps/meteor/app/api/server/v1/chat.js index 73f4b6931ddc..f47642331dbe 100644 --- a/apps/meteor/app/api/server/v1/chat.js +++ b/apps/meteor/app/api/server/v1/chat.js @@ -372,31 +372,6 @@ API.v1.addRoute( }, ); -API.v1.addRoute( - 'chat.getMessageReadReceipts', - { authRequired: true }, - { - async get() { - const { messageId } = this.queryParams; - if (!messageId) { - return API.v1.failure({ - error: "The required 'messageId' param is missing.", - }); - } - - try { - return API.v1.success({ - receipts: await Meteor.call('getReadReceipts', { messageId }), - }); - } catch (error) { - return API.v1.failure({ - error: error.message, - }); - } - }, - }, -); - API.v1.addRoute( 'chat.reportMessage', { authRequired: true }, diff --git a/apps/meteor/app/threads/server/functions.js b/apps/meteor/app/threads/server/functions.js index 55b9383575b6..b96b9e81a9ef 100644 --- a/apps/meteor/app/threads/server/functions.js +++ b/apps/meteor/app/threads/server/functions.js @@ -1,8 +1,5 @@ -import { Reads } from '@rocket.chat/core-services'; - import { Messages, Subscriptions } from '../../models/server'; import { getMentions } from '../../lib/server/lib/notifyUsersOnMessage'; -import { settings } from '../../settings/server'; export const reply = ({ tmid }, message, parentMessage, followers) => { const { rid, ts, u, editedAt } = message; @@ -78,7 +75,4 @@ export const readThread = ({ userId, rid, tmid }) => { const clearAlert = sub.tunread?.length <= 1 && sub.tunread.includes(tmid); Subscriptions.removeUnreadThreadByRoomIdAndUserId(rid, userId, tmid, clearAlert); - if (settings.get('Message_Read_Receipt_Enabled')) { - Reads.readThread(userId, tmid); - } }; diff --git a/apps/meteor/app/threads/server/methods/getThreadMessages.js b/apps/meteor/app/threads/server/methods/getThreadMessages.js index 233fdae18209..3104af01efbe 100644 --- a/apps/meteor/app/threads/server/methods/getThreadMessages.js +++ b/apps/meteor/app/threads/server/methods/getThreadMessages.js @@ -1,5 +1,6 @@ import { Meteor } from 'meteor/meteor'; +import { callbacks } from '../../../../lib/callbacks'; import { Messages, Rooms } from '../../../models/server'; import { canAccessRoom } from '../../../authorization/server'; import { settings } from '../../../settings/server'; @@ -28,6 +29,7 @@ Meteor.methods({ const user = Meteor.user(); const room = Rooms.findOneById(thread.rid); + callbacks.run('beforeReadMessages', thread.rid, user._id); if (!canAccessRoom(room, user)) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'getThreadMessages' }); @@ -44,6 +46,7 @@ Meteor.methods({ ...(limit && { limit }), sort: { ts: -1 }, }).fetch(); + callbacks.runAsync('afterReadMessages', room._id, { uid: user._id, tmid }); return [thread, ...result]; }, diff --git a/apps/meteor/client/startup/index.ts b/apps/meteor/client/startup/index.ts index 8a203ecca545..8fa3cfadf859 100644 --- a/apps/meteor/client/startup/index.ts +++ b/apps/meteor/client/startup/index.ts @@ -23,7 +23,7 @@ import './oauth'; import './openedRoom'; import './otr'; import './readMessage'; -import './readReceipt'; +import '../../ee/client/startup/readReceipt'; import './reloadRoomAfterLogin'; import './renderMessage'; import './renderNotification'; diff --git a/apps/meteor/client/views/admin/info/LicenseCard.tsx b/apps/meteor/client/views/admin/info/LicenseCard.tsx index 177db8fb5835..916fca4c2e09 100644 --- a/apps/meteor/client/views/admin/info/LicenseCard.tsx +++ b/apps/meteor/client/views/admin/info/LicenseCard.tsx @@ -27,6 +27,7 @@ const LicenseCard = (): ReactElement => { const hasOmnichannel = modules.includes('livechat-enterprise'); const hasAuditing = modules.includes('auditing'); const hasCannedResponses = modules.includes('canned-responses'); + const hasReadReceipts = modules.includes('message-read-receipt'); const handleApplyLicense = useMutableCallback(() => setModal( @@ -64,6 +65,7 @@ const LicenseCard = (): ReactElement => { + )} diff --git a/apps/meteor/client/views/room/modals/ReadReceiptsModal/ReadReceiptsModal.tsx b/apps/meteor/client/views/room/modals/ReadReceiptsModal/ReadReceiptsModal.tsx index 418f45baefa5..a2ebb7151808 100644 --- a/apps/meteor/client/views/room/modals/ReadReceiptsModal/ReadReceiptsModal.tsx +++ b/apps/meteor/client/views/room/modals/ReadReceiptsModal/ReadReceiptsModal.tsx @@ -1,6 +1,6 @@ import type { IMessage, ReadReceipt } from '@rocket.chat/core-typings'; import { Skeleton } from '@rocket.chat/fuselage'; -import { useMethod, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; +import { useEndpoint, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import type { ReactElement } from 'react'; import React, { useEffect } from 'react'; @@ -17,9 +17,12 @@ const ReadReceiptsModal = ({ messageId, onClose }: ReadReceiptsModalProps): Reac const t = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); - const getReadReceipts = useMethod('getReadReceipts'); + const getReadReceipts = useEndpoint('GET', '/v1/chat.getMessageReadReceipts'); - const readReceiptsResult = useQuery(['read-receipts', messageId], () => getReadReceipts({ messageId })); + const readReceiptsResult = useQuery( + ['read-receipts', messageId], + async () => (await getReadReceipts({ messageId })).receipts, + ); useEffect(() => { if (readReceiptsResult.isError) { diff --git a/apps/meteor/ee/app/license/server/bundles.ts b/apps/meteor/ee/app/license/server/bundles.ts index eedce00f4c86..8eb80d4b8f01 100644 --- a/apps/meteor/ee/app/license/server/bundles.ts +++ b/apps/meteor/ee/app/license/server/bundles.ts @@ -13,7 +13,8 @@ export type BundleFeature = | 'device-management' | 'oauth-enterprise' | 'federation' - | 'videoconference-enterprise'; + | 'videoconference-enterprise' + | 'message-read-receipt'; interface IBundle { [key: string]: BundleFeature[]; @@ -36,6 +37,7 @@ const bundles: IBundle = { 'device-management', 'federation', 'videoconference-enterprise', + 'message-read-receipt', ], pro: [], }; diff --git a/apps/meteor/imports/message-read-receipt/server/hooks.js b/apps/meteor/ee/app/message-read-receipt/server/hooks/hooks.js similarity index 59% rename from apps/meteor/imports/message-read-receipt/server/hooks.js rename to apps/meteor/ee/app/message-read-receipt/server/hooks/hooks.js index 4155761a1189..0847a3dc99f1 100644 --- a/apps/meteor/imports/message-read-receipt/server/hooks.js +++ b/apps/meteor/ee/app/message-read-receipt/server/hooks/hooks.js @@ -1,7 +1,9 @@ import { Subscriptions } from '@rocket.chat/models'; -import { ReadReceipt } from './lib/ReadReceipt'; -import { callbacks } from '../../../lib/callbacks'; +import { Reads } from '../../../../server/sdk'; +import { ReadReceipt } from '../../../../server/lib/message-read-receipt/ReadReceipt'; +import { callbacks } from '../../../../../lib/callbacks'; +import { settings } from '../../../../../app/settings/server'; callbacks.add( 'afterSaveMessage', @@ -27,8 +29,16 @@ callbacks.add( callbacks.add( 'afterReadMessages', - (rid, { uid, lastSeen }) => { - ReadReceipt.markMessagesAsRead(rid, uid, lastSeen); + (rid, { uid, lastSeen, tmid }) => { + if (!settings.get('Message_Read_Receipt_Enabled')) { + return; + } + + if (tmid) { + Reads.readThread(uid, tmid); + } else if (lastSeen) { + ReadReceipt.markMessagesAsRead(rid, uid, lastSeen); + } }, callbacks.priority.MEDIUM, 'message-read-receipt-afterReadMessages', diff --git a/apps/meteor/ee/app/message-read-receipt/server/index.js b/apps/meteor/ee/app/message-read-receipt/server/index.js new file mode 100644 index 000000000000..e459c2f0889c --- /dev/null +++ b/apps/meteor/ee/app/message-read-receipt/server/index.js @@ -0,0 +1,9 @@ +import { onLicense } from '../../license/server'; + +onLicense('message-read-receipt', () => { + const { createSettings } = require('./settings'); + require('./hooks/hooks'); + require('./methods/getReadReceipts'); + + createSettings(); +}); diff --git a/apps/meteor/imports/message-read-receipt/server/api/methods/getReadReceipts.js b/apps/meteor/ee/app/message-read-receipt/server/methods/getReadReceipts.js similarity index 91% rename from apps/meteor/imports/message-read-receipt/server/api/methods/getReadReceipts.js rename to apps/meteor/ee/app/message-read-receipt/server/methods/getReadReceipts.js index a037fea74910..bbb2ec6fc41a 100644 --- a/apps/meteor/imports/message-read-receipt/server/api/methods/getReadReceipts.js +++ b/apps/meteor/ee/app/message-read-receipt/server/methods/getReadReceipts.js @@ -3,7 +3,7 @@ import { check } from 'meteor/check'; import { Messages } from '../../../../../app/models/server'; import { canAccessRoomId } from '../../../../../app/authorization/server'; -import { ReadReceipt } from '../../lib/ReadReceipt'; +import { ReadReceipt } from '../../../../server/lib/message-read-receipt/ReadReceipt'; Meteor.methods({ async getReadReceipts({ messageId }) { diff --git a/apps/meteor/ee/app/message-read-receipt/server/settings.ts b/apps/meteor/ee/app/message-read-receipt/server/settings.ts new file mode 100644 index 000000000000..681ad8756136 --- /dev/null +++ b/apps/meteor/ee/app/message-read-receipt/server/settings.ts @@ -0,0 +1,22 @@ +import { settingsRegistry } from '../../../../app/settings/server'; + +export const createSettings = (): void => { + settingsRegistry.add('Message_Read_Receipt_Enabled', false, { + group: 'Message', + enterprise: true, + modules: ['message-read-receipt'], + type: 'boolean', + invalidValue: false, + public: true, + }); + + settingsRegistry.add('Message_Read_Receipt_Store_Users', false, { + group: 'Message', + enterprise: true, + modules: ['message-read-receipt'], + type: 'boolean', + invalidValue: false, + public: true, + enableQuery: { _id: 'Message_Read_Receipt_Enabled', value: true }, + }); +}; diff --git a/apps/meteor/ee/client/startup/index.ts b/apps/meteor/ee/client/startup/index.ts index 91b0202cafcc..cd34bcf595f6 100644 --- a/apps/meteor/ee/client/startup/index.ts +++ b/apps/meteor/ee/client/startup/index.ts @@ -2,3 +2,4 @@ import './engagementDashboard'; import './deviceManagement'; import './routes'; import './slashCommands'; +import './readReceipt'; diff --git a/apps/meteor/client/startup/readReceipt.ts b/apps/meteor/ee/client/startup/readReceipt.ts similarity index 67% rename from apps/meteor/client/startup/readReceipt.ts rename to apps/meteor/ee/client/startup/readReceipt.ts index 306f07082dac..702d19ed57a3 100644 --- a/apps/meteor/client/startup/readReceipt.ts +++ b/apps/meteor/ee/client/startup/readReceipt.ts @@ -1,11 +1,11 @@ import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; -import { settings } from '../../app/settings/client'; -import { MessageAction } from '../../app/ui-utils/client'; -import { imperativeModal } from '../lib/imperativeModal'; -import { messageArgs } from '../lib/utils/messageArgs'; -import ReadReceiptsModal from '../views/room/modals/ReadReceiptsModal'; +import { settings } from '../../../app/settings/client'; +import { MessageAction } from '../../../app/ui-utils/client'; +import { imperativeModal } from '../../../client/lib/imperativeModal'; +import { messageArgs } from '../../../client/lib/utils/messageArgs'; +import ReadReceiptsModal from '../../../client/views/room/modals/ReadReceiptsModal'; Meteor.startup(() => { Tracker.autorun(() => { diff --git a/apps/meteor/ee/definition/rest/index.ts b/apps/meteor/ee/definition/rest/index.ts index 654d8dd08b8c..f43ab2ca22fa 100644 --- a/apps/meteor/ee/definition/rest/index.ts +++ b/apps/meteor/ee/definition/rest/index.ts @@ -3,3 +3,4 @@ import './v1/engagementDashboard'; import './v1/omnichannel'; import './v1/sessions'; +import './v1/chat'; diff --git a/apps/meteor/ee/definition/rest/v1/chat.ts b/apps/meteor/ee/definition/rest/v1/chat.ts new file mode 100644 index 000000000000..f2f6ef328418 --- /dev/null +++ b/apps/meteor/ee/definition/rest/v1/chat.ts @@ -0,0 +1,34 @@ +import type { IMessage, ReadReceipt } from '@rocket.chat/core-typings'; +import Ajv from 'ajv'; + +const ajv = new Ajv({ + coerceTypes: true, +}); + +type GetMessageReadReceiptsProps = { + messageId: IMessage['_id']; +}; + +const roleUpdatePropsSchema = { + type: 'object', + properties: { + messageId: { + type: 'string', + }, + }, + required: ['messageId'], + additionalProperties: false, +}; + +export const isGetMessageReadReceiptsProps = ajv.compile(roleUpdatePropsSchema); + +declare module '@rocket.chat/rest-typings' { + // eslint-disable-next-line @typescript-eslint/naming-convention + interface Endpoints { + '/v1/chat.getMessageReadReceipts': { + GET: (params: GetMessageReadReceiptsProps) => { + receipts: ReadReceipt[]; + }; + }; + } +} diff --git a/apps/meteor/ee/server/api/chat.ts b/apps/meteor/ee/server/api/chat.ts new file mode 100644 index 000000000000..3255015653d0 --- /dev/null +++ b/apps/meteor/ee/server/api/chat.ts @@ -0,0 +1,33 @@ +import { Meteor } from 'meteor/meteor'; + +import { API } from '../../../app/api/server/api'; +import { isEnterprise } from '../../app/license/server'; + +API.v1.addRoute( + 'chat.getMessageReadReceipts', + { authRequired: true }, + { + async get() { + if (!isEnterprise()) { + throw new Meteor.Error('error-action-not-allowed', 'This is an enterprise feature'); + } + + const { messageId } = this.queryParams; + if (!messageId) { + return API.v1.failure({ + error: "The required 'messageId' param is missing.", + }); + } + + try { + return API.v1.success({ + receipts: await Meteor.call('getReadReceipts', { messageId }), + }); + } catch (error: any) { + return API.v1.failure({ + error: error.message, + }); + } + }, + }, +); diff --git a/apps/meteor/ee/server/api/index.ts b/apps/meteor/ee/server/api/index.ts index 510bffe04baf..87d69543f124 100644 --- a/apps/meteor/ee/server/api/index.ts +++ b/apps/meteor/ee/server/api/index.ts @@ -2,3 +2,4 @@ import './api'; import './ldap'; import './licenses'; import './sessions'; +import './chat'; diff --git a/apps/meteor/imports/message-read-receipt/server/lib/ReadReceipt.js b/apps/meteor/ee/server/lib/message-read-receipt/ReadReceipt.js similarity index 100% rename from apps/meteor/imports/message-read-receipt/server/lib/ReadReceipt.js rename to apps/meteor/ee/server/lib/message-read-receipt/ReadReceipt.js diff --git a/apps/meteor/server/services/reads/service.ts b/apps/meteor/ee/server/local-services/reads/service.ts similarity index 82% rename from apps/meteor/server/services/reads/service.ts rename to apps/meteor/ee/server/local-services/reads/service.ts index 9ce1e92f454a..0eba154e1181 100644 --- a/apps/meteor/server/services/reads/service.ts +++ b/apps/meteor/ee/server/local-services/reads/service.ts @@ -1,10 +1,10 @@ import { Reads } from '@rocket.chat/models'; import type { ISubscription } from '@rocket.chat/core-typings'; import { ServiceClassInternal } from '@rocket.chat/core-services'; -import type { IReadsService } from '@rocket.chat/core-services'; -import { Messages, Subscriptions } from '../../../app/models/server'; -import { ReadReceipt } from '../../../imports/message-read-receipt/server/lib/ReadReceipt'; +import type { IReadsService } from '../../sdk/types/IReadsService'; +import { Messages, Subscriptions } from '../../../../app/models/server'; +import { ReadReceipt } from '../../lib/message-read-receipt/ReadReceipt'; export class ReadsService extends ServiceClassInternal implements IReadsService { protected name = 'reads'; @@ -33,7 +33,7 @@ export class ReadsService extends ServiceClassInternal implements IReadsService } const firstRead = await Reads.getMinimumLastSeenByThreadId(tmid); - if (firstRead && firstRead.ls) { + if (firstRead?.ls) { Messages.setThreadMessagesAsRead(tmid, firstRead.ls); } } diff --git a/apps/meteor/server/models/ReadReceipts.ts b/apps/meteor/ee/server/models/ReadReceipts.ts similarity index 76% rename from apps/meteor/server/models/ReadReceipts.ts rename to apps/meteor/ee/server/models/ReadReceipts.ts index d54a985ef18a..079c7c34a6a8 100644 --- a/apps/meteor/server/models/ReadReceipts.ts +++ b/apps/meteor/ee/server/models/ReadReceipts.ts @@ -1,6 +1,6 @@ import { registerModel } from '@rocket.chat/models'; -import { db } from '../database/utils'; +import { db } from '../../../server/database/utils'; import { ReadReceiptsRaw } from './raw/ReadReceipts'; registerModel('IReadReceiptsModel', new ReadReceiptsRaw(db)); diff --git a/apps/meteor/server/models/raw/ReadReceipts.ts b/apps/meteor/ee/server/models/raw/ReadReceipts.ts similarity index 91% rename from apps/meteor/server/models/raw/ReadReceipts.ts rename to apps/meteor/ee/server/models/raw/ReadReceipts.ts index 9763a2616c13..bd5eb76bc112 100644 --- a/apps/meteor/server/models/raw/ReadReceipts.ts +++ b/apps/meteor/ee/server/models/raw/ReadReceipts.ts @@ -2,7 +2,7 @@ import type { ReadReceipt, RocketChatRecordDeleted } from '@rocket.chat/core-typ import type { IReadReceiptsModel } from '@rocket.chat/model-typings'; import type { Collection, FindCursor, Db, IndexDescription } from 'mongodb'; -import { BaseRaw } from './BaseRaw'; +import { BaseRaw } from '../../../../server/models/raw/BaseRaw'; export class ReadReceiptsRaw extends BaseRaw implements IReadReceiptsModel { constructor(db: Db, trash?: Collection>) { diff --git a/apps/meteor/ee/server/models/startup.ts b/apps/meteor/ee/server/models/startup.ts index 9645c9cd0501..e66f22a2190d 100644 --- a/apps/meteor/ee/server/models/startup.ts +++ b/apps/meteor/ee/server/models/startup.ts @@ -7,4 +7,5 @@ onLicense('livechat-enterprise', () => { import('./LivechatUnit'); import('./LivechatUnitMonitors'); import('./LivechatRooms'); + import('./ReadReceipts'); }); diff --git a/apps/meteor/ee/server/sdk/index.ts b/apps/meteor/ee/server/sdk/index.ts index 0a463a498843..bf0286f0d9ee 100644 --- a/apps/meteor/ee/server/sdk/index.ts +++ b/apps/meteor/ee/server/sdk/index.ts @@ -1,5 +1,6 @@ import { proxifyWithWait } from '@rocket.chat/core-services'; -import type { ILDAPEEService } from './types/ILDAPEEService'; +import type { ILDAPEEService, IReadsService } from './types'; export const LDAPEE = proxifyWithWait('ldap-enterprise'); +export const Reads = proxifyWithWait('reads'); diff --git a/apps/meteor/ee/server/sdk/types/IReadsService.ts b/apps/meteor/ee/server/sdk/types/IReadsService.ts new file mode 100644 index 000000000000..bc7408f49a29 --- /dev/null +++ b/apps/meteor/ee/server/sdk/types/IReadsService.ts @@ -0,0 +1,3 @@ +export interface IReadsService { + readThread(userId: string, threadId: string): Promise; +} diff --git a/apps/meteor/ee/server/sdk/types/index.ts b/apps/meteor/ee/server/sdk/types/index.ts new file mode 100644 index 000000000000..3a8399568f58 --- /dev/null +++ b/apps/meteor/ee/server/sdk/types/index.ts @@ -0,0 +1,2 @@ +export * from './ILDAPEEService'; +export * from './IReadsService'; diff --git a/apps/meteor/ee/server/startup/services.ts b/apps/meteor/ee/server/startup/services.ts index 37ecf9a21a91..7b1d21345ea4 100644 --- a/apps/meteor/ee/server/startup/services.ts +++ b/apps/meteor/ee/server/startup/services.ts @@ -2,9 +2,11 @@ import { api } from '@rocket.chat/core-services'; import { EnterpriseSettings } from '../../app/settings/server/settings.internalService'; import { LDAPEEService } from '../local-services/ldap/service'; +import { ReadsService } from '../local-services/reads/service'; import { LicenseService } from '../../app/license/server/license.internalService'; // TODO consider registering these services only after a valid license is added api.registerService(new EnterpriseSettings()); api.registerService(new LDAPEEService()); api.registerService(new LicenseService()); +api.registerService(new ReadsService()); diff --git a/apps/meteor/imports/message-read-receipt/server/index.js b/apps/meteor/imports/message-read-receipt/server/index.js deleted file mode 100644 index b58297d84fc6..000000000000 --- a/apps/meteor/imports/message-read-receipt/server/index.js +++ /dev/null @@ -1,4 +0,0 @@ -import './hooks'; -import './settings'; - -import './api/methods/getReadReceipts'; diff --git a/apps/meteor/imports/message-read-receipt/server/settings.ts b/apps/meteor/imports/message-read-receipt/server/settings.ts deleted file mode 100644 index 8a20742847af..000000000000 --- a/apps/meteor/imports/message-read-receipt/server/settings.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { settingsRegistry } from '../../../app/settings/server'; - -settingsRegistry.add('Message_Read_Receipt_Enabled', false, { - group: 'Message', - type: 'boolean', - public: true, -}); - -settingsRegistry.add('Message_Read_Receipt_Store_Users', false, { - group: 'Message', - type: 'boolean', - public: true, - enableQuery: { _id: 'Message_Read_Receipt_Enabled', value: true }, -}); diff --git a/apps/meteor/imports/startup/server/index.ts b/apps/meteor/imports/startup/server/index.ts index 8b7c5bd8020b..90c02562d36c 100644 --- a/apps/meteor/imports/startup/server/index.ts +++ b/apps/meteor/imports/startup/server/index.ts @@ -1,2 +1 @@ -import '../../message-read-receipt/server'; import '../../personal-access-tokens/server'; diff --git a/apps/meteor/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts index f381c9b9fc39..1900e01e20a4 100644 --- a/apps/meteor/lib/callbacks.ts +++ b/apps/meteor/lib/callbacks.ts @@ -42,7 +42,7 @@ type EventLikeCallbackSignatures = { 'afterDeleteMessage': (message: IMessage, room: IRoom) => void; 'validateUserRoles': (userData: Partial) => void; 'workspaceLicenseChanged': (license: string) => void; - 'afterReadMessages': (rid: IRoom['_id'], params: { uid: IUser['_id']; lastSeen: Date }) => void; + 'afterReadMessages': (rid: IRoom['_id'], params: { uid: IUser['_id']; lastSeen?: Date; tmid?: IMessage['_id'] }) => void; 'beforeReadMessages': (rid: IRoom['_id'], uid: IUser['_id']) => void; 'afterDeleteUser': (user: IUser) => void; 'afterFileUpload': (params: { user: IUser; room: IRoom; message: IMessage }) => void; diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 4fb24dac3515..2f592c3a83d6 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -3881,6 +3881,7 @@ "Reactions": "Reactions", "Read_by": "Read by", "Read_only": "Read Only", + "Read_Receipts": "Read Receipts", "This_room_is_read_only": "This room is read only", "Only_people_with_permission_can_send_messages_here": "Only people with permission can send messages here", "Read_only_changed_successfully": "Read only changed successfully", diff --git a/apps/meteor/server/models/startup.ts b/apps/meteor/server/models/startup.ts index 10995488fb57..32b1e5a71530 100644 --- a/apps/meteor/server/models/startup.ts +++ b/apps/meteor/server/models/startup.ts @@ -37,7 +37,6 @@ import './OEmbedCache'; import './PbxEvents'; import './PushToken'; import './Permissions'; -import './ReadReceipts'; import './Reads'; import './Reports'; import './Roles'; diff --git a/apps/meteor/server/services/startup.ts b/apps/meteor/server/services/startup.ts index f955c5d235ea..79be55d3a1ea 100644 --- a/apps/meteor/server/services/startup.ts +++ b/apps/meteor/server/services/startup.ts @@ -20,7 +20,6 @@ import { VideoConfService } from './video-conference/service'; import { isRunningMs } from '../lib/isRunningMs'; import { PushService } from './push/service'; import { DeviceManagementService } from './device-management/service'; -import { ReadsService } from './reads/service'; import { FederationService } from './federation/service'; import { UploadService } from './upload/service'; @@ -44,7 +43,6 @@ api.registerService(new UiKitCoreApp()); api.registerService(new PushService()); api.registerService(new DeviceManagementService()); api.registerService(new VideoConfService()); -api.registerService(new ReadsService()); api.registerService(new FederationService()); api.registerService(new UploadService()); diff --git a/packages/core-services/src/index.ts b/packages/core-services/src/index.ts index d162b90b8210..20b6761377b6 100644 --- a/packages/core-services/src/index.ts +++ b/packages/core-services/src/index.ts @@ -35,7 +35,6 @@ import type { IDeviceManagementService } from './types/IDeviceManagementService' import type { IPushService } from './types/IPushService'; import type { IOmnichannelService } from './types/IOmnichannelService'; import type { ITelemetryEvent, TelemetryMap, TelemetryEvents } from './types/ITelemetryEvent'; -import type { IReadsService } from './types/IReadsService'; export { asyncLocalStorage } from './lib/asyncLocalStorage'; export { MeteorError, isMeteorError } from './MeteorError'; @@ -72,7 +71,6 @@ export { IOmnichannelVoipService, IPresence, IPushService, - IReadsService, IRoomService, ISAUMonitorService, ISubscriptionExtraData, @@ -121,7 +119,6 @@ export const LDAP = proxifyWithWait('ldap'); export const SAUMonitor = proxifyWithWait('sau-monitor'); export const DeviceManagement = proxifyWithWait('device-management'); export const VideoConf = proxifyWithWait('video-conference'); -export const Reads = proxifyWithWait('reads'); export const Upload = proxifyWithWait('upload'); // Calls without wait. Means that the service is optional and the result may be an error From 9fdaeaabc4647c6f4851cfa75546e520c748dcfc Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Tue, 24 Jan 2023 14:45:11 -0300 Subject: [PATCH 07/29] Fix getReadReceipts method in EE --- .../ReadReceiptsModal/ReadReceiptsModal.tsx | 9 +++---- .../server/hooks/afterReadMessages.ts | 24 ++++++++++++++++++ .../hooks/{hooks.js => afterSaveMessage.ts} | 25 +++---------------- .../server/hooks/index.ts | 2 ++ .../app/message-read-receipt/server/index.js | 3 +-- apps/meteor/ee/server/index.ts | 2 ++ .../lib/message-read-receipt/ReadReceipt.js | 2 ++ .../server/methods/getReadReceipts.js | 8 +++--- packages/core-typings/src/IRoom.ts | 2 +- 9 files changed, 43 insertions(+), 34 deletions(-) create mode 100644 apps/meteor/ee/app/message-read-receipt/server/hooks/afterReadMessages.ts rename apps/meteor/ee/app/message-read-receipt/server/hooks/{hooks.js => afterSaveMessage.ts} (57%) create mode 100644 apps/meteor/ee/app/message-read-receipt/server/hooks/index.ts rename apps/meteor/ee/{app/message-read-receipt => }/server/methods/getReadReceipts.js (75%) diff --git a/apps/meteor/client/views/room/modals/ReadReceiptsModal/ReadReceiptsModal.tsx b/apps/meteor/client/views/room/modals/ReadReceiptsModal/ReadReceiptsModal.tsx index a2ebb7151808..418f45baefa5 100644 --- a/apps/meteor/client/views/room/modals/ReadReceiptsModal/ReadReceiptsModal.tsx +++ b/apps/meteor/client/views/room/modals/ReadReceiptsModal/ReadReceiptsModal.tsx @@ -1,6 +1,6 @@ import type { IMessage, ReadReceipt } from '@rocket.chat/core-typings'; import { Skeleton } from '@rocket.chat/fuselage'; -import { useEndpoint, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; +import { useMethod, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import type { ReactElement } from 'react'; import React, { useEffect } from 'react'; @@ -17,12 +17,9 @@ const ReadReceiptsModal = ({ messageId, onClose }: ReadReceiptsModalProps): Reac const t = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); - const getReadReceipts = useEndpoint('GET', '/v1/chat.getMessageReadReceipts'); + const getReadReceipts = useMethod('getReadReceipts'); - const readReceiptsResult = useQuery( - ['read-receipts', messageId], - async () => (await getReadReceipts({ messageId })).receipts, - ); + const readReceiptsResult = useQuery(['read-receipts', messageId], () => getReadReceipts({ messageId })); useEffect(() => { if (readReceiptsResult.isError) { diff --git a/apps/meteor/ee/app/message-read-receipt/server/hooks/afterReadMessages.ts b/apps/meteor/ee/app/message-read-receipt/server/hooks/afterReadMessages.ts new file mode 100644 index 000000000000..cb1b32fba912 --- /dev/null +++ b/apps/meteor/ee/app/message-read-receipt/server/hooks/afterReadMessages.ts @@ -0,0 +1,24 @@ +import type { IUser, IRoom, IMessage } from '@rocket.chat/core-typings'; + +import { Reads } from '../../../../server/sdk'; +import { ReadReceipt } from '../../../../server/lib/message-read-receipt/ReadReceipt'; +import { callbacks } from '../../../../../lib/callbacks'; +import { settings } from '../../../../../app/settings/server'; + +callbacks.add( + 'afterReadMessages', + (rid: IRoom['_id'], params: { uid: IUser['_id']; lastSeen?: Date; tmid?: IMessage['_id'] }) => { + if (!settings.get('Message_Read_Receipt_Enabled')) { + return; + } + const { uid, lastSeen, tmid } = params; + + if (tmid) { + Reads.readThread(uid, tmid); + } else if (lastSeen) { + ReadReceipt.markMessagesAsRead(rid, uid, lastSeen); + } + }, + callbacks.priority.MEDIUM, + 'message-read-receipt-afterReadMessages', +); diff --git a/apps/meteor/ee/app/message-read-receipt/server/hooks/hooks.js b/apps/meteor/ee/app/message-read-receipt/server/hooks/afterSaveMessage.ts similarity index 57% rename from apps/meteor/ee/app/message-read-receipt/server/hooks/hooks.js rename to apps/meteor/ee/app/message-read-receipt/server/hooks/afterSaveMessage.ts index 0847a3dc99f1..2ce472bac518 100644 --- a/apps/meteor/ee/app/message-read-receipt/server/hooks/hooks.js +++ b/apps/meteor/ee/app/message-read-receipt/server/hooks/afterSaveMessage.ts @@ -1,15 +1,15 @@ import { Subscriptions } from '@rocket.chat/models'; +import type { IRoom, IMessage } from '@rocket.chat/core-typings'; +import { isEditedMessage } from '@rocket.chat/core-typings'; -import { Reads } from '../../../../server/sdk'; import { ReadReceipt } from '../../../../server/lib/message-read-receipt/ReadReceipt'; import { callbacks } from '../../../../../lib/callbacks'; -import { settings } from '../../../../../app/settings/server'; callbacks.add( 'afterSaveMessage', - (message, room) => { + (message: IMessage, room: IRoom) => { // skips this callback if the message was edited - if (message.editedAt) { + if (isEditedMessage(message) && message.editedAt) { return message; } @@ -26,20 +26,3 @@ callbacks.add( callbacks.priority.MEDIUM, 'message-read-receipt-afterSaveMessage', ); - -callbacks.add( - 'afterReadMessages', - (rid, { uid, lastSeen, tmid }) => { - if (!settings.get('Message_Read_Receipt_Enabled')) { - return; - } - - if (tmid) { - Reads.readThread(uid, tmid); - } else if (lastSeen) { - ReadReceipt.markMessagesAsRead(rid, uid, lastSeen); - } - }, - callbacks.priority.MEDIUM, - 'message-read-receipt-afterReadMessages', -); diff --git a/apps/meteor/ee/app/message-read-receipt/server/hooks/index.ts b/apps/meteor/ee/app/message-read-receipt/server/hooks/index.ts new file mode 100644 index 000000000000..8ef3f27635d0 --- /dev/null +++ b/apps/meteor/ee/app/message-read-receipt/server/hooks/index.ts @@ -0,0 +1,2 @@ +import './afterReadMessages'; +import './afterSaveMessage'; diff --git a/apps/meteor/ee/app/message-read-receipt/server/index.js b/apps/meteor/ee/app/message-read-receipt/server/index.js index e459c2f0889c..766ffb0cc653 100644 --- a/apps/meteor/ee/app/message-read-receipt/server/index.js +++ b/apps/meteor/ee/app/message-read-receipt/server/index.js @@ -2,8 +2,7 @@ import { onLicense } from '../../license/server'; onLicense('message-read-receipt', () => { const { createSettings } = require('./settings'); - require('./hooks/hooks'); - require('./methods/getReadReceipts'); + require('./hooks'); createSettings(); }); diff --git a/apps/meteor/ee/server/index.ts b/apps/meteor/ee/server/index.ts index c0af6bd0ff61..c7526c492797 100644 --- a/apps/meteor/ee/server/index.ts +++ b/apps/meteor/ee/server/index.ts @@ -7,6 +7,7 @@ import '../app/auditing/server/index'; import '../app/authorization/server/index'; import '../app/canned-responses/server/index'; import '../app/livechat-enterprise/server/index'; +import '../app/message-read-receipt/server/index'; import '../app/voip-enterprise/server/index'; import '../app/settings/server/index'; import '../app/teams-mention/server/index'; @@ -16,3 +17,4 @@ import './requestSeatsRoute'; import './configuration/index'; import './local-services/ldap/service'; import './settings/index'; +import './methods/getReadReceipts'; diff --git a/apps/meteor/ee/server/lib/message-read-receipt/ReadReceipt.js b/apps/meteor/ee/server/lib/message-read-receipt/ReadReceipt.js index b0443b0ab221..fa6e0e888983 100644 --- a/apps/meteor/ee/server/lib/message-read-receipt/ReadReceipt.js +++ b/apps/meteor/ee/server/lib/message-read-receipt/ReadReceipt.js @@ -53,6 +53,7 @@ export const ReadReceipt = { }, markMessageAsReadBySender(message, { _id: roomId, t }, userId) { + console.log('Mark as read by sender'); if (!settings.get('Message_Read_Receipt_Enabled')) { return; } @@ -69,6 +70,7 @@ export const ReadReceipt = { const extraData = roomCoordinator.getRoomDirectives(t)?.getReadReceiptsExtraData(message); this.storeReadReceipts([{ _id: message._id }], roomId, userId, extraData); + console.log('Mark as read by sender - store'); }, storeThreadMessagesReadReceipts(tmid, userId, userLastSeen) { diff --git a/apps/meteor/ee/app/message-read-receipt/server/methods/getReadReceipts.js b/apps/meteor/ee/server/methods/getReadReceipts.js similarity index 75% rename from apps/meteor/ee/app/message-read-receipt/server/methods/getReadReceipts.js rename to apps/meteor/ee/server/methods/getReadReceipts.js index bbb2ec6fc41a..cd32f1f283a0 100644 --- a/apps/meteor/ee/app/message-read-receipt/server/methods/getReadReceipts.js +++ b/apps/meteor/ee/server/methods/getReadReceipts.js @@ -1,12 +1,12 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; -import { Messages } from '../../../../../app/models/server'; -import { canAccessRoomId } from '../../../../../app/authorization/server'; -import { ReadReceipt } from '../../../../server/lib/message-read-receipt/ReadReceipt'; +import { Messages } from '../../../app/models/server'; +import { canAccessRoomId } from '../../../app/authorization/server'; +import { ReadReceipt } from '../lib/message-read-receipt/ReadReceipt'; Meteor.methods({ - async getReadReceipts({ messageId }) { + getReadReceipts({ messageId }) { if (!messageId) { throw new Meteor.Error('error-invalid-message', "The required 'messageId' param is missing.", { method: 'getReadReceipts' }); } diff --git a/packages/core-typings/src/IRoom.ts b/packages/core-typings/src/IRoom.ts index b44fd8b8b5be..48bcc5ea3687 100644 --- a/packages/core-typings/src/IRoom.ts +++ b/packages/core-typings/src/IRoom.ts @@ -74,6 +74,7 @@ export interface IRoom extends IRocketChatRecord { usernames?: string[]; ts?: Date; + closedAt?: Date; cl?: boolean; ro?: boolean; favorite?: boolean; @@ -167,7 +168,6 @@ export interface IOmnichannelGenericRoom extends Omit Date: Thu, 26 Jan 2023 12:53:55 -0300 Subject: [PATCH 08/29] Remove logs --- apps/meteor/ee/server/lib/message-read-receipt/ReadReceipt.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/meteor/ee/server/lib/message-read-receipt/ReadReceipt.js b/apps/meteor/ee/server/lib/message-read-receipt/ReadReceipt.js index fa6e0e888983..b0443b0ab221 100644 --- a/apps/meteor/ee/server/lib/message-read-receipt/ReadReceipt.js +++ b/apps/meteor/ee/server/lib/message-read-receipt/ReadReceipt.js @@ -53,7 +53,6 @@ export const ReadReceipt = { }, markMessageAsReadBySender(message, { _id: roomId, t }, userId) { - console.log('Mark as read by sender'); if (!settings.get('Message_Read_Receipt_Enabled')) { return; } @@ -70,7 +69,6 @@ export const ReadReceipt = { const extraData = roomCoordinator.getRoomDirectives(t)?.getReadReceiptsExtraData(message); this.storeReadReceipts([{ _id: message._id }], roomId, userId, extraData); - console.log('Mark as read by sender - store'); }, storeThreadMessagesReadReceipts(tmid, userId, userLastSeen) { From 11ecc8c92b53dec7b362bbc66659ca5091e41e76 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Fri, 27 Jan 2023 09:22:48 -0300 Subject: [PATCH 09/29] Add EE license check --- apps/meteor/ee/server/api/chat.ts | 4 ++-- apps/meteor/ee/server/methods/getReadReceipts.js | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/meteor/ee/server/api/chat.ts b/apps/meteor/ee/server/api/chat.ts index 3255015653d0..ebc6e1b04cb2 100644 --- a/apps/meteor/ee/server/api/chat.ts +++ b/apps/meteor/ee/server/api/chat.ts @@ -1,14 +1,14 @@ import { Meteor } from 'meteor/meteor'; import { API } from '../../../app/api/server/api'; -import { isEnterprise } from '../../app/license/server'; +import { hasLicense } from '../../app/license/server/license'; API.v1.addRoute( 'chat.getMessageReadReceipts', { authRequired: true }, { async get() { - if (!isEnterprise()) { + if (!hasLicense('message-read-receipt')) { throw new Meteor.Error('error-action-not-allowed', 'This is an enterprise feature'); } diff --git a/apps/meteor/ee/server/methods/getReadReceipts.js b/apps/meteor/ee/server/methods/getReadReceipts.js index cd32f1f283a0..9e20297d08e0 100644 --- a/apps/meteor/ee/server/methods/getReadReceipts.js +++ b/apps/meteor/ee/server/methods/getReadReceipts.js @@ -3,10 +3,15 @@ import { check } from 'meteor/check'; import { Messages } from '../../../app/models/server'; import { canAccessRoomId } from '../../../app/authorization/server'; +import { hasLicense } from '../../app/license/server/license'; import { ReadReceipt } from '../lib/message-read-receipt/ReadReceipt'; Meteor.methods({ getReadReceipts({ messageId }) { + if (!hasLicense('message-read-receipt')) { + throw new Meteor.Error('error-action-not-allowed', 'This is an enterprise feature', { method: 'getReadReceipts' }); + } + if (!messageId) { throw new Meteor.Error('error-invalid-message', "The required 'messageId' param is missing.", { method: 'getReadReceipts' }); } From 030fd7c83a26cc939408014f008f3123446a25ef Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Fri, 27 Jan 2023 09:23:11 -0300 Subject: [PATCH 10/29] Apply requested changes --- apps/meteor/ee/server/local-services/reads/service.ts | 7 +++---- apps/meteor/server/models/raw/Reads.ts | 10 +++++++++- packages/model-typings/src/models/IReadsModel.ts | 1 + 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/apps/meteor/ee/server/local-services/reads/service.ts b/apps/meteor/ee/server/local-services/reads/service.ts index 0eba154e1181..f63a10c5cfd7 100644 --- a/apps/meteor/ee/server/local-services/reads/service.ts +++ b/apps/meteor/ee/server/local-services/reads/service.ts @@ -12,22 +12,21 @@ export class ReadsService extends ServiceClassInternal implements IReadsService async readThread(userId: string, tmid: string): Promise { const read = await Reads.findOneByUserIdAndThreadId(userId, tmid); - await Reads.updateReadTimestampByUserIdAndThreadId(userId, tmid); - const threadMessage = Messages.findOneById(tmid, { projection: { ts: 1, tlm: 1, rid: 1 } }); if (!threadMessage || !threadMessage.tlm) { return; } + await Reads.updateReadTimestampByUserIdAndThreadId(userId, tmid); ReadReceipt.storeThreadMessagesReadReceipts(tmid, userId, read?.ls || threadMessage.ts); // doesn't mark as read if not all room members have read the thread const subscriptions = await Subscriptions.findByRoomId(threadMessage.rid, { fields: { 'u._id': 1 }, }); - const members = subscriptions.map((s: ISubscription) => s.u?._id); + const members = subscriptions.map((s: ISubscription) => !s?.archived && s.u?._id); - const totalReads = await Reads.col.countDocuments({ tmid, userId: { $in: members } }); + const totalReads = await Reads.countByThreadAndUserIds(tmid, members); if (totalReads < members.length) { return; } diff --git a/apps/meteor/server/models/raw/Reads.ts b/apps/meteor/server/models/raw/Reads.ts index cdccc9cf05d8..029ae4a8baaf 100644 --- a/apps/meteor/server/models/raw/Reads.ts +++ b/apps/meteor/server/models/raw/Reads.ts @@ -10,7 +10,7 @@ export class ReadsRaw extends BaseRaw implements IReadsModel { } protected modelIndexes(): IndexDescription[] { - return [{ key: { tmid: 1, userId: 1 }, unique: true }, { key: { tmid: 1 } }]; + return [{ key: { tmid: 1, userId: 1 }, unique: true }]; } async findOneByUserIdAndThreadId(userId: IUser['_id'], tmid: IMessage['_id']): Promise { @@ -44,4 +44,12 @@ export class ReadsRaw extends BaseRaw implements IReadsModel { return this.updateOne(query, update, { upsert: true }); } + + async countByThreadAndUserIds(tmid: IMessage['_id'], userIds: IUser['_id'][]): Promise { + const query = { + tmid, + userId: { $in: userIds }, + }; + return this.col.countDocuments(query); + } } diff --git a/packages/model-typings/src/models/IReadsModel.ts b/packages/model-typings/src/models/IReadsModel.ts index c64912b56a7e..608885474e8c 100644 --- a/packages/model-typings/src/models/IReadsModel.ts +++ b/packages/model-typings/src/models/IReadsModel.ts @@ -7,4 +7,5 @@ export interface IReadsModel extends IBaseModel { findOneByUserIdAndThreadId(userId: IUser['_id'], tmid: IMessage['_id']): Promise; getMinimumLastSeenByThreadId(tmid: string): Promise; updateReadTimestampByUserIdAndThreadId(userId: IUser['_id'], tmid: IMessage['_id']): Promise; + countByThreadAndUserIds(tmid: IMessage['_id'], userIds: IUser['_id'][]): Promise; } From 1bf8ba04ace89139834e1ebb91d00774558132cc Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Mon, 30 Jan 2023 11:28:49 -0300 Subject: [PATCH 11/29] Update read receipts on readThreads call --- apps/meteor/ee/definition/rest/v1/chat.ts | 4 ++-- apps/meteor/server/methods/readThreads.js | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/meteor/ee/definition/rest/v1/chat.ts b/apps/meteor/ee/definition/rest/v1/chat.ts index f2f6ef328418..dee7cadc6542 100644 --- a/apps/meteor/ee/definition/rest/v1/chat.ts +++ b/apps/meteor/ee/definition/rest/v1/chat.ts @@ -9,7 +9,7 @@ type GetMessageReadReceiptsProps = { messageId: IMessage['_id']; }; -const roleUpdatePropsSchema = { +const getMessageReadReceiptsPropsSchema = { type: 'object', properties: { messageId: { @@ -20,7 +20,7 @@ const roleUpdatePropsSchema = { additionalProperties: false, }; -export const isGetMessageReadReceiptsProps = ajv.compile(roleUpdatePropsSchema); +export const isGetMessageReadReceiptsProps = ajv.compile(getMessageReadReceiptsPropsSchema); declare module '@rocket.chat/rest-typings' { // eslint-disable-next-line @typescript-eslint/naming-convention diff --git a/apps/meteor/server/methods/readThreads.js b/apps/meteor/server/methods/readThreads.js index 4a735ba9a47e..8f424a76085c 100644 --- a/apps/meteor/server/methods/readThreads.js +++ b/apps/meteor/server/methods/readThreads.js @@ -5,6 +5,7 @@ import { settings } from '../../app/settings/server'; import { Messages, Rooms } from '../../app/models/server'; import { canAccessRoom } from '../../app/authorization/server'; import { readThread } from '../../app/threads/server/functions'; +import { callbacks } from '../../lib/callbacks'; Meteor.methods({ readThreads(tmid) { @@ -22,12 +23,15 @@ Meteor.methods({ } const user = Meteor.user(); + callbacks.run('beforeReadMessages', thread.rid, user._id); + const room = Rooms.findOneById(thread.rid); if (!canAccessRoom(room, user)) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'getThreadMessages' }); } - return readThread({ userId: user._id, rid: thread.rid, tmid }); + readThread({ userId: user._id, rid: thread.rid, tmid }); + callbacks.runAsync('afterReadMessages', room._id, { uid: user._id, tmid }); }, }); From 169f20551b8aa75c2a96f3912336a69f8c56c81d Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Mon, 30 Jan 2023 12:49:25 -0300 Subject: [PATCH 12/29] Add new strategy to change thread messages to read in large rooms --- apps/meteor/ee/server/lib/constants.ts | 1 + .../ee/server/local-services/reads/service.ts | 15 ++++++++++++--- apps/meteor/server/models/raw/Reads.ts | 7 +++++++ packages/model-typings/src/models/IReadsModel.ts | 1 + 4 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 apps/meteor/ee/server/lib/constants.ts diff --git a/apps/meteor/ee/server/lib/constants.ts b/apps/meteor/ee/server/lib/constants.ts new file mode 100644 index 000000000000..dfaa54af84d2 --- /dev/null +++ b/apps/meteor/ee/server/lib/constants.ts @@ -0,0 +1 @@ +export const MAX_ROOM_SIZE_CHECK_INDIVIDUAL_READ_RECEIPTS = 50; diff --git a/apps/meteor/ee/server/local-services/reads/service.ts b/apps/meteor/ee/server/local-services/reads/service.ts index f63a10c5cfd7..0d8bc5b3c727 100644 --- a/apps/meteor/ee/server/local-services/reads/service.ts +++ b/apps/meteor/ee/server/local-services/reads/service.ts @@ -5,6 +5,7 @@ import { ServiceClassInternal } from '@rocket.chat/core-services'; import type { IReadsService } from '../../sdk/types/IReadsService'; import { Messages, Subscriptions } from '../../../../app/models/server'; import { ReadReceipt } from '../../lib/message-read-receipt/ReadReceipt'; +import { MAX_ROOM_SIZE_CHECK_INDIVIDUAL_READ_RECEIPTS } from '../../lib/constants'; export class ReadsService extends ServiceClassInternal implements IReadsService { protected name = 'reads'; @@ -26,9 +27,17 @@ export class ReadsService extends ServiceClassInternal implements IReadsService }); const members = subscriptions.map((s: ISubscription) => !s?.archived && s.u?._id); - const totalReads = await Reads.countByThreadAndUserIds(tmid, members); - if (totalReads < members.length) { - return; + if (members.length <= MAX_ROOM_SIZE_CHECK_INDIVIDUAL_READ_RECEIPTS) { + const totalReads = await Reads.countByThreadAndUserIds(tmid, members); + if (totalReads < members.length) { + return; + } + } else { + // for large rooms, mark as read if there are as many reads as room members (instead of checking each user's read receipts) + const totalReads = await Reads.countByThreadId(tmid); + if (totalReads < members.length) { + return; + } } const firstRead = await Reads.getMinimumLastSeenByThreadId(tmid); diff --git a/apps/meteor/server/models/raw/Reads.ts b/apps/meteor/server/models/raw/Reads.ts index 029ae4a8baaf..0ebb60891f32 100644 --- a/apps/meteor/server/models/raw/Reads.ts +++ b/apps/meteor/server/models/raw/Reads.ts @@ -52,4 +52,11 @@ export class ReadsRaw extends BaseRaw implements IReadsModel { }; return this.col.countDocuments(query); } + + async countByThreadId(tmid: IMessage['_id']): Promise { + const query = { + tmid, + }; + return this.col.countDocuments(query); + } } diff --git a/packages/model-typings/src/models/IReadsModel.ts b/packages/model-typings/src/models/IReadsModel.ts index 608885474e8c..750dd17afaa1 100644 --- a/packages/model-typings/src/models/IReadsModel.ts +++ b/packages/model-typings/src/models/IReadsModel.ts @@ -8,4 +8,5 @@ export interface IReadsModel extends IBaseModel { getMinimumLastSeenByThreadId(tmid: string): Promise; updateReadTimestampByUserIdAndThreadId(userId: IUser['_id'], tmid: IMessage['_id']): Promise; countByThreadAndUserIds(tmid: IMessage['_id'], userIds: IUser['_id'][]): Promise; + countByThreadId(tmid: IMessage['_id']): Promise; } From eb702c46b907b27633c561ecff9aac72cd6b5178 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Mon, 30 Jan 2023 14:38:30 -0300 Subject: [PATCH 13/29] Remove startup import --- apps/meteor/client/startup/index.ts | 1 - apps/meteor/ee/server/local-services/reads/service.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/meteor/client/startup/index.ts b/apps/meteor/client/startup/index.ts index 5089227c7397..e409856f3f9b 100644 --- a/apps/meteor/client/startup/index.ts +++ b/apps/meteor/client/startup/index.ts @@ -23,7 +23,6 @@ import './oauth'; import './openedRoom'; import './otr'; import './readMessage'; -import '../../ee/client/startup/readReceipt'; import './reloadRoomAfterLogin'; import './renderMessage'; import './renderNotification'; diff --git a/apps/meteor/ee/server/local-services/reads/service.ts b/apps/meteor/ee/server/local-services/reads/service.ts index 0d8bc5b3c727..150e9ef65682 100644 --- a/apps/meteor/ee/server/local-services/reads/service.ts +++ b/apps/meteor/ee/server/local-services/reads/service.ts @@ -33,7 +33,7 @@ export class ReadsService extends ServiceClassInternal implements IReadsService return; } } else { - // for large rooms, mark as read if there are as many reads as room members (instead of checking each user's read receipts) + // for large rooms, mark as read if there are as many reads as room members to improve performance (instead of checking each read) const totalReads = await Reads.countByThreadId(tmid); if (totalReads < members.length) { return; From cdfdde2b544c0045d61b1351db45fb96da111757 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Mon, 30 Jan 2023 15:24:11 -0300 Subject: [PATCH 14/29] Fix typecheck error --- .../teams/contextualBar/channels/hooks/useTeamsChannelList.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/meteor/client/views/teams/contextualBar/channels/hooks/useTeamsChannelList.ts b/apps/meteor/client/views/teams/contextualBar/channels/hooks/useTeamsChannelList.ts index c9967a0cfbb1..ebab543954b6 100644 --- a/apps/meteor/client/views/teams/contextualBar/channels/hooks/useTeamsChannelList.ts +++ b/apps/meteor/client/views/teams/contextualBar/channels/hooks/useTeamsChannelList.ts @@ -41,9 +41,10 @@ export const useTeamsChannelList = ( }); return { - items: rooms.map(({ _updatedAt, lastMessage, lm, ts, webRtcCallStartTime, ...room }) => ({ + items: rooms.map(({ _updatedAt, lastMessage, lm, ts, closedAt, webRtcCallStartTime, ...room }) => ({ ...(lm && { lm: new Date(lm) }), ...(ts && { ts: new Date(ts) }), + ...(closedAt && { closedAt: new Date(closedAt) }), _updatedAt: new Date(_updatedAt), ...(lastMessage && { lastMessage: mapMessageFromApi(lastMessage) }), ...(webRtcCallStartTime && { webRtcCallStartTime: new Date(webRtcCallStartTime) }), From a31375aeeeb33afe1c05aa22cd1f4c757848722d Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Tue, 31 Jan 2023 15:58:18 -0300 Subject: [PATCH 15/29] Remove chat.getMessageReadReceipts endpoint tests (TODO - add in a new file) --- apps/meteor/tests/end-to-end/api/05-chat.js | 33 --------------------- 1 file changed, 33 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/05-chat.js b/apps/meteor/tests/end-to-end/api/05-chat.js index 601dcf30dbb4..777522dadfd1 100644 --- a/apps/meteor/tests/end-to-end/api/05-chat.js +++ b/apps/meteor/tests/end-to-end/api/05-chat.js @@ -1346,39 +1346,6 @@ describe('[Chat]', function () { }); }); - describe('[/chat.getMessageReadReceipts]', () => { - describe('when execute successfully', () => { - it("should return the statusCode 200 and 'receipts' property and should be equal an array", (done) => { - request - .get(api(`chat.getMessageReadReceipts?messageId=${message._id}`)) - .set(credentials) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('receipts').and.to.be.an('array'); - expect(res.body).to.have.property('success', true); - }) - .end(done); - }); - }); - - describe('when an error occurs', () => { - it('should return statusCode 400 and an error', (done) => { - request - .get(api('chat.getMessageReadReceipts')) - .set(credentials) - .expect('Content-Type', 'application/json') - .expect(400) - .expect((res) => { - expect(res.body).not.have.property('receipts'); - expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('error'); - }) - .end(done); - }); - }); - }); - describe('[/chat.reportMessage]', () => { describe('when execute successfully', () => { it('should return the statusCode 200', (done) => { From 7e6538aa73efc5d99ee58ce9b74975bc45590b71 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Wed, 8 Feb 2023 10:53:16 -0300 Subject: [PATCH 16/29] Make read receipts settings visible to community --- .../meteor/app/lib/server/startup/settings.ts | 17 ++++++++++++++ .../app/message-read-receipt/server/index.js | 3 --- .../message-read-receipt/server/settings.ts | 22 ------------------- 3 files changed, 17 insertions(+), 25 deletions(-) delete mode 100644 apps/meteor/ee/app/message-read-receipt/server/settings.ts diff --git a/apps/meteor/app/lib/server/startup/settings.ts b/apps/meteor/app/lib/server/startup/settings.ts index 4e45541acdce..c058850afdf9 100644 --- a/apps/meteor/app/lib/server/startup/settings.ts +++ b/apps/meteor/app/lib/server/startup/settings.ts @@ -1211,6 +1211,23 @@ settingsRegistry.addGroup('Message', function () { public: true, }); }); + this.section('Read_Receipts', function () { + this.add('Message_Read_Receipt_Enabled', false, { + type: 'boolean', + enterprise: true, + invalidValue: false, + modules: ['message-read-receipt'], + public: true, + }); + this.add('Message_Read_Receipt_Store_Users', false, { + type: 'boolean', + enterprise: true, + invalidValue: false, + modules: ['message-read-receipt'], + public: true, + enableQuery: { _id: 'Message_Read_Receipt_Enabled', value: true }, + }); + }); this.add('Message_AllowEditing', true, { type: 'boolean', public: true, diff --git a/apps/meteor/ee/app/message-read-receipt/server/index.js b/apps/meteor/ee/app/message-read-receipt/server/index.js index 766ffb0cc653..2b0ee13bbedc 100644 --- a/apps/meteor/ee/app/message-read-receipt/server/index.js +++ b/apps/meteor/ee/app/message-read-receipt/server/index.js @@ -1,8 +1,5 @@ import { onLicense } from '../../license/server'; onLicense('message-read-receipt', () => { - const { createSettings } = require('./settings'); require('./hooks'); - - createSettings(); }); diff --git a/apps/meteor/ee/app/message-read-receipt/server/settings.ts b/apps/meteor/ee/app/message-read-receipt/server/settings.ts deleted file mode 100644 index 681ad8756136..000000000000 --- a/apps/meteor/ee/app/message-read-receipt/server/settings.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { settingsRegistry } from '../../../../app/settings/server'; - -export const createSettings = (): void => { - settingsRegistry.add('Message_Read_Receipt_Enabled', false, { - group: 'Message', - enterprise: true, - modules: ['message-read-receipt'], - type: 'boolean', - invalidValue: false, - public: true, - }); - - settingsRegistry.add('Message_Read_Receipt_Store_Users', false, { - group: 'Message', - enterprise: true, - modules: ['message-read-receipt'], - type: 'boolean', - invalidValue: false, - public: true, - enableQuery: { _id: 'Message_Read_Receipt_Enabled', value: true }, - }); -}; From cd506552b812fe6c25dff1d1c9d25c82e97232f8 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Fri, 10 Feb 2023 14:40:54 -0300 Subject: [PATCH 17/29] Force update cached collection on version upgrade (so as to get the correct value of ee settings) --- .../app/ui-cached-collection/client/models/CachedCollection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.ts b/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.ts index ba042a930bb8..ca2d3e26cc73 100644 --- a/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.ts +++ b/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.ts @@ -44,7 +44,7 @@ export class CachedCollection extends Emitter<{ changed: T; re public eventType: EventType; - public version = 17; + public version = 18; public userRelated: boolean; From 461d877171037f7bb96fe6bac531822291a627f7 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Fri, 10 Feb 2023 14:42:07 -0300 Subject: [PATCH 18/29] Filter unarchived subscriptions on DB query --- .../ee/server/local-services/reads/service.ts | 20 ++++++++++--------- .../meteor/server/models/raw/Subscriptions.ts | 19 ++++++++++++++++++ .../src/models/ISubscriptionsModel.ts | 4 ++++ 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/apps/meteor/ee/server/local-services/reads/service.ts b/apps/meteor/ee/server/local-services/reads/service.ts index 150e9ef65682..ee85ccf1bf9c 100644 --- a/apps/meteor/ee/server/local-services/reads/service.ts +++ b/apps/meteor/ee/server/local-services/reads/service.ts @@ -1,9 +1,9 @@ -import { Reads } from '@rocket.chat/models'; +import { Reads, Subscriptions } from '@rocket.chat/models'; import type { ISubscription } from '@rocket.chat/core-typings'; import { ServiceClassInternal } from '@rocket.chat/core-services'; import type { IReadsService } from '../../sdk/types/IReadsService'; -import { Messages, Subscriptions } from '../../../../app/models/server'; +import { Messages } from '../../../../app/models/server'; import { ReadReceipt } from '../../lib/message-read-receipt/ReadReceipt'; import { MAX_ROOM_SIZE_CHECK_INDIVIDUAL_READ_RECEIPTS } from '../../lib/constants'; @@ -22,20 +22,22 @@ export class ReadsService extends ServiceClassInternal implements IReadsService ReadReceipt.storeThreadMessagesReadReceipts(tmid, userId, read?.ls || threadMessage.ts); // doesn't mark as read if not all room members have read the thread - const subscriptions = await Subscriptions.findByRoomId(threadMessage.rid, { - fields: { 'u._id': 1 }, - }); - const members = subscriptions.map((s: ISubscription) => !s?.archived && s.u?._id); + const membersCount = await Subscriptions.countUnarchivedByRoomId(threadMessage.rid); + + if (membersCount <= MAX_ROOM_SIZE_CHECK_INDIVIDUAL_READ_RECEIPTS) { + const subscriptions = await Subscriptions.findUnarchivedByRoomId(threadMessage.rid, { + projection: { 'u._id': 1 }, + }).toArray(); + const members = subscriptions.map((s: ISubscription) => s.u._id); - if (members.length <= MAX_ROOM_SIZE_CHECK_INDIVIDUAL_READ_RECEIPTS) { const totalReads = await Reads.countByThreadAndUserIds(tmid, members); - if (totalReads < members.length) { + if (totalReads < membersCount) { return; } } else { // for large rooms, mark as read if there are as many reads as room members to improve performance (instead of checking each read) const totalReads = await Reads.countByThreadId(tmid); - if (totalReads < members.length) { + if (totalReads < membersCount) { return; } } diff --git a/apps/meteor/server/models/raw/Subscriptions.ts b/apps/meteor/server/models/raw/Subscriptions.ts index d579c52eb4ec..33e868bcd459 100644 --- a/apps/meteor/server/models/raw/Subscriptions.ts +++ b/apps/meteor/server/models/raw/Subscriptions.ts @@ -71,6 +71,16 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return this.find(query, options); } + findUnarchivedByRoomId(roomId: string, options: FindOptions = {}): FindCursor { + const query = { + 'rid': roomId, + 'archived': { $ne: true }, + 'u._id': { $exists: true }, + }; + + return this.find(query, options); + } + findByRoomIdAndNotUserId(roomId: string, userId: string, options: FindOptions = {}): FindCursor { const query = { 'rid': roomId, @@ -102,6 +112,15 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return this.col.countDocuments(query); } + countUnarchivedByRoomId(rid: string): Promise { + const query = { + rid, + 'archived': { $ne: true }, + 'u._id': { $exists: true }, + }; + return this.col.countDocuments(query); + } + async isUserInRole(uid: IUser['_id'], roleId: IRole['_id'], rid?: IRoom['_id']): Promise { if (rid == null) { return null; diff --git a/packages/model-typings/src/models/ISubscriptionsModel.ts b/packages/model-typings/src/models/ISubscriptionsModel.ts index f0b80986dfec..ac2a6c5975bb 100644 --- a/packages/model-typings/src/models/ISubscriptionsModel.ts +++ b/packages/model-typings/src/models/ISubscriptionsModel.ts @@ -12,12 +12,16 @@ export interface ISubscriptionsModel extends IBaseModel { findByRoomId(roomId: string, options?: FindOptions): FindCursor; + findUnarchivedByRoomId(roomId: string, options?: FindOptions): FindCursor; + findByRoomIdAndNotUserId(roomId: string, userId: string, options?: FindOptions): FindCursor; findByLivechatRoomIdAndNotUserId(roomId: string, userId: string, options?: FindOptions): FindCursor; countByRoomIdAndUserId(rid: string, uid: string | undefined): Promise; + countUnarchivedByRoomId(rid: string): Promise; + isUserInRole(uid: IUser['_id'], roleId: IRole['_id'], rid?: IRoom['_id']): Promise; setAsReadByRoomIdAndUserId( From 3d3ca04407f4038bf0f7ea8075dbfa7b45757fca Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Fri, 10 Feb 2023 16:03:17 -0300 Subject: [PATCH 19/29] Add ls index in Reads model --- apps/meteor/server/models/raw/Reads.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/server/models/raw/Reads.ts b/apps/meteor/server/models/raw/Reads.ts index 0ebb60891f32..8942b9088069 100644 --- a/apps/meteor/server/models/raw/Reads.ts +++ b/apps/meteor/server/models/raw/Reads.ts @@ -10,7 +10,7 @@ export class ReadsRaw extends BaseRaw implements IReadsModel { } protected modelIndexes(): IndexDescription[] { - return [{ key: { tmid: 1, userId: 1 }, unique: true }]; + return [{ key: { tmid: 1, userId: 1 }, unique: true }, { key: { ls: 1 } }]; } async findOneByUserIdAndThreadId(userId: IUser['_id'], tmid: IMessage['_id']): Promise { From a9cd2ce6d42d20aad16a2b633f78a5cf65cc670d Mon Sep 17 00:00:00 2001 From: LucianoPierdona Date: Fri, 10 Feb 2023 22:18:04 -0300 Subject: [PATCH 20/29] add tests to EE --- apps/meteor/tests/end-to-end/api/05-chat.js | 63 +++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/apps/meteor/tests/end-to-end/api/05-chat.js b/apps/meteor/tests/end-to-end/api/05-chat.js index eee101448d29..5da70cd19632 100644 --- a/apps/meteor/tests/end-to-end/api/05-chat.js +++ b/apps/meteor/tests/end-to-end/api/05-chat.js @@ -1346,6 +1346,69 @@ describe('[Chat]', function () { }); }); + describe('[/chat.getMessageReadReceipts]', () => { + describe('when execute successfully', () => { + it("should return the statusCode 200 and 'receipts' property and should be equal an array", (done) => { + if (!process.env.IS_EE) { + this.skip(); + return; + } + + request + .get(api(`chat.getMessageReadReceipts?messageId=${message._id}`)) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('receipts').and.to.be.an('array'); + expect(res.body).to.have.property('success', true); + }) + .end(done); + }); + }); + + describe('when an error occurs', () => { + it('should throw an error containing totp-required error when not running EE', function (done) { + // TODO this is not the right way to do it. We're doing this way for now just because we have separate CI jobs for EE and CE, + // ideally we should have a single CI job that adds a license and runs both CE and EE tests. + if (process.env.IS_EE) { + this.skip(); + return; + } + request + .get(api(`chat.getMessageReadReceipts?messageId=${message._id}`)) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error', 'This is an enterprise feature [error-action-not-allowed]'); + expect(res.body).to.have.property('errorType', 'error-action-not-allowed'); + }) + .end(done); + }); + + it('should return statusCode 400 and an error', (done) => { + if (!process.env.IS_EE) { + this.skip(); + return; + } + + request + .get(api('chat.getMessageReadReceipts')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).not.have.property('receipts'); + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error'); + }) + .end(done); + }); + }); + }); + describe('[/chat.reportMessage]', () => { describe('when execute successfully', () => { it('should return the statusCode 200', (done) => { From f576c08a0ce67faf86166c5066d2ee2f2671f5a7 Mon Sep 17 00:00:00 2001 From: LucianoPierdona Date: Fri, 10 Feb 2023 22:30:20 -0300 Subject: [PATCH 21/29] update tests --- apps/meteor/tests/end-to-end/api/05-chat.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/05-chat.js b/apps/meteor/tests/end-to-end/api/05-chat.js index 5da70cd19632..00d9fcb70ffc 100644 --- a/apps/meteor/tests/end-to-end/api/05-chat.js +++ b/apps/meteor/tests/end-to-end/api/05-chat.js @@ -1347,10 +1347,11 @@ describe('[Chat]', function () { }); describe('[/chat.getMessageReadReceipts]', () => { + const isEnterprise = typeof process.env.IS_EE === 'string' ? process.env.IS_EE === 'true' : !!process.env.IS_EE; describe('when execute successfully', () => { it("should return the statusCode 200 and 'receipts' property and should be equal an array", (done) => { - if (!process.env.IS_EE) { - this.skip(); + if (!isEnterprise) { + done(); return; } @@ -1371,8 +1372,8 @@ describe('[Chat]', function () { it('should throw an error containing totp-required error when not running EE', function (done) { // TODO this is not the right way to do it. We're doing this way for now just because we have separate CI jobs for EE and CE, // ideally we should have a single CI job that adds a license and runs both CE and EE tests. - if (process.env.IS_EE) { - this.skip(); + if (isEnterprise) { + done(); return; } request @@ -1389,8 +1390,8 @@ describe('[Chat]', function () { }); it('should return statusCode 400 and an error', (done) => { - if (!process.env.IS_EE) { - this.skip(); + if (!isEnterprise) { + done(); return; } From 848bea817183ccf2c08c7d3d1cb0d3b0b3531a14 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Fri, 10 Feb 2023 23:55:56 -0300 Subject: [PATCH 22/29] Add ReadsService to core-services package --- .../message-read-receipt/server/hooks/afterReadMessages.ts | 2 +- apps/meteor/ee/app/message-read-receipt/server/index.js | 5 ----- apps/meteor/ee/app/message-read-receipt/server/index.ts | 5 +++++ packages/core-services/src/index.ts | 3 +++ 4 files changed, 9 insertions(+), 6 deletions(-) delete mode 100644 apps/meteor/ee/app/message-read-receipt/server/index.js create mode 100644 apps/meteor/ee/app/message-read-receipt/server/index.ts diff --git a/apps/meteor/ee/app/message-read-receipt/server/hooks/afterReadMessages.ts b/apps/meteor/ee/app/message-read-receipt/server/hooks/afterReadMessages.ts index cb1b32fba912..e8caab5ceb17 100644 --- a/apps/meteor/ee/app/message-read-receipt/server/hooks/afterReadMessages.ts +++ b/apps/meteor/ee/app/message-read-receipt/server/hooks/afterReadMessages.ts @@ -1,6 +1,6 @@ import type { IUser, IRoom, IMessage } from '@rocket.chat/core-typings'; +import { Reads } from '@rocket.chat/core-services'; -import { Reads } from '../../../../server/sdk'; import { ReadReceipt } from '../../../../server/lib/message-read-receipt/ReadReceipt'; import { callbacks } from '../../../../../lib/callbacks'; import { settings } from '../../../../../app/settings/server'; diff --git a/apps/meteor/ee/app/message-read-receipt/server/index.js b/apps/meteor/ee/app/message-read-receipt/server/index.js deleted file mode 100644 index 2b0ee13bbedc..000000000000 --- a/apps/meteor/ee/app/message-read-receipt/server/index.js +++ /dev/null @@ -1,5 +0,0 @@ -import { onLicense } from '../../license/server'; - -onLicense('message-read-receipt', () => { - require('./hooks'); -}); diff --git a/apps/meteor/ee/app/message-read-receipt/server/index.ts b/apps/meteor/ee/app/message-read-receipt/server/index.ts new file mode 100644 index 000000000000..89a890267248 --- /dev/null +++ b/apps/meteor/ee/app/message-read-receipt/server/index.ts @@ -0,0 +1,5 @@ +import { onLicense } from '../../license/server'; + +onLicense('message-read-receipt', async () => { + await import('./hooks'); +}); diff --git a/packages/core-services/src/index.ts b/packages/core-services/src/index.ts index 20b6761377b6..4ec16c9350ae 100644 --- a/packages/core-services/src/index.ts +++ b/packages/core-services/src/index.ts @@ -23,6 +23,7 @@ import type { ITeamAutocompleteResult, IListRoomsFilter, } from './types/ITeamService'; +import type { IReadsService } from './types/IReadsService'; import type { IRoomService, ICreateRoomParams, ISubscriptionExtraData } from './types/IRoomService'; import type { IMediaService, ResizeResult } from './types/IMediaService'; import type { IVoipService } from './types/IVoipService'; @@ -71,6 +72,7 @@ export { IOmnichannelVoipService, IPresence, IPushService, + IReadsService, IRoomService, ISAUMonitorService, ISubscriptionExtraData, @@ -110,6 +112,7 @@ export const Banner = proxifyWithWait('banner'); export const UiKitCoreApp = proxifyWithWait('uikit-core-app'); export const NPS = proxifyWithWait('nps'); export const Team = proxifyWithWait('team'); +export const Reads = proxifyWithWait('reads'); export const Room = proxifyWithWait('room'); export const Media = proxifyWithWait('media'); export const Voip = proxifyWithWait('voip'); From dca7967e78dbe5249b9a677343b8daffa76d4d3e Mon Sep 17 00:00:00 2001 From: LucianoPierdona Date: Sat, 11 Feb 2023 11:35:32 -0300 Subject: [PATCH 23/29] update test description --- apps/meteor/tests/end-to-end/api/05-chat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/tests/end-to-end/api/05-chat.js b/apps/meteor/tests/end-to-end/api/05-chat.js index 00d9fcb70ffc..c9c9f9bb2e48 100644 --- a/apps/meteor/tests/end-to-end/api/05-chat.js +++ b/apps/meteor/tests/end-to-end/api/05-chat.js @@ -1369,7 +1369,7 @@ describe('[Chat]', function () { }); describe('when an error occurs', () => { - it('should throw an error containing totp-required error when not running EE', function (done) { + it('should throw an error containing error-action-not-allowed error when not running EE', function (done) { // TODO this is not the right way to do it. We're doing this way for now just because we have separate CI jobs for EE and CE, // ideally we should have a single CI job that adds a license and runs both CE and EE tests. if (isEnterprise) { From 40ed6ba84f0f158318f637d43bae269537fd879b Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Mon, 13 Feb 2023 11:04:55 -0300 Subject: [PATCH 24/29] Skip tests when workspace is not EE --- apps/meteor/tests/end-to-end/api/05-chat.js | 25 ++++++++++++--------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/05-chat.js b/apps/meteor/tests/end-to-end/api/05-chat.js index c9c9f9bb2e48..2c4c7a98366d 100644 --- a/apps/meteor/tests/end-to-end/api/05-chat.js +++ b/apps/meteor/tests/end-to-end/api/05-chat.js @@ -1349,15 +1349,17 @@ describe('[Chat]', function () { describe('[/chat.getMessageReadReceipts]', () => { const isEnterprise = typeof process.env.IS_EE === 'string' ? process.env.IS_EE === 'true' : !!process.env.IS_EE; describe('when execute successfully', () => { - it("should return the statusCode 200 and 'receipts' property and should be equal an array", (done) => { + it('should return statusCode: 200 and an array of receipts when running EE', function (done) { if (!isEnterprise) { - done(); - return; + this.skip(); } request - .get(api(`chat.getMessageReadReceipts?messageId=${message._id}`)) + .get(api(`chat.getMessageReadReceipts`)) .set(credentials) + .query({ + messageId: message._id, + }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { @@ -1369,16 +1371,18 @@ describe('[Chat]', function () { }); describe('when an error occurs', () => { - it('should throw an error containing error-action-not-allowed error when not running EE', function (done) { + it('should throw error-action-not-allowed error when not running EE', function (done) { // TODO this is not the right way to do it. We're doing this way for now just because we have separate CI jobs for EE and CE, // ideally we should have a single CI job that adds a license and runs both CE and EE tests. if (isEnterprise) { - done(); - return; + this.skip(); } request - .get(api(`chat.getMessageReadReceipts?messageId=${message._id}`)) + .get(api(`chat.getMessageReadReceipts`)) .set(credentials) + .query({ + messageId: message._id, + }) .expect('Content-Type', 'application/json') .expect(400) .expect((res) => { @@ -1389,10 +1393,9 @@ describe('[Chat]', function () { .end(done); }); - it('should return statusCode 400 and an error', (done) => { + it('should return statusCode: 400 and an error when no messageId is provided', function (done) { if (!isEnterprise) { - done(); - return; + this.skip(); } request From bc007ef3327700434485c6ae1d6d001061702911 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Mon, 13 Feb 2023 16:59:15 -0300 Subject: [PATCH 25/29] Run beforeReadMessages callback after checks --- apps/meteor/app/threads/server/methods/getThreadMessages.js | 2 +- apps/meteor/server/methods/readThreads.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/meteor/app/threads/server/methods/getThreadMessages.js b/apps/meteor/app/threads/server/methods/getThreadMessages.js index 3104af01efbe..c92b631ad648 100644 --- a/apps/meteor/app/threads/server/methods/getThreadMessages.js +++ b/apps/meteor/app/threads/server/methods/getThreadMessages.js @@ -29,7 +29,6 @@ Meteor.methods({ const user = Meteor.user(); const room = Rooms.findOneById(thread.rid); - callbacks.run('beforeReadMessages', thread.rid, user._id); if (!canAccessRoom(room, user)) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'getThreadMessages' }); @@ -39,6 +38,7 @@ Meteor.methods({ return []; } + callbacks.run('beforeReadMessages', thread.rid, user._id); readThread({ userId: user._id, rid: thread.rid, tmid }); const result = Messages.findVisibleThreadByThreadId(tmid, { diff --git a/apps/meteor/server/methods/readThreads.js b/apps/meteor/server/methods/readThreads.js index 8f424a76085c..afab0fe355e2 100644 --- a/apps/meteor/server/methods/readThreads.js +++ b/apps/meteor/server/methods/readThreads.js @@ -23,7 +23,6 @@ Meteor.methods({ } const user = Meteor.user(); - callbacks.run('beforeReadMessages', thread.rid, user._id); const room = Rooms.findOneById(thread.rid); @@ -31,6 +30,7 @@ Meteor.methods({ throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'getThreadMessages' }); } + callbacks.run('beforeReadMessages', thread.rid, user._id); readThread({ userId: user._id, rid: thread.rid, tmid }); callbacks.runAsync('afterReadMessages', room._id, { uid: user._id, tmid }); }, From f39d800d70bd5f73423477611140499ad4117c42 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Mon, 13 Feb 2023 17:00:00 -0300 Subject: [PATCH 26/29] Remove try/catch from chat.getMessageReadReceipts endpoint --- apps/meteor/ee/server/api/chat.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/apps/meteor/ee/server/api/chat.ts b/apps/meteor/ee/server/api/chat.ts index ebc6e1b04cb2..238c0cb55d0b 100644 --- a/apps/meteor/ee/server/api/chat.ts +++ b/apps/meteor/ee/server/api/chat.ts @@ -19,15 +19,9 @@ API.v1.addRoute( }); } - try { - return API.v1.success({ - receipts: await Meteor.call('getReadReceipts', { messageId }), - }); - } catch (error: any) { - return API.v1.failure({ - error: error.message, - }); - } + return API.v1.success({ + receipts: await Meteor.call('getReadReceipts', { messageId }), + }); }, }, ); From e8b263745fa79e16eee6a54840fe9ce70c7abeb6 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Mon, 13 Feb 2023 17:29:37 -0300 Subject: [PATCH 27/29] Update service and collection names to MessageReads --- .../server/hooks/afterReadMessages.ts | 4 ++-- .../{reads => message-reads}/service.ts | 22 +++++++++---------- apps/meteor/ee/server/sdk/index.ts | 4 ++-- ...eadsService.ts => IMessageReadsService.ts} | 2 +- apps/meteor/ee/server/sdk/types/index.ts | 2 +- apps/meteor/ee/server/startup/services.ts | 4 ++-- apps/meteor/server/models/MessageReads.ts | 6 +++++ apps/meteor/server/models/Reads.ts | 6 ----- .../models/raw/{Reads.ts => MessageReads.ts} | 14 ++++++------ apps/meteor/server/models/startup.ts | 2 +- packages/core-services/src/index.ts | 6 ++--- ...eadsService.ts => IMessageReadsService.ts} | 2 +- .../src/{Reads.ts => MessageReads.ts} | 2 +- packages/core-typings/src/index.ts | 2 +- packages/model-typings/src/index.ts | 2 +- .../{IReadsModel.ts => IMessageReadsModel.ts} | 8 +++---- packages/models/src/index.ts | 4 ++-- 17 files changed, 46 insertions(+), 46 deletions(-) rename apps/meteor/ee/server/local-services/{reads => message-reads}/service.ts (64%) rename apps/meteor/ee/server/sdk/types/{IReadsService.ts => IMessageReadsService.ts} (61%) create mode 100644 apps/meteor/server/models/MessageReads.ts delete mode 100644 apps/meteor/server/models/Reads.ts rename apps/meteor/server/models/raw/{Reads.ts => MessageReads.ts} (71%) rename packages/core-services/src/types/{IReadsService.ts => IMessageReadsService.ts} (65%) rename packages/core-typings/src/{Reads.ts => MessageReads.ts} (85%) rename packages/model-typings/src/models/{IReadsModel.ts => IMessageReadsModel.ts} (61%) diff --git a/apps/meteor/ee/app/message-read-receipt/server/hooks/afterReadMessages.ts b/apps/meteor/ee/app/message-read-receipt/server/hooks/afterReadMessages.ts index e8caab5ceb17..bcbf0c751d38 100644 --- a/apps/meteor/ee/app/message-read-receipt/server/hooks/afterReadMessages.ts +++ b/apps/meteor/ee/app/message-read-receipt/server/hooks/afterReadMessages.ts @@ -1,5 +1,5 @@ import type { IUser, IRoom, IMessage } from '@rocket.chat/core-typings'; -import { Reads } from '@rocket.chat/core-services'; +import { MessageReads } from '@rocket.chat/core-services'; import { ReadReceipt } from '../../../../server/lib/message-read-receipt/ReadReceipt'; import { callbacks } from '../../../../../lib/callbacks'; @@ -14,7 +14,7 @@ callbacks.add( const { uid, lastSeen, tmid } = params; if (tmid) { - Reads.readThread(uid, tmid); + MessageReads.readThread(uid, tmid); } else if (lastSeen) { ReadReceipt.markMessagesAsRead(rid, uid, lastSeen); } diff --git a/apps/meteor/ee/server/local-services/reads/service.ts b/apps/meteor/ee/server/local-services/message-reads/service.ts similarity index 64% rename from apps/meteor/ee/server/local-services/reads/service.ts rename to apps/meteor/ee/server/local-services/message-reads/service.ts index ee85ccf1bf9c..8ed82c66af0c 100644 --- a/apps/meteor/ee/server/local-services/reads/service.ts +++ b/apps/meteor/ee/server/local-services/message-reads/service.ts @@ -1,24 +1,24 @@ -import { Reads, Subscriptions } from '@rocket.chat/models'; +import { MessageReads, Subscriptions } from '@rocket.chat/models'; import type { ISubscription } from '@rocket.chat/core-typings'; import { ServiceClassInternal } from '@rocket.chat/core-services'; -import type { IReadsService } from '../../sdk/types/IReadsService'; +import type { IMessageReadsService } from '../../sdk/types/IMessageReadsService'; import { Messages } from '../../../../app/models/server'; import { ReadReceipt } from '../../lib/message-read-receipt/ReadReceipt'; import { MAX_ROOM_SIZE_CHECK_INDIVIDUAL_READ_RECEIPTS } from '../../lib/constants'; -export class ReadsService extends ServiceClassInternal implements IReadsService { - protected name = 'reads'; +export class MessageReadsService extends ServiceClassInternal implements IMessageReadsService { + protected name = 'message-reads'; async readThread(userId: string, tmid: string): Promise { - const read = await Reads.findOneByUserIdAndThreadId(userId, tmid); + const read = await MessageReads.findOneByUserIdAndThreadId(userId, tmid); const threadMessage = Messages.findOneById(tmid, { projection: { ts: 1, tlm: 1, rid: 1 } }); if (!threadMessage || !threadMessage.tlm) { return; } - await Reads.updateReadTimestampByUserIdAndThreadId(userId, tmid); + await MessageReads.updateReadTimestampByUserIdAndThreadId(userId, tmid); ReadReceipt.storeThreadMessagesReadReceipts(tmid, userId, read?.ls || threadMessage.ts); // doesn't mark as read if not all room members have read the thread @@ -30,19 +30,19 @@ export class ReadsService extends ServiceClassInternal implements IReadsService }).toArray(); const members = subscriptions.map((s: ISubscription) => s.u._id); - const totalReads = await Reads.countByThreadAndUserIds(tmid, members); - if (totalReads < membersCount) { + const totalMessageReads = await MessageReads.countByThreadAndUserIds(tmid, members); + if (totalMessageReads < membersCount) { return; } } else { // for large rooms, mark as read if there are as many reads as room members to improve performance (instead of checking each read) - const totalReads = await Reads.countByThreadId(tmid); - if (totalReads < membersCount) { + const totalMessageReads = await MessageReads.countByThreadId(tmid); + if (totalMessageReads < membersCount) { return; } } - const firstRead = await Reads.getMinimumLastSeenByThreadId(tmid); + const firstRead = await MessageReads.getMinimumLastSeenByThreadId(tmid); if (firstRead?.ls) { Messages.setThreadMessagesAsRead(tmid, firstRead.ls); } diff --git a/apps/meteor/ee/server/sdk/index.ts b/apps/meteor/ee/server/sdk/index.ts index 074a2ca1efe2..ea6a92b2a9d8 100644 --- a/apps/meteor/ee/server/sdk/index.ts +++ b/apps/meteor/ee/server/sdk/index.ts @@ -2,8 +2,8 @@ import { proxifyWithWait } from '@rocket.chat/core-services'; import type { ILDAPEEService } from './types/ILDAPEEService'; import type { IInstanceService } from './types/IInstanceService'; -import type { IReadsService } from './types/IReadsService'; +import type { IMessageReadsService } from './types/IMessageReadsService'; export const LDAPEE = proxifyWithWait('ldap-enterprise'); export const Instance = proxifyWithWait('instance'); -export const Reads = proxifyWithWait('reads'); +export const MessageReads = proxifyWithWait('message-reads'); diff --git a/apps/meteor/ee/server/sdk/types/IReadsService.ts b/apps/meteor/ee/server/sdk/types/IMessageReadsService.ts similarity index 61% rename from apps/meteor/ee/server/sdk/types/IReadsService.ts rename to apps/meteor/ee/server/sdk/types/IMessageReadsService.ts index bc7408f49a29..ec7987bb8f6d 100644 --- a/apps/meteor/ee/server/sdk/types/IReadsService.ts +++ b/apps/meteor/ee/server/sdk/types/IMessageReadsService.ts @@ -1,3 +1,3 @@ -export interface IReadsService { +export interface IMessageReadsService { readThread(userId: string, threadId: string): Promise; } diff --git a/apps/meteor/ee/server/sdk/types/index.ts b/apps/meteor/ee/server/sdk/types/index.ts index 3a8399568f58..ad33c3085a59 100644 --- a/apps/meteor/ee/server/sdk/types/index.ts +++ b/apps/meteor/ee/server/sdk/types/index.ts @@ -1,2 +1,2 @@ export * from './ILDAPEEService'; -export * from './IReadsService'; +export * from './IMessageReadsService'; diff --git a/apps/meteor/ee/server/startup/services.ts b/apps/meteor/ee/server/startup/services.ts index 69fcec534cbb..7ee7a7abc2be 100644 --- a/apps/meteor/ee/server/startup/services.ts +++ b/apps/meteor/ee/server/startup/services.ts @@ -2,7 +2,7 @@ import { api } from '@rocket.chat/core-services'; import { EnterpriseSettings } from '../../app/settings/server/settings.internalService'; import { LDAPEEService } from '../local-services/ldap/service'; -import { ReadsService } from '../local-services/reads/service'; +import { MessageReadsService } from '../local-services/message-reads/service'; import { InstanceService } from '../local-services/instance/service'; import { LicenseService } from '../../app/license/server/license.internalService'; import { isRunningMs } from '../../../server/lib/isRunningMs'; @@ -11,7 +11,7 @@ import { isRunningMs } from '../../../server/lib/isRunningMs'; api.registerService(new EnterpriseSettings()); api.registerService(new LDAPEEService()); api.registerService(new LicenseService()); -api.registerService(new ReadsService()); +api.registerService(new MessageReadsService()); // when not running micro services we want to start up the instance intercom if (!isRunningMs()) { diff --git a/apps/meteor/server/models/MessageReads.ts b/apps/meteor/server/models/MessageReads.ts new file mode 100644 index 000000000000..43984e9e2b4a --- /dev/null +++ b/apps/meteor/server/models/MessageReads.ts @@ -0,0 +1,6 @@ +import { registerModel } from '@rocket.chat/models'; + +import { db } from '../database/utils'; +import { MessageReadsRaw } from './raw/MessageReads'; + +registerModel('IMessageReadsModel', new MessageReadsRaw(db)); diff --git a/apps/meteor/server/models/Reads.ts b/apps/meteor/server/models/Reads.ts deleted file mode 100644 index ded578974e02..000000000000 --- a/apps/meteor/server/models/Reads.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { registerModel } from '@rocket.chat/models'; - -import { db } from '../database/utils'; -import { ReadsRaw } from './raw/Reads'; - -registerModel('IReadsModel', new ReadsRaw(db)); diff --git a/apps/meteor/server/models/raw/Reads.ts b/apps/meteor/server/models/raw/MessageReads.ts similarity index 71% rename from apps/meteor/server/models/raw/Reads.ts rename to apps/meteor/server/models/raw/MessageReads.ts index 8942b9088069..bebe0abc2b70 100644 --- a/apps/meteor/server/models/raw/Reads.ts +++ b/apps/meteor/server/models/raw/MessageReads.ts @@ -1,23 +1,23 @@ -import type { Reads, IUser, IMessage, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; -import type { IReadsModel } from '@rocket.chat/model-typings'; +import type { MessageReads, IUser, IMessage, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; +import type { IMessageReadsModel } from '@rocket.chat/model-typings'; import type { Collection, Db, IndexDescription, UpdateResult } from 'mongodb'; import { BaseRaw } from './BaseRaw'; -export class ReadsRaw extends BaseRaw implements IReadsModel { - constructor(db: Db, trash?: Collection>) { - super(db, 'reads', trash); +export class MessageReadsRaw extends BaseRaw implements IMessageReadsModel { + constructor(db: Db, trash?: Collection>) { + super(db, 'message_reads', trash); } protected modelIndexes(): IndexDescription[] { return [{ key: { tmid: 1, userId: 1 }, unique: true }, { key: { ls: 1 } }]; } - async findOneByUserIdAndThreadId(userId: IUser['_id'], tmid: IMessage['_id']): Promise { + async findOneByUserIdAndThreadId(userId: IUser['_id'], tmid: IMessage['_id']): Promise { return this.findOne({ userId, tmid }); } - getMinimumLastSeenByThreadId(tmid: IMessage['_id']): Promise { + getMinimumLastSeenByThreadId(tmid: IMessage['_id']): Promise { return this.findOne( { tmid, diff --git a/apps/meteor/server/models/startup.ts b/apps/meteor/server/models/startup.ts index 32b1e5a71530..d72c43cfe4a5 100644 --- a/apps/meteor/server/models/startup.ts +++ b/apps/meteor/server/models/startup.ts @@ -37,7 +37,7 @@ import './OEmbedCache'; import './PbxEvents'; import './PushToken'; import './Permissions'; -import './Reads'; +import './MessageReads'; import './Reports'; import './Roles'; import './Rooms'; diff --git a/packages/core-services/src/index.ts b/packages/core-services/src/index.ts index 4ec16c9350ae..9e9aa6f199b7 100644 --- a/packages/core-services/src/index.ts +++ b/packages/core-services/src/index.ts @@ -23,7 +23,7 @@ import type { ITeamAutocompleteResult, IListRoomsFilter, } from './types/ITeamService'; -import type { IReadsService } from './types/IReadsService'; +import type { IMessageReadsService } from './types/IMessageReadsService'; import type { IRoomService, ICreateRoomParams, ISubscriptionExtraData } from './types/IRoomService'; import type { IMediaService, ResizeResult } from './types/IMediaService'; import type { IVoipService } from './types/IVoipService'; @@ -72,7 +72,7 @@ export { IOmnichannelVoipService, IPresence, IPushService, - IReadsService, + IMessageReadsService, IRoomService, ISAUMonitorService, ISubscriptionExtraData, @@ -112,7 +112,7 @@ export const Banner = proxifyWithWait('banner'); export const UiKitCoreApp = proxifyWithWait('uikit-core-app'); export const NPS = proxifyWithWait('nps'); export const Team = proxifyWithWait('team'); -export const Reads = proxifyWithWait('reads'); +export const MessageReads = proxifyWithWait('message-reads'); export const Room = proxifyWithWait('room'); export const Media = proxifyWithWait('media'); export const Voip = proxifyWithWait('voip'); diff --git a/packages/core-services/src/types/IReadsService.ts b/packages/core-services/src/types/IMessageReadsService.ts similarity index 65% rename from packages/core-services/src/types/IReadsService.ts rename to packages/core-services/src/types/IMessageReadsService.ts index 58755d443548..6bb892abd363 100644 --- a/packages/core-services/src/types/IReadsService.ts +++ b/packages/core-services/src/types/IMessageReadsService.ts @@ -1,5 +1,5 @@ import type { IServiceClass } from './ServiceClass'; -export interface IReadsService extends IServiceClass { +export interface IMessageReadsService extends IServiceClass { readThread(userId: string, threadId: string): Promise; } diff --git a/packages/core-typings/src/Reads.ts b/packages/core-typings/src/MessageReads.ts similarity index 85% rename from packages/core-typings/src/Reads.ts rename to packages/core-typings/src/MessageReads.ts index 6468500eaaa4..c95cec5c5d17 100644 --- a/packages/core-typings/src/Reads.ts +++ b/packages/core-typings/src/MessageReads.ts @@ -1,7 +1,7 @@ import type { IMessage } from './IMessage/IMessage'; import type { IUser } from './IUser'; -export type Reads = { +export type MessageReads = { _id: string; tmid: IMessage['_id']; ls: Date; diff --git a/packages/core-typings/src/index.ts b/packages/core-typings/src/index.ts index c3b5e9997e82..70ab98e742a0 100644 --- a/packages/core-typings/src/index.ts +++ b/packages/core-typings/src/index.ts @@ -59,7 +59,7 @@ export * from './ICustomUserStatus'; export * from './IEmailMessageHistory'; export * from './ReadReceipt'; -export * from './Reads'; +export * from './MessageReads'; export * from './IUpload'; export * from './IOEmbedCache'; export * from './IOembed'; diff --git a/packages/model-typings/src/index.ts b/packages/model-typings/src/index.ts index 6f19c60de874..a99cfd2c12ff 100644 --- a/packages/model-typings/src/index.ts +++ b/packages/model-typings/src/index.ts @@ -42,7 +42,7 @@ export * from './models/IPbxEventsModel'; export * from './models/IPushTokenModel'; export * from './models/IPermissionsModel'; export * from './models/IReadReceiptsModel'; -export * from './models/IReadsModel'; +export * from './models/IMessageReadsModel'; export * from './models/IReportsModel'; export * from './models/IRolesModel'; export * from './models/IRoomsModel'; diff --git a/packages/model-typings/src/models/IReadsModel.ts b/packages/model-typings/src/models/IMessageReadsModel.ts similarity index 61% rename from packages/model-typings/src/models/IReadsModel.ts rename to packages/model-typings/src/models/IMessageReadsModel.ts index 750dd17afaa1..fd696d8403fe 100644 --- a/packages/model-typings/src/models/IReadsModel.ts +++ b/packages/model-typings/src/models/IMessageReadsModel.ts @@ -1,11 +1,11 @@ import type { UpdateResult } from 'mongodb'; -import type { Reads, IUser, IMessage } from '@rocket.chat/core-typings'; +import type { MessageReads, IUser, IMessage } from '@rocket.chat/core-typings'; import type { IBaseModel } from './IBaseModel'; -export interface IReadsModel extends IBaseModel { - findOneByUserIdAndThreadId(userId: IUser['_id'], tmid: IMessage['_id']): Promise; - getMinimumLastSeenByThreadId(tmid: string): Promise; +export interface IMessageReadsModel extends IBaseModel { + findOneByUserIdAndThreadId(userId: IUser['_id'], tmid: IMessage['_id']): Promise; + getMinimumLastSeenByThreadId(tmid: string): Promise; updateReadTimestampByUserIdAndThreadId(userId: IUser['_id'], tmid: IMessage['_id']): Promise; countByThreadAndUserIds(tmid: IMessage['_id'], userIds: IUser['_id'][]): Promise; countByThreadId(tmid: IMessage['_id']): Promise; diff --git a/packages/models/src/index.ts b/packages/models/src/index.ts index 85699e7403eb..e2c296991239 100644 --- a/packages/models/src/index.ts +++ b/packages/models/src/index.ts @@ -42,7 +42,7 @@ import type { IPushTokenModel, IPermissionsModel, IReadReceiptsModel, - IReadsModel, + IMessageReadsModel, IReportsModel, IRolesModel, IRoomsModel, @@ -117,7 +117,7 @@ export const PbxEvents = proxify('IPbxEventsModel'); export const PushToken = proxify('IPushTokenModel'); export const Permissions = proxify('IPermissionsModel'); export const ReadReceipts = proxify('IReadReceiptsModel'); -export const Reads = proxify('IReadsModel'); +export const MessageReads = proxify('IMessageReadsModel'); export const Reports = proxify('IReportsModel'); export const Roles = proxify('IRolesModel'); export const Rooms = proxify('IRoomsModel'); From 0e19cc8e6268337266718d7edbafc6cf00793cfb Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Mon, 13 Feb 2023 17:50:07 -0300 Subject: [PATCH 28/29] Revert IRoom type change --- .../teams/contextualBar/channels/hooks/useTeamsChannelList.ts | 3 +-- .../app/message-read-receipt/server/hooks/afterSaveMessage.ts | 4 ++-- packages/core-typings/src/IRoom.ts | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/apps/meteor/client/views/teams/contextualBar/channels/hooks/useTeamsChannelList.ts b/apps/meteor/client/views/teams/contextualBar/channels/hooks/useTeamsChannelList.ts index ebab543954b6..c9967a0cfbb1 100644 --- a/apps/meteor/client/views/teams/contextualBar/channels/hooks/useTeamsChannelList.ts +++ b/apps/meteor/client/views/teams/contextualBar/channels/hooks/useTeamsChannelList.ts @@ -41,10 +41,9 @@ export const useTeamsChannelList = ( }); return { - items: rooms.map(({ _updatedAt, lastMessage, lm, ts, closedAt, webRtcCallStartTime, ...room }) => ({ + items: rooms.map(({ _updatedAt, lastMessage, lm, ts, webRtcCallStartTime, ...room }) => ({ ...(lm && { lm: new Date(lm) }), ...(ts && { ts: new Date(ts) }), - ...(closedAt && { closedAt: new Date(closedAt) }), _updatedAt: new Date(_updatedAt), ...(lastMessage && { lastMessage: mapMessageFromApi(lastMessage) }), ...(webRtcCallStartTime && { webRtcCallStartTime: new Date(webRtcCallStartTime) }), diff --git a/apps/meteor/ee/app/message-read-receipt/server/hooks/afterSaveMessage.ts b/apps/meteor/ee/app/message-read-receipt/server/hooks/afterSaveMessage.ts index 2ce472bac518..9534dccb7575 100644 --- a/apps/meteor/ee/app/message-read-receipt/server/hooks/afterSaveMessage.ts +++ b/apps/meteor/ee/app/message-read-receipt/server/hooks/afterSaveMessage.ts @@ -1,5 +1,5 @@ import { Subscriptions } from '@rocket.chat/models'; -import type { IRoom, IMessage } from '@rocket.chat/core-typings'; +import type { IRoom, IMessage, IOmnichannelRoom } from '@rocket.chat/core-typings'; import { isEditedMessage } from '@rocket.chat/core-typings'; import { ReadReceipt } from '../../../../server/lib/message-read-receipt/ReadReceipt'; @@ -13,7 +13,7 @@ callbacks.add( return message; } - if (room && !room.closedAt) { + if (room && !(room as IOmnichannelRoom).closedAt) { // set subscription as read right after message was sent Promise.await(Subscriptions.setAsReadByRoomIdAndUserId(room._id, message.u._id)); } diff --git a/packages/core-typings/src/IRoom.ts b/packages/core-typings/src/IRoom.ts index 20272ee98f33..3250254a0946 100644 --- a/packages/core-typings/src/IRoom.ts +++ b/packages/core-typings/src/IRoom.ts @@ -74,7 +74,6 @@ export interface IRoom extends IRocketChatRecord { usernames?: string[]; ts?: Date; - closedAt?: Date; cl?: boolean; ro?: boolean; favorite?: boolean; @@ -168,6 +167,7 @@ export interface IOmnichannelGenericRoom extends Omit Date: Mon, 13 Feb 2023 17:57:16 -0300 Subject: [PATCH 29/29] Update condition to check if room is from omnichannel --- .../message-read-receipt/server/hooks/afterSaveMessage.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/meteor/ee/app/message-read-receipt/server/hooks/afterSaveMessage.ts b/apps/meteor/ee/app/message-read-receipt/server/hooks/afterSaveMessage.ts index 9534dccb7575..278826dea4e7 100644 --- a/apps/meteor/ee/app/message-read-receipt/server/hooks/afterSaveMessage.ts +++ b/apps/meteor/ee/app/message-read-receipt/server/hooks/afterSaveMessage.ts @@ -1,6 +1,6 @@ import { Subscriptions } from '@rocket.chat/models'; -import type { IRoom, IMessage, IOmnichannelRoom } from '@rocket.chat/core-typings'; -import { isEditedMessage } from '@rocket.chat/core-typings'; +import type { IRoom, IMessage } from '@rocket.chat/core-typings'; +import { isEditedMessage, isOmnichannelRoom } from '@rocket.chat/core-typings'; import { ReadReceipt } from '../../../../server/lib/message-read-receipt/ReadReceipt'; import { callbacks } from '../../../../../lib/callbacks'; @@ -13,7 +13,7 @@ callbacks.add( return message; } - if (room && !(room as IOmnichannelRoom).closedAt) { + if (!isOmnichannelRoom(room) || !room.closedAt) { // set subscription as read right after message was sent Promise.await(Subscriptions.setAsReadByRoomIdAndUserId(room._id, message.u._id)); }