From e2ac5d7ac3a4b59729f704d59f05705bf392d93c Mon Sep 17 00:00:00 2001 From: Subhooo5 Date: Sun, 29 Mar 2026 13:55:23 +0530 Subject: [PATCH 1/2] feat: activity hub implementation and tests --- apps/meteor/app/api/server/index.ts | 1 + apps/meteor/app/api/server/lib/activityHub.ts | 142 +++++++++ apps/meteor/app/api/server/v1/activityHub.ts | 283 ++++++++++++++++++ .../lib/server/lib/notifyUsersOnMessage.ts | 2 + .../app/reactions/server/setReaction.ts | 2 + apps/meteor/server/models.ts | 2 + .../core-typings/src/INotificationHistory.ts | 15 + packages/core-typings/src/index.ts | 3 +- packages/model-typings/src/index.ts | 1 + .../src/models/IMessagesModel.ts | 2 + .../src/models/INotificationHistoryModel.ts | 15 + packages/models/src/index.ts | 2 + packages/models/src/modelClasses.ts | 1 + packages/models/src/models/Messages.ts | 10 + .../models/src/models/NotificationHistory.ts | 34 +++ rocketchat-compose | 1 + 16 files changed, 515 insertions(+), 1 deletion(-) create mode 100644 apps/meteor/app/api/server/lib/activityHub.ts create mode 100644 apps/meteor/app/api/server/v1/activityHub.ts create mode 100644 packages/core-typings/src/INotificationHistory.ts create mode 100644 packages/model-typings/src/models/INotificationHistoryModel.ts create mode 100644 packages/models/src/models/NotificationHistory.ts create mode 160000 rocketchat-compose diff --git a/apps/meteor/app/api/server/index.ts b/apps/meteor/app/api/server/index.ts index 176141af83e08..80798bcc0b385 100644 --- a/apps/meteor/app/api/server/index.ts +++ b/apps/meteor/app/api/server/index.ts @@ -44,6 +44,7 @@ import './v1/mailer'; import './v1/teams'; import './v1/moderation'; import './v1/uploads'; +import './v1/activityHub'; // This has to come last so all endpoints are registered before generating the OpenAPI documentation import './default/openApi'; diff --git a/apps/meteor/app/api/server/lib/activityHub.ts b/apps/meteor/app/api/server/lib/activityHub.ts new file mode 100644 index 0000000000000..6d2b5905eec66 --- /dev/null +++ b/apps/meteor/app/api/server/lib/activityHub.ts @@ -0,0 +1,142 @@ +import type { IMessage } from '@rocket.chat/core-typings'; +import { Messages, NotificationHistory, Rooms } from '@rocket.chat/models'; +import type { FindOptions } from 'mongodb'; + +type FindAllActivityHubUserParams = { + uid: string; + pagination: { + offset: number; + count: number; + sort?: Record; + }; +}; + +export async function findAllStarredMessagesByUser({ + uid, + pagination: { offset, count, sort }, +}: FindAllActivityHubUserParams): Promise<{ messages: IMessage[]; total: number }> { + const options: FindOptions = { + sort: sort ?? { ts: -1 }, + skip: offset, + limit: count, + }; + + const { cursor, totalCount } = Messages.findPaginatedStarredByUserId(uid, options); + + const [messages, total] = await Promise.all([cursor.toArray(), totalCount]); + + return { messages, total }; +} + +export async function findAllMentionsByUser({ + uid, + pagination: { offset, count }, +}: FindAllActivityHubUserParams): Promise<{ messages: IMessage[]; total: number }> { + const { cursor, totalCount } = NotificationHistory.findPaginatedByUserId(uid, { + limit: count, + skip: offset, + type: 'mention', + }); + + const [notifications, total] = await Promise.all([cursor.toArray(), totalCount]); + const msgIds = notifications.map((n) => n.msgId); + + if (msgIds.length === 0) { + return { messages: [], total }; + } + + const messages = await Messages.findVisibleByIds(msgIds).toArray(); + + return { messages, total }; +} + +export async function findAllReactionsForUser({ + uid, + pagination: { offset, count }, +}: FindAllActivityHubUserParams): Promise<{ messages: IMessage[]; total: number }> { + const { cursor, totalCount } = NotificationHistory.findPaginatedByUserId(uid, { + limit: count, + skip: offset, + type: 'reaction', + }); + + const [notifications, total] = await Promise.all([cursor.toArray(), totalCount]); + const msgIds = notifications.map((n) => n.msgId); + + if (msgIds.length === 0) { + return { messages: [], total }; + } + + const messages = await Messages.findVisibleByIds(msgIds).toArray(); + + return { messages, total }; +} + +export async function findAllActivitiesByUser({ + uid, + pagination: { offset, count }, +}: FindAllActivityHubUserParams): Promise<{ messages: IMessage[]; total: number }> { + const { cursor, totalCount } = NotificationHistory.findPaginatedByUserId(uid, { + limit: count, + skip: offset, + }); + + const [notifications, total] = await Promise.all([cursor.toArray(), totalCount]); + const msgIds = notifications.map((n) => n.msgId); + + if (msgIds.length === 0) { + return { messages: [], total }; + } + + // Remove duplicates (same message might have multiple activity types, e.g. mentioned and starred) + const uniqueMsgIds = [...new Set(msgIds)]; + const messages = await Messages.findVisibleByIds(uniqueMsgIds).toArray(); + + return { messages, total }; +} + +export async function createReactionNotification(userId: string, message: IMessage, reaction: string, shouldReact: boolean): Promise { + if (!shouldReact || message.u._id === userId) { + return; + } + + const room = await Rooms.findOneById(message.rid); + if (!room) { + return; + } + + await NotificationHistory.insertOne({ + userId: message.u._id, + roomId: message.rid, + msgId: message._id, + roomName: room.name || room.fname, + message: `${reaction}`, // Store the reaction as the message or separate field? For now, follow the pattern. + type: 'reaction', + ts: new Date(), + _updatedAt: new Date(), + } as any); +} + +export async function createMentionNotifications(message: IMessage, mentionIds: string[]): Promise { + if (!mentionIds.length) { + return; + } + + const room = await Rooms.findOneById(message.rid); + if (!room) { + return; + } + + const notifications = mentionIds.map((userId) => ({ + userId, + roomId: message.rid, + msgId: message._id, + roomName: room.name || room.fname, + message: message.msg, + type: 'mention', + ts: new Date(), + _updatedAt: new Date(), + })); + + await NotificationHistory.insertMany(notifications as any); +} \ No newline at end of file diff --git a/apps/meteor/app/api/server/v1/activityHub.ts b/apps/meteor/app/api/server/v1/activityHub.ts new file mode 100644 index 0000000000000..bf1be30dfe2e0 --- /dev/null +++ b/apps/meteor/app/api/server/v1/activityHub.ts @@ -0,0 +1,283 @@ +import type { INotificationHistory } from '@rocket.chat/core-typings'; +import { NotificationHistory } from '@rocket.chat/models'; +import { ajv, validateBadRequestErrorResponse, validateUnauthorizedErrorResponse } from '@rocket.chat/rest-typings'; + +import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; +import type { ExtractRoutesFromAPI } from '../ApiClass'; +import { API } from '../api'; +import { findAllActivitiesByUser, findAllMentionsByUser, findAllReactionsForUser, findAllStarredMessagesByUser } from '../lib/activityHub'; + +export const activityHubEndpoints = API.v1 + .get( + 'activity-hub.notifications', + { + authRequired: true, + query: ajv.compile<{ count?: number; offset?: number; type?: INotificationHistory['type'] }>({ + type: 'object', + properties: { + count: { type: 'number', nullable: true }, + offset: { type: 'number', nullable: true }, + type: { + type: 'string', + enum: ['general', 'direct', 'mention', 'reaction', 'star', 'quote', 'thread'], + nullable: true, + }, + }, + additionalProperties: false, + }), + response: { + 200: ajv.compile<{ + notifications: INotificationHistory[]; + total: number; + count: number; + offset: number; + }>({ + type: 'object', + properties: { + notifications: { type: 'array', items: { type: 'object' } }, + total: { type: 'number' }, + count: { type: 'number' }, + offset: { type: 'number' }, + success: { type: 'boolean', enum: [true] }, + }, + required: ['notifications', 'total', 'count', 'offset', 'success'], + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, + }, + async function action() { + const { count = 50, offset = 0, type } = this.queryParams; + const { cursor, totalCount } = NotificationHistory.findPaginatedByUserId(this.userId, { + limit: count, + skip: offset, + type, + }); + + const [notifications, total] = await Promise.all([cursor.toArray(), totalCount]); + + return API.v1.success({ notifications, total, count: notifications.length, offset }); + }, + ) + .post( + 'activity-hub.notifications.delete', + { + authRequired: true, + body: ajv.compile<{ notificationId: string }>({ + type: 'object', + properties: { + notificationId: { type: 'string', minLength: 1 }, + }, + required: ['notificationId'], + additionalProperties: false, + }), + response: { + 200: ajv.compile({ + type: 'object', + properties: { success: { type: 'boolean', enum: [true] } }, + required: ['success'], + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, + }, + async function action() { + const { notificationId } = this.bodyParams; + await NotificationHistory.deleteOneByIdAndUserId(notificationId, this.userId); + return API.v1.success(); + }, + ) + .post( + 'activity-hub.notifications.clearAll', + { + authRequired: true, + body: ajv.compile>({ + type: 'object', + additionalProperties: false, + }), + response: { + 200: ajv.compile({ + type: 'object', + properties: { success: { type: 'boolean', enum: [true] } }, + required: ['success'], + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, + }, + async function action() { + await NotificationHistory.deleteAllByUserId(this.userId); + return API.v1.success(); + }, + ) + .get( + 'activity-hub.starredMessages', + { + authRequired: true, + query: ajv.compile<{ count?: number; offset?: number }>({ + type: 'object', + properties: { + count: { type: 'number', nullable: true }, + offset: { type: 'number', nullable: true }, + }, + additionalProperties: false, + }), + response: { + 200: ajv.compile({ + type: 'object', + properties: { + messages: { type: 'array', items: { type: 'object' } }, + total: { type: 'number' }, + count: { type: 'number' }, + offset: { type: 'number' }, + success: { type: 'boolean', enum: [true] }, + }, + required: ['messages', 'total', 'count', 'offset', 'success'], + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, + }, + async function action() { + const { count = 50, offset = 0 } = this.queryParams; + + const result = await findAllStarredMessagesByUser({ + uid: this.userId, + pagination: { offset, count }, + }); + + result.messages = await normalizeMessagesForUser(result.messages, this.userId); + + return API.v1.success(result); + }, + ) + .get( + 'activity-hub.mentions', + { + authRequired: true, + query: ajv.compile<{ count?: number; offset?: number }>({ + type: 'object', + properties: { + count: { type: 'number', nullable: true }, + offset: { type: 'number', nullable: true }, + }, + additionalProperties: false, + }), + response: { + 200: ajv.compile({ + type: 'object', + properties: { + messages: { type: 'array', items: { type: 'object' } }, + total: { type: 'number' }, + count: { type: 'number' }, + offset: { type: 'number' }, + success: { type: 'boolean', enum: [true] }, + }, + required: ['messages', 'total', 'count', 'offset', 'success'], + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, + }, + async function action() { + const { count = 50, offset = 0 } = this.queryParams; + + const result = await findAllMentionsByUser({ + uid: this.userId, + pagination: { offset, count }, + }); + + result.messages = await normalizeMessagesForUser(result.messages, this.userId); + + return API.v1.success(result); + }, + ) + .get( + 'activity-hub.reactions', + { + authRequired: true, + query: ajv.compile<{ count?: number; offset?: number }>({ + type: 'object', + properties: { + count: { type: 'number', nullable: true }, + offset: { type: 'number', nullable: true }, + }, + additionalProperties: false, + }), + response: { + 200: ajv.compile({ + type: 'object', + properties: { + messages: { type: 'array', items: { type: 'object' } }, + total: { type: 'number' }, + count: { type: 'number' }, + offset: { type: 'number' }, + success: { type: 'boolean', enum: [true] }, + }, + required: ['messages', 'total', 'count', 'offset', 'success'], + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, + }, + async function action() { + const { count = 50, offset = 0 } = this.queryParams; + + const result = await findAllReactionsForUser({ + uid: this.userId, + pagination: { offset, count }, + }); + + result.messages = await normalizeMessagesForUser(result.messages, this.userId); + + return API.v1.success(result); + }, + ) + .get( + 'activity-hub.all', + { + authRequired: true, + query: ajv.compile<{ count?: number; offset?: number }>({ + type: 'object', + properties: { + count: { type: 'number', nullable: true }, + offset: { type: 'number', nullable: true }, + }, + additionalProperties: false, + }), + response: { + 200: ajv.compile({ + type: 'object', + properties: { + messages: { type: 'array', items: { type: 'object' } }, + total: { type: 'number' }, + count: { type: 'number' }, + offset: { type: 'number' }, + success: { type: 'boolean', enum: [true] }, + }, + required: ['messages', 'total', 'count', 'offset', 'success'], + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, + }, + async function action() { + const { count = 50, offset = 0 } = this.queryParams; + + const result = await findAllActivitiesByUser({ + uid: this.userId, + pagination: { offset, count }, + }); + + result.messages = await normalizeMessagesForUser(result.messages, this.userId); + + return API.v1.success(result); + }, + ); + +type ActivityHubEndpoints = ExtractRoutesFromAPI; + +declare module '@rocket.chat/rest-typings' { + // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface + interface Endpoints extends ActivityHubEndpoints {} +} \ No newline at end of file diff --git a/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts b/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts index 80cf172c9dcb1..756bf38f7f309 100644 --- a/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts +++ b/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts @@ -10,6 +10,7 @@ import { notifyOnSubscriptionChangedByRoomIdAndUserIds, } from './notifyListener'; import { callbacks } from '../../../../server/lib/callbacks'; +import { createMentionNotifications } from '../../../api/server/lib/activityHub'; import { settings } from '../../../settings/server'; import { messageContainsHighlight } from '../functions/notifications/messageContainsHighlight'; @@ -110,6 +111,7 @@ async function updateUsersSubscriptions(message: IMessage, room: IRoom): Promise // Give priority to user mentions over group mentions if (userIds.length) { + void createMentionNotifications(message, userIds); await Subscriptions.incUserMentionsAndUnreadForRoomIdAndUserIds(room._id, userIds, 1, userMentionInc); } else if (toAll || toHere) { await Subscriptions.incGroupMentionsAndUnreadForRoomIdExcludingUserId(room._id, message.u._id, 1, groupMentionInc); diff --git a/apps/meteor/app/reactions/server/setReaction.ts b/apps/meteor/app/reactions/server/setReaction.ts index 483b1fff8ed7a..e7a9c79916b9e 100644 --- a/apps/meteor/app/reactions/server/setReaction.ts +++ b/apps/meteor/app/reactions/server/setReaction.ts @@ -6,6 +6,7 @@ import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../server/lib/callbacks'; import { i18n } from '../../../server/lib/i18n'; +import { createReactionNotification } from '../../api/server/lib/activityHub'; import { canAccessRoomAsync } from '../../authorization/server'; import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission'; import { emoji } from '../../emoji/server'; @@ -81,6 +82,7 @@ export async function setReaction(room: IRoom, user: IUser, message: IMessage, r await Rooms.setReactionsInLastMessage(room._id, message.reactions); } + void createReactionNotification(user._id, message, reaction, true); void callbacks.run('afterSetReaction', message, { user, reaction, shouldReact: true, room }); isReacted = true; diff --git a/apps/meteor/server/models.ts b/apps/meteor/server/models.ts index 49b5b98b19f08..87fc98c00d439 100644 --- a/apps/meteor/server/models.ts +++ b/apps/meteor/server/models.ts @@ -74,6 +74,7 @@ import { VideoConferenceRaw, WebdavAccountsRaw, WorkspaceCredentialsRaw, + NotificationHistoryRaw, AbacAttributesRaw, } from '@rocket.chat/models'; import type { Collection } from 'mongodb'; @@ -131,6 +132,7 @@ registerModel('IMessageReadsModel', new MessageReadsRaw(db)); registerModel('IMessagesModel', new MessagesRaw(db, trashCollection)); registerModel('IMigrationsModel', new MigrationsRaw(db)); registerModel('IModerationReportsModel', new ModerationReportsRaw(db)); +registerModel('INotificationHistoryModel', new NotificationHistoryRaw(db)); registerModel('INotificationQueueModel', new NotificationQueueRaw(db)); registerModel('INpsModel', new NpsRaw(db)); registerModel('INpsVoteModel', new NpsVoteRaw(db)); diff --git a/packages/core-typings/src/INotificationHistory.ts b/packages/core-typings/src/INotificationHistory.ts new file mode 100644 index 0000000000000..9aabb0c952fe1 --- /dev/null +++ b/packages/core-typings/src/INotificationHistory.ts @@ -0,0 +1,15 @@ +import type { IMessage } from './IMessage'; +import type { IRoom } from './IRoom'; +import type { IUser } from './IUser'; + +export interface INotificationHistory { + _id: string; + userId: IUser['_id']; + roomId: IRoom['_id']; + msgId: IMessage['_id']; + roomName?: string; + message: string; + type: 'general' | 'direct' | 'mention' | 'reaction' | 'star' | 'quote' | 'thread'; + ts: Date; + _updatedAt: Date; +} \ No newline at end of file diff --git a/packages/core-typings/src/index.ts b/packages/core-typings/src/index.ts index 011df49c0f28c..1917e86dc9210 100644 --- a/packages/core-typings/src/index.ts +++ b/packages/core-typings/src/index.ts @@ -131,5 +131,6 @@ export type * from './IAbacAttribute'; export * from './Abac'; export type * from './ServerAudit/IAuditServerAbacAction'; export type * from './ServerAudit/IAuditUserChangedEvent'; +export type * from './INotificationHistory'; -export { schemas } from './Ajv'; +export { schemas } from './Ajv'; \ No newline at end of file diff --git a/packages/model-typings/src/index.ts b/packages/model-typings/src/index.ts index 439447c49ea02..8f6d6b5ad5a20 100644 --- a/packages/model-typings/src/index.ts +++ b/packages/model-typings/src/index.ts @@ -34,6 +34,7 @@ export type * from './models/ILivechatUnitMonitorsModel'; export type * from './models/ILivechatVisitorsModel'; export type * from './models/ILoginServiceConfigurationModel'; export type * from './models/IMessagesModel'; +export type * from './models/INotificationHistoryModel'; export type * from './models/INotificationQueueModel'; export type * from './models/INpsModel'; export type * from './models/INpsVoteModel'; diff --git a/packages/model-typings/src/models/IMessagesModel.ts b/packages/model-typings/src/models/IMessagesModel.ts index 707f49b1937b6..d360c9783787f 100644 --- a/packages/model-typings/src/models/IMessagesModel.ts +++ b/packages/model-typings/src/models/IMessagesModel.ts @@ -41,6 +41,8 @@ export interface IMessagesModel extends IBaseModel { findStarredByUserAtRoom(userId: IUser['_id'], roomId: IRoom['_id'], options?: FindOptions): FindPaginated>; + findPaginatedStarredByUserId(userId: IUser['_id'], options?: FindOptions): FindPaginated>; + findPaginatedByRoomIdAndType( roomId: IRoom['_id'], type: IMessage['t'], diff --git a/packages/model-typings/src/models/INotificationHistoryModel.ts b/packages/model-typings/src/models/INotificationHistoryModel.ts new file mode 100644 index 0000000000000..4af79f3c5054f --- /dev/null +++ b/packages/model-typings/src/models/INotificationHistoryModel.ts @@ -0,0 +1,15 @@ +import type { INotificationHistory } from '@rocket.chat/core-typings'; +import type { DeleteResult, FindCursor, WithId } from 'mongodb'; + +import type { IBaseModel } from './IBaseModel'; + +export interface INotificationHistoryModel extends IBaseModel { + findPaginatedByUserId( + userId: string, + options: { limit: number; skip: number; type?: INotificationHistory['type'] }, + ): { cursor: FindCursor>; totalCount: Promise }; + + deleteOneByIdAndUserId(_id: string, userId: string): Promise; + + deleteAllByUserId(userId: string): Promise; +} \ No newline at end of file diff --git a/packages/models/src/index.ts b/packages/models/src/index.ts index 4805f237ef585..b1b2b07121b44 100644 --- a/packages/models/src/index.ts +++ b/packages/models/src/index.ts @@ -78,6 +78,7 @@ import type { IMediaCallsModel, IMediaCallChannelsModel, IMediaCallNegotiationsModel, + INotificationHistoryModel, ICallHistoryModel, IAbacAttributesModel, } from '@rocket.chat/model-typings'; @@ -166,6 +167,7 @@ export const Messages = proxify('IMessagesModel'); export const MediaCalls = proxify('IMediaCallsModel'); export const MediaCallChannels = proxify('IMediaCallChannelsModel'); export const MediaCallNegotiations = proxify('IMediaCallNegotiationsModel'); +export const NotificationHistory = proxify('INotificationHistoryModel'); export const NotificationQueue = proxify('INotificationQueueModel'); export const Nps = proxify('INpsModel'); export const NpsVote = proxify('INpsVoteModel'); diff --git a/packages/models/src/modelClasses.ts b/packages/models/src/modelClasses.ts index c554fc9698550..4ccf68454e6f5 100644 --- a/packages/models/src/modelClasses.ts +++ b/packages/models/src/modelClasses.ts @@ -73,3 +73,4 @@ export * from './models/MediaCallNegotiations'; export * from './models/WorkspaceCredentials'; export * from './models/Trash'; export * from './models/CallHistory'; +export * from './models/NotificationHistory'; \ No newline at end of file diff --git a/packages/models/src/models/Messages.ts b/packages/models/src/models/Messages.ts index e16cb35fd41ee..edbd09f9de579 100644 --- a/packages/models/src/models/Messages.ts +++ b/packages/models/src/models/Messages.ts @@ -119,6 +119,16 @@ export class MessagesRaw extends BaseRaw implements IMessagesModel { return this.findPaginated(query, options); } + findPaginatedStarredByUserId(userId: IUser['_id'], options?: FindOptions): FindPaginated> { + const query: Filter = { + '_hidden': { $ne: true }, + 'starred._id': userId, + // no 'rid' filter — all rooms + }; + + return this.findPaginated(query, options); + } + findPaginatedByRoomIdAndType( roomId: IRoom['_id'], type: IMessage['t'], diff --git a/packages/models/src/models/NotificationHistory.ts b/packages/models/src/models/NotificationHistory.ts new file mode 100644 index 0000000000000..5b08de54cb925 --- /dev/null +++ b/packages/models/src/models/NotificationHistory.ts @@ -0,0 +1,34 @@ +import type { INotificationHistory, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; +import type { INotificationHistoryModel } from '@rocket.chat/model-typings'; +import type { Collection, Db, DeleteResult, FindCursor, IndexDescription, WithId } from 'mongodb'; + +import { BaseRaw } from './BaseRaw'; + +export class NotificationHistoryRaw extends BaseRaw implements INotificationHistoryModel { + constructor(db: Db, trash?: Collection>) { + super(db, 'notification_history', trash); + } + + protected override modelIndexes(): IndexDescription[] { + return [{ key: { userId: 1 } }, { key: { ts: -1 } }]; + } + + findPaginatedByUserId( + userId: string, + options: { limit: number; skip: number; type?: INotificationHistory['type'] }, + ): { cursor: FindCursor>; totalCount: Promise } { + const query: any = { userId }; + if (options.type) { + query.type = options.type; + } + return this.findPaginated(query, { sort: { ts: -1 }, limit: options.limit, skip: options.skip }); + } + + deleteOneByIdAndUserId(_id: string, userId: string): Promise { + return this.col.deleteOne({ _id, userId }); + } + + deleteAllByUserId(userId: string): Promise { + return this.col.deleteMany({ userId }); + } +} \ No newline at end of file diff --git a/rocketchat-compose b/rocketchat-compose new file mode 160000 index 0000000000000..1f6688de87081 --- /dev/null +++ b/rocketchat-compose @@ -0,0 +1 @@ +Subproject commit 1f6688de8708159d21946dbfce90f382c34b800f From 6aeed1f6453c4487d2d44cc6149710de93710210 Mon Sep 17 00:00:00 2001 From: Subhooo5 Date: Sun, 29 Mar 2026 13:57:11 +0530 Subject: [PATCH 2/2] fix: remove embedded repo (rocketchat-compose) --- .gitignore | 1 + rocketchat-compose | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 160000 rocketchat-compose diff --git a/.gitignore b/.gitignore index 8ee032f2d8c32..9cc405e5cde5e 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,4 @@ storybook-static development/tempo-data/ .env +rocketchat-compose/ diff --git a/rocketchat-compose b/rocketchat-compose deleted file mode 160000 index 1f6688de87081..0000000000000 --- a/rocketchat-compose +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 1f6688de8708159d21946dbfce90f382c34b800f