From 6a73d5a9340ff6cf301068f1e81d5923f92545d4 Mon Sep 17 00:00:00 2001 From: qmaster0803 Date: Wed, 25 Feb 2026 19:56:37 +0300 Subject: [PATCH 1/3] feat(backend): add subscription for read count --- .../src/models/ISubscriptionsModel.ts | 2 ++ packages/models/src/models/Subscriptions.ts | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/packages/model-typings/src/models/ISubscriptionsModel.ts b/packages/model-typings/src/models/ISubscriptionsModel.ts index 121da7e8ad1c3..efd12fff81a20 100644 --- a/packages/model-typings/src/models/ISubscriptionsModel.ts +++ b/packages/model-typings/src/models/ISubscriptionsModel.ts @@ -22,6 +22,8 @@ import type { DocumentWithProjection } from '../types/DocumentWithProjection'; export interface ISubscriptionsModel extends IBaseModel { getBadgeCount(uid: string): Promise; + countReadersByRoomIdAndMessageTs(rid: string, messageTs: Date, excludeUserId?: string): Promise; + findOneByRoomIdAndUserId(rid: string, uid: string, options?: FindOptions): Promise; findByUserIdAndRoomIds(userId: string, roomIds: Array, options?: FindOptions): FindCursor; diff --git a/packages/models/src/models/Subscriptions.ts b/packages/models/src/models/Subscriptions.ts index dbd72d644cf11..8cc5161e15ac8 100644 --- a/packages/models/src/models/Subscriptions.ts +++ b/packages/models/src/models/Subscriptions.ts @@ -86,6 +86,20 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return result?.total || 0; } + countReadersByRoomIdAndMessageTs(rid: string, messageTs: Date, excludeUserId?: string): Promise { + const query: Filter = { + rid, + archived: { $ne: true }, + ls: { $gte: messageTs }, + 'u._id': { + ...(excludeUserId ? { $ne: excludeUserId } : {}), + $exists: true, + }, + }; + + return this.countDocuments(query); + } + findOneByRoomIdAndUserId(rid: string, uid: string, options: FindOptions = {}): Promise { const query = { rid, From 5515b9afa3bab2b2fd26a45396d8cd50a3adc9d0 Mon Sep 17 00:00:00 2001 From: qmaster0803 Date: Wed, 25 Feb 2026 19:57:06 +0300 Subject: [PATCH 2/3] feat(backend): add backend method for read count --- .../server/getMessageReadCount.ts | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 apps/meteor/app/read-counter/server/getMessageReadCount.ts diff --git a/apps/meteor/app/read-counter/server/getMessageReadCount.ts b/apps/meteor/app/read-counter/server/getMessageReadCount.ts new file mode 100644 index 0000000000000..0515d134d12a2 --- /dev/null +++ b/apps/meteor/app/read-counter/server/getMessageReadCount.ts @@ -0,0 +1,45 @@ +import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; +import { Authorization } from '@rocket.chat/core-services'; +import { Messages, Rooms, Subscriptions } from '@rocket.chat/models'; + +export const getMessageReadCount = async ( + messageId: IMessage['_id'], + userId: IUser['_id'], +): Promise<{ readCount: number } | null> => { + const message = await Messages.findOneById(messageId, { + projection: { _id: 1, rid: 1, ts: 1, 'u._id': 1 }, + }); + + if (!message) { + return null; + } + + const room = await Rooms.findOneById(message.rid as IRoom['_id'], { + projection: { _id: 1, t: 1 }, + }); + + if (!room) { + return null; + } + + // Exclude DMs from read counter + if (room.t === 'd') { + return null; + } + + // Ensure the requesting user can access the room + if (!(await Authorization.canReadRoom(room, { _id: userId }))) { + return null; + } + + if (!message.ts) { + return { readCount: 0 }; + } + + const excludeUserId = message.u?._id; + + const readCount = await Subscriptions.countReadersByRoomIdAndMessageTs(message.rid, message.ts, excludeUserId); + + return { readCount }; +}; + From 58aff137a9615664cfbb9a954c45e55c4e77a70d Mon Sep 17 00:00:00 2001 From: qmaster0803 Date: Wed, 25 Feb 2026 20:16:40 +0300 Subject: [PATCH 3/3] feat(backend): add logging for demo purposes --- .../lib/server/functions/loadMessageHistory.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/apps/meteor/app/lib/server/functions/loadMessageHistory.ts b/apps/meteor/app/lib/server/functions/loadMessageHistory.ts index 5addbd896889b..14f0bef872e64 100644 --- a/apps/meteor/app/lib/server/functions/loadMessageHistory.ts +++ b/apps/meteor/app/lib/server/functions/loadMessageHistory.ts @@ -5,6 +5,8 @@ import type { FindOptions } from 'mongodb'; import { settings } from '../../../settings/server/cached'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; import { getHiddenSystemMessages } from '../lib/getHiddenSystemMessages'; +import { getMessageReadCount } from '../../../read-counter/server/getMessageReadCount'; +import { SystemLogger } from '../../../../server/lib/logger/system'; export async function loadMessageHistory({ userId, @@ -52,6 +54,20 @@ export async function loadMessageHistory({ ).toArray() : await Messages.findVisibleByRoomIdNotContainingTypes(rid, hiddenMessageTypes, options, showThreadMessages).toArray(); const messages = await normalizeMessagesForUser(records, userId); + + if (messages[0]?._id && userId) { + const readCountResult = await getMessageReadCount(messages[0]._id, userId); + + if (readCountResult) { + SystemLogger.info({ + msg: '[read-count demo] newest message readCount', + rid, + mid: messages[0]._id, + readCount: readCountResult.readCount, + uid: userId, + }); + } + } let unreadNotLoaded = 0; let firstUnread;