diff --git a/apps/meteor/app/lib/client/methods/sendMessage.ts b/apps/meteor/app/lib/client/methods/sendMessage.ts index f87684eb978e0..486a0528a5095 100644 --- a/apps/meteor/app/lib/client/methods/sendMessage.ts +++ b/apps/meteor/app/lib/client/methods/sendMessage.ts @@ -16,7 +16,7 @@ Meteor.methods({ if (!uid || trim(message.msg) === '') { return false; } - const messageAlreadyExists = message._id && Messages.findOne({ _id: message._id }); + const messageAlreadyExists = message._id && Messages.state.get(message._id); if (messageAlreadyExists) { return dispatchToastMessage({ type: 'error', message: t('Message_Already_Sent') }); } @@ -42,7 +42,7 @@ Meteor.methods({ } await onClientMessageReceived(message as IMessage).then((message) => { - Messages.insert(message); + Messages.state.store(message); return callbacks.run('afterSaveMessage', message, { room }); }); }, diff --git a/apps/meteor/app/models/client/index.ts b/apps/meteor/app/models/client/index.ts index d985275faaa8f..8b4b923ceaf86 100644 --- a/apps/meteor/app/models/client/index.ts +++ b/apps/meteor/app/models/client/index.ts @@ -19,6 +19,6 @@ export { Rooms, /** @deprecated new code refer to Minimongo collections like this one; prefer fetching data from the REST API, listening to changes via streamer events, and storing the state in a Tanstack Query */ Subscriptions, - /** @deprecated new code refer to Minimongo collections like this one; prefer fetching data from the REST API, listening to changes via streamer events, and storing the state in a Tanstack Query */ + /** @deprecated prefer fetching data from the REST API, listening to changes via streamer events, and storing the state in a Tanstack Query */ Messages, }; diff --git a/apps/meteor/app/models/client/models/Messages.ts b/apps/meteor/app/models/client/models/Messages.ts index ab3048eda645e..2ada45bdd105d 100644 --- a/apps/meteor/app/models/client/models/Messages.ts +++ b/apps/meteor/app/models/client/models/Messages.ts @@ -1,8 +1,11 @@ import type { IMessage } from '@rocket.chat/core-typings'; -import { MinimongoCollection } from '../../../../client/lib/cachedCollections/MinimongoCollection'; +import { createDocumentMapStore } from '../../../../client/lib/cachedCollections/DocumentMapStore'; -/** @deprecated new code refer to Minimongo collections like this one; prefer fetching data from the REST API, listening to changes via streamer events, and storing the state in a Tanstack Query */ -export const Messages = new MinimongoCollection< - IMessage & { ignored?: boolean; autoTranslateFetching?: boolean; autoTranslateShowInverse?: boolean } ->(); +/** @deprecated prefer fetching data from the REST API, listening to changes via streamer events, and storing the state in a Tanstack Query */ +export const Messages = { + use: createDocumentMapStore(), + get state() { + return Messages.use.getState(); + }, +}; diff --git a/apps/meteor/app/otr/client/OTRRoom.ts b/apps/meteor/app/otr/client/OTRRoom.ts index 83485a7442cfc..34320d84f4c06 100644 --- a/apps/meteor/app/otr/client/OTRRoom.ts +++ b/apps/meteor/app/otr/client/OTRRoom.ts @@ -180,7 +180,9 @@ export class OTRRoom implements IOTRRoom { } deleteOTRMessages(): void { - Messages.remove({ t: { $in: ['otr', 'otr-ack', ...Object.values(otrSystemMessages)] }, rid: this._roomId }); + Messages.state.remove( + (record) => record.rid === this._roomId && !!record.t && ['otr', 'otr-ack', ...Object.values(otrSystemMessages)].includes(record.t), + ); } end(): void { diff --git a/apps/meteor/app/reactions/client/methods/setReaction.ts b/apps/meteor/app/reactions/client/methods/setReaction.ts index 2e3a03b248c20..10dfce236765f 100644 --- a/apps/meteor/app/reactions/client/methods/setReaction.ts +++ b/apps/meteor/app/reactions/client/methods/setReaction.ts @@ -18,7 +18,7 @@ Meteor.methods({ return false; } - const message: IMessage | undefined = Messages.findOne({ _id: messageId }); + const message: IMessage | undefined = Messages.state.get(messageId); if (!message) { return false; } @@ -53,9 +53,15 @@ Meteor.methods({ if (!message.reactions || typeof message.reactions !== 'object' || Object.keys(message.reactions).length === 0) { delete message.reactions; - Messages.update({ _id: messageId }, { $unset: { reactions: 1 } }); + Messages.state.update( + (record) => record._id === messageId, + ({ reactions: _, ...record }) => record, + ); } else { - Messages.update({ _id: messageId }, { $set: { reactions: message.reactions } }); + Messages.state.update( + (record) => record._id === messageId, + (record) => ({ ...record, reactions: message.reactions }), + ); } } else { if (!message.reactions) { @@ -68,7 +74,10 @@ Meteor.methods({ } message.reactions[reaction].usernames.push(user.username); - Messages.update({ _id: messageId }, { $set: { reactions: message.reactions } }); + Messages.state.update( + (record) => record._id === messageId, + (record) => ({ ...record, reactions: message.reactions }), + ); } }, }); diff --git a/apps/meteor/app/ui-message/client/findParentMessage.ts b/apps/meteor/app/ui-message/client/findParentMessage.ts index ca221d79812b0..bfd9bc961d474 100644 --- a/apps/meteor/app/ui-message/client/findParentMessage.ts +++ b/apps/meteor/app/ui-message/client/findParentMessage.ts @@ -27,7 +27,7 @@ export const findParentMessage = (() => { }; return async (tmid: IMessage['_id']) => { - const message = Messages.findOne({ _id: tmid }); + const message = Messages.state.get(tmid); if (message) { return message; diff --git a/apps/meteor/app/ui-utils/client/lib/LegacyRoomManager.ts b/apps/meteor/app/ui-utils/client/lib/LegacyRoomManager.ts index c26df03f0f03e..b53715599480f 100644 --- a/apps/meteor/app/ui-utils/client/lib/LegacyRoomManager.ts +++ b/apps/meteor/app/ui-utils/client/lib/LegacyRoomManager.ts @@ -1,7 +1,8 @@ import type { IMessage, IRoom } from '@rocket.chat/core-typings'; +import { createPredicateFromFilter } from '@rocket.chat/mongo-adapter'; +import type { Filter } from '@rocket.chat/mongo-adapter'; import { ReactiveVar } from 'meteor/reactive-var'; import { Tracker } from 'meteor/tracker'; -import type { Filter } from 'mongodb'; import { upsertMessage, RoomHistoryManager } from './RoomHistoryManager'; import { mainReady } from './mainReady'; @@ -104,7 +105,8 @@ const computation = Tracker.autorun(() => { // Do not load command messages into channel if (msg.t !== 'command') { const subscription = Subscriptions.findOne({ rid: record.rid }, { reactive: false }); - const isNew = !Messages.findOne({ _id: msg._id, temp: { $ne: true } }); + const isNew = !Messages.state.find((record) => record._id === msg._id && record.temp !== true); + ({ _id: msg._id, temp: { $ne: true } }); await upsertMessage({ msg, subscription }); if (isNew) { @@ -137,10 +139,13 @@ const computation = Tracker.autorun(() => { }); sdk.stream('notify-room', [`${record.rid}/deleteMessage`], (msg) => { - Messages.remove({ _id: msg._id }); + Messages.state.delete(msg._id); // remove thread refenrece from deleted message - Messages.update({ tmid: msg._id }, { $unset: { tmid: 1 } }, { multi: true }); + Messages.state.update( + (record) => record.tmid === msg._id, + ({ tmid: _, ...record }) => record, + ); }); sdk.stream( @@ -164,44 +169,36 @@ const computation = Tracker.autorun(() => { query['u.username'] = { $in: users }; } + const predicate = createPredicateFromFilter(query); + if (showDeletedStatus) { - return Messages.update( - query, - { $set: { t: 'rm', msg: '', urls: [], mentions: [], attachments: [], reactions: {} } }, - { multi: true }, - ); + return Messages.state.update(predicate, (record) => ({ + ...record, + t: 'rm', + msg: '', + urls: [], + mentions: [], + attachments: [], + reactions: {}, + })); } - return Messages.remove(query); + + return Messages.state.remove(predicate); }, ); sdk.stream('notify-room', [`${record.rid}/messagesRead`], ({ tmid, until }) => { if (tmid) { - return Messages.update( - { - tmid, - unread: true, - }, - { $unset: { unread: 1 } }, - { multi: true }, + Messages.state.update( + (record) => record.tmid === tmid && record.unread === true, + ({ unread: _, ...record }) => record, ); + return; } - Messages.update( - { - rid: record.rid, - unread: true, - ts: { $lt: until }, - $or: [ - { - tmid: { $exists: false }, - }, - { - tshow: true, - }, - ], - }, - { $unset: { unread: 1 } }, - { multi: true }, + Messages.state.update( + (r) => + r.rid === record.rid && r.unread === true && r.ts.getTime() < until.getTime() && (r.tmid === undefined || r.tshow === true), + ({ unread: _, ...r }) => r, ); }); } diff --git a/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts b/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts index 1ddd44f21b2bd..f91e60cbb2711 100644 --- a/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts +++ b/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts @@ -227,7 +227,10 @@ class RoomHistoryManagerClass extends Emitter { room.isLoading.set(true); - const lastMessage = Messages.findOne({ rid, _hidden: { $ne: true } }, { sort: { ts: -1 } }); + const lastMessage = Messages.state.findFirst( + (record) => record.rid === rid && record._hidden !== true, + (a, b) => b.ts.getTime() - a.ts.getTime(), + ); const subscription = Subscriptions.findOne({ rid }); @@ -278,13 +281,13 @@ class RoomHistoryManagerClass extends Emitter { } public close(rid: IRoom['_id']) { - Messages.remove({ rid }); + Messages.state.remove((record) => record.rid === rid); delete this.histories[rid]; } public clear(rid: IRoom['_id']) { const room = this.getRoom(rid); - Messages.remove({ rid }); + Messages.state.remove((record) => record.rid === rid); room.isLoading.set(false); room.hasMore.set(true); room.hasMoreNext.set(false); @@ -297,7 +300,7 @@ class RoomHistoryManagerClass extends Emitter { return; } - const messageAlreadyLoaded = Boolean(Messages.findOne({ _id: message._id, _hidden: { $ne: true } })); + const messageAlreadyLoaded = Messages.state.some((record) => record._id === message._id && record._hidden !== true); if (messageAlreadyLoaded) { return; diff --git a/apps/meteor/client/components/message/toolbar/useTranslateAction.ts b/apps/meteor/client/components/message/toolbar/useTranslateAction.ts index 5848b172b9787..f9448ced93f80 100644 --- a/apps/meteor/client/components/message/toolbar/useTranslateAction.ts +++ b/apps/meteor/client/components/message/toolbar/useTranslateAction.ts @@ -26,6 +26,8 @@ export const useTranslateAction = ( [message, language], ); + const updateMessages = Messages.use((state) => state.update); + if (!autoTranslateEnabled || !canAutoTranslate || !user) { return null; } @@ -48,11 +50,19 @@ export const useTranslateAction = ( action() { if (!hasTranslations) { AutoTranslate.messageIdsToWait[message._id] = true; - Messages.update({ _id: message._id }, { $set: { autoTranslateFetching: true } }); + updateMessages( + (record) => record._id === message._id, + (record) => ({ ...record, autoTranslateFetching: true }), + ); void translateMessage(message, language); } - const action = 'autoTranslateShowInverse' in message ? '$unset' : '$set'; - Messages.update({ _id: message._id }, { [action]: { autoTranslateShowInverse: true } }); + + updateMessages( + (record) => record._id === message._id, + 'autoTranslateShowInverse' in message + ? ({ autoTranslateShowInverse: _, ...record }) => record + : (record) => ({ ...record, autoTranslateShowInverse: true }), + ); }, order: 90, }; diff --git a/apps/meteor/client/components/message/toolbar/useViewOriginalTranslationAction.ts b/apps/meteor/client/components/message/toolbar/useViewOriginalTranslationAction.ts index 3d6e91e5eb545..103471e1a6f6d 100644 --- a/apps/meteor/client/components/message/toolbar/useViewOriginalTranslationAction.ts +++ b/apps/meteor/client/components/message/toolbar/useViewOriginalTranslationAction.ts @@ -26,6 +26,8 @@ export const useViewOriginalTranslationAction = ( [message, language], ); + const updateMessages = Messages.use((state) => state.update); + if (!autoTranslateEnabled || !canAutoTranslate || !user) { return null; } @@ -48,11 +50,19 @@ export const useViewOriginalTranslationAction = ( action() { if (!hasTranslations) { AutoTranslate.messageIdsToWait[message._id] = true; - Messages.update({ _id: message._id }, { $set: { autoTranslateFetching: true } }); + updateMessages( + (record) => record._id === message._id, + (record) => ({ ...record, autoTranslateFetching: true }), + ); void translateMessage(message, language); } - const action = 'autoTranslateShowInverse' in message ? '$unset' : '$set'; - Messages.update({ _id: message._id }, { [action]: { autoTranslateShowInverse: true } }); + + updateMessages( + (record) => record._id === message._id, + 'autoTranslateShowInverse' in message + ? ({ autoTranslateShowInverse: _, ...record }) => record + : (record) => ({ ...record, autoTranslateShowInverse: true }), + ); }, order: 90, }; diff --git a/apps/meteor/client/lib/cachedCollections/CachedCollection.ts b/apps/meteor/client/lib/cachedCollections/CachedCollection.ts index fb1c141fb3db3..c68839f3159f6 100644 --- a/apps/meteor/client/lib/cachedCollections/CachedCollection.ts +++ b/apps/meteor/client/lib/cachedCollections/CachedCollection.ts @@ -211,7 +211,7 @@ export abstract class CachedCollection { } if (action === 'removed') { - this.collection.state.delete(newRecord); + this.collection.state.delete(newRecord._id); } else { const { _id } = newRecord; if (!_id) return; @@ -280,7 +280,7 @@ export abstract class CachedCollection { const actionTime = newRecord._deletedAt; changes.push({ action: () => { - this.collection.state.delete(newRecord); + this.collection.state.delete(newRecord._id); if (actionTime > this.updatedAt) { this.updatedAt = actionTime; } diff --git a/apps/meteor/client/lib/cachedCollections/DocumentMapStore.ts b/apps/meteor/client/lib/cachedCollections/DocumentMapStore.ts index 188f849004aa9..74d92800441da 100644 --- a/apps/meteor/client/lib/cachedCollections/DocumentMapStore.ts +++ b/apps/meteor/client/lib/cachedCollections/DocumentMapStore.ts @@ -1,17 +1,92 @@ +import { create } from 'zustand'; + export interface IDocumentMapStore { readonly records: readonly T[]; has(_id: T['_id']): boolean; get(_id: T['_id']): T | undefined; + some(predicate: (record: T) => boolean): boolean; find(predicate: (record: T) => record is U): U | undefined; find(predicate: (record: T) => boolean): T | undefined; + findFirst(predicate: (record: T) => record is U, comparator: (a: T, b: T) => number): U | undefined; + findFirst(predicate: (record: T) => boolean, comparator: (a: T, b: T) => number): T | undefined; filter(predicate: (record: T) => record is U): U[]; filter(predicate: (record: T) => boolean): T[]; replaceAll(records: T[]): void; store(doc: T): void; storeMany(docs: Iterable): void; - delete(doc: T): void; + delete(_id: T['_id']): void; update(predicate: (record: T) => record is U, modifier: (record: U) => U): void; update(predicate: (record: T) => boolean, modifier: (record: T) => T): void; updateAsync(predicate: (record: T) => record is U, modifier: (record: U) => Promise): Promise; updateAsync(predicate: (record: T) => boolean, modifier: (record: T) => Promise): Promise; + remove(predicate: (record: T) => boolean): void; } + +export const createDocumentMapStore = ({ onMutate }: { onMutate?: () => void } = {}) => + create>()((set, get) => ({ + records: [], + has: (id: T['_id']) => get().records.some((record) => record._id === id), + get: (id: T['_id']) => get().records.find((record) => record._id === id), + some: (predicate: (record: T) => boolean) => get().records.some(predicate), + find: (predicate: (record: T) => boolean) => get().records.find(predicate), + findFirst: (predicate: (record: T) => boolean, comparator: (a: T, b: T) => number) => + get().records.filter(predicate).sort(comparator)[0], // TODO: optimize this + filter: (predicate: (record: T) => boolean) => get().records.filter(predicate), + replaceAll: (records: T[]) => { + set({ records: records.map(Object.freeze) }); + onMutate?.(); + }, + store: (doc) => { + set((state) => { + const records = [...state.records]; + const index = records.findIndex((r) => r._id === doc._id); + if (index !== -1) { + records[index] = Object.freeze(doc); + } else { + records.push(Object.freeze(doc)); + } + return { records }; + }); + onMutate?.(); + }, + storeMany: (docs) => { + const records = [...get().records]; + + for (const doc of docs) { + const index = records.findIndex((r) => r._id === doc._id); + if (index !== -1) { + records[index] = Object.freeze(doc); + } else { + records.push(Object.freeze(doc)); + } + } + set({ records }); + onMutate?.(); + }, + delete: (_id) => { + set((state) => { + const records = state.records.filter((r) => r._id !== _id); + return { records }; + }); + onMutate?.(); + }, + update: (predicate: (record: T) => boolean, modifier: (record: T) => T) => { + set({ + records: get().records.map((record) => (predicate(record) ? modifier(record) : record)), + }); + onMutate?.(); + }, + updateAsync: async (predicate: (record: T) => boolean, modifier: (record: T) => Promise) => { + set({ + records: await Promise.all(get().records.map((record) => (predicate(record) ? modifier(record) : record))), + }); + onMutate?.(); + }, + remove: (predicate: (record: T) => boolean) => { + set((state) => { + const records = state.records.filter((record) => !predicate(record)); + return { records }; + }); + onMutate?.(); + }, + })); diff --git a/apps/meteor/client/lib/cachedCollections/MinimongoCollection.ts b/apps/meteor/client/lib/cachedCollections/MinimongoCollection.ts index 8cc664de75a41..7b92764e6901c 100644 --- a/apps/meteor/client/lib/cachedCollections/MinimongoCollection.ts +++ b/apps/meteor/client/lib/cachedCollections/MinimongoCollection.ts @@ -1,7 +1,6 @@ import { Mongo } from 'meteor/mongo'; -import { create } from 'zustand'; -import type { IDocumentMapStore } from './DocumentMapStore'; +import { createDocumentMapStore } from './DocumentMapStore'; import { LocalCollection } from './LocalCollection'; /** @@ -15,63 +14,9 @@ export class MinimongoCollection extends Mongo.Collec * * It should be used as a hook in React components to access the collection's records and methods. */ - readonly use = create>()((set, get) => ({ - records: [], - has: (id: T['_id']) => get().records.some((record) => record._id === id), - get: (id: T['_id']) => get().records.find((record) => record._id === id), - find: (predicate: (record: T) => boolean) => get().records.find(predicate), - filter: (predicate: (record: T) => boolean) => get().records.filter(predicate), - replaceAll: (records: T[]) => { - set({ records: records.map(Object.freeze) }); - this.recomputeQueries(); - }, - store: (doc) => { - set((state) => { - const records = [...state.records]; - const index = records.findIndex((r) => r._id === doc._id); - if (index !== -1) { - records[index] = Object.freeze(doc); - } else { - records.push(Object.freeze(doc)); - } - return { records }; - }); - this.recomputeQueries(); - }, - storeMany: (docs) => { - const records = [...get().records]; - - for (const doc of docs) { - const index = records.findIndex((r) => r._id === doc._id); - if (index !== -1) { - records[index] = Object.freeze(doc); - } else { - records.push(Object.freeze(doc)); - } - } - set({ records }); - this.recomputeQueries(); - }, - delete: (doc) => { - set((state) => { - const records = state.records.filter((r) => r._id !== doc._id); - return { records }; - }); - this.recomputeQueries(); - }, - update: (predicate: (record: T) => boolean, modifier: (record: T) => T) => { - set({ - records: get().records.map((record) => (predicate(record) ? modifier(record) : record)), - }); - this._collection.recomputeAllResults(); - }, - updateAsync: async (predicate: (record: T) => boolean, modifier: (record: T) => Promise) => { - set({ - records: await Promise.all(get().records.map((record) => (predicate(record) ? modifier(record) : record))), - }); - this._collection.recomputeAllResults(); - }, - })); + readonly use = createDocumentMapStore({ + onMutate: () => this.recomputeQueries(), + }); /** * The internal collection that manages the queries and results. diff --git a/apps/meteor/client/lib/chats/data.ts b/apps/meteor/client/lib/chats/data.ts index 260db8da3f303..f53f956b93b8f 100644 --- a/apps/meteor/client/lib/chats/data.ts +++ b/apps/meteor/client/lib/chats/data.ts @@ -39,7 +39,7 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage }; const findMessageByID = async (mid: IMessage['_id']): Promise => - Messages.findOne({ _id: mid, _hidden: { $ne: true } }, { reactive: false }) ?? sdk.call('getSingleMessage', mid); + Messages.state.find((record) => record._id === mid && record._hidden !== true) ?? sdk.call('getSingleMessage', mid); const getMessageByID = async (mid: IMessage['_id']): Promise => { const message = await findMessageByID(mid); @@ -52,7 +52,10 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage }; const findLastMessage = async (): Promise => - Messages.findOne({ rid, tmid: tmid ?? { $exists: false }, _hidden: { $ne: true } }, { sort: { ts: -1 }, reactive: false }); + Messages.state.findFirst( + (record) => record.rid === rid && (tmid ? record.tmid === tmid : !record.tmid) && record._hidden !== true, + (a, b) => b.ts.getTime() - a.ts.getTime(), + ); const getLastMessage = async (): Promise => { const message = await findLastMessage(); @@ -99,9 +102,14 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage return undefined; } - const msg = Messages.findOne( - { rid, 'tmid': tmid ?? { $exists: false }, 'u._id': uid, '_hidden': { $ne: true }, 'ts': { ...(message && { $lt: message.ts }) } }, - { sort: { ts: -1 }, reactive: false }, + const msg = Messages.state.findFirst( + (record) => + record.rid === rid && + (tmid ? record.tmid === tmid : !record.tmid) && + record.u._id === uid && + record._hidden !== true && + record.ts.getTime() < (message?.ts.getTime() ?? Date.now()), + (a, b) => b.ts.getTime() - a.ts.getTime(), ); if (!msg) { @@ -132,9 +140,14 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage return undefined; } - const msg = Messages.findOne( - { rid, 'tmid': tmid ?? { $exists: false }, 'u._id': uid, '_hidden': { $ne: true }, 'ts': { $gt: message.ts } }, - { sort: { ts: 1 }, reactive: false }, + const msg = Messages.state.findFirst( + (record) => + record.rid === rid && + (tmid ? record.tmid === tmid : !record.tmid) && + record.u._id === uid && + record._hidden !== true && + record.ts.getTime() > message.ts.getTime(), + (a, b) => a.ts.getTime() - b.ts.getTime(), ); if (!msg) { @@ -159,7 +172,7 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage }; const pushEphemeralMessage = async (message: Omit): Promise => { - Messages.upsert({ _id: message._id }, { $set: { ...message, rid, ...(tmid && { tmid }) } }); + Messages.state.store({ ...message, rid, ...(tmid && { tmid }) }); }; const updateMessage = async (message: IEditedMessage, previewUrls?: string[]): Promise => diff --git a/apps/meteor/client/lib/chats/readStateManager.ts b/apps/meteor/client/lib/chats/readStateManager.ts index fbb0c849e9680..38314073f3af8 100644 --- a/apps/meteor/client/lib/chats/readStateManager.ts +++ b/apps/meteor/client/lib/chats/readStateManager.ts @@ -74,21 +74,10 @@ export class ReadStateManager extends Emitter { return; } - const firstUnreadRecord = Messages.findOne( - { - 'rid': this.subscription.rid, - 'ts': { - $gt: this.subscription.ls, - }, - 'u._id': { - $ne: Meteor.userId() ?? undefined, - }, - }, - { - sort: { - ts: 1, - }, - }, + const firstUnreadRecord = Messages.state.findFirst( + (record) => + record.rid === this.subscription?.rid && record.ts.getTime() > this.subscription.ls.getTime() && record.u._id !== Meteor.userId(), + (a, b) => a.ts.getTime() - b.ts.getTime(), ); this.setFirstUnreadRecordId(firstUnreadRecord?._id); diff --git a/apps/meteor/client/lib/getPermaLink.ts b/apps/meteor/client/lib/getPermaLink.ts index 279786c003750..3c0def1459137 100644 --- a/apps/meteor/client/lib/getPermaLink.ts +++ b/apps/meteor/client/lib/getPermaLink.ts @@ -18,7 +18,7 @@ export const getPermaLink = async (msgId: string): Promise => { const { Messages, Rooms, Subscriptions } = await import('../../app/models/client'); - const msg = Messages.findOne(msgId) || (await getMessage(msgId)); + const msg = Messages.state.get(msgId) || (await getMessage(msgId)); if (!msg) { throw new Error('message-not-found'); } diff --git a/apps/meteor/client/lib/mutationEffects/starredMessage.ts b/apps/meteor/client/lib/mutationEffects/starredMessage.ts index 45bd772415a85..e5657940a2e37 100644 --- a/apps/meteor/client/lib/mutationEffects/starredMessage.ts +++ b/apps/meteor/client/lib/mutationEffects/starredMessage.ts @@ -7,23 +7,21 @@ export const toggleStarredMessage = (message: IMessage, starred: boolean) => { const uid = Meteor.userId()!; if (starred) { - Messages.update( - { _id: message._id }, - { - $addToSet: { - starred: { _id: uid }, - }, - }, + Messages.state.update( + (record) => record._id === message._id, + (record) => ({ + ...record, + starred: [...(record.starred ?? []), { _id: uid }], + }), ); return; } - Messages.update( - { _id: message._id }, - { - $pull: { - starred: { _id: uid }, - }, - }, + Messages.state.update( + (record) => record._id === message._id, + (record) => ({ + ...record, + starred: (record.starred ?? []).filter((star) => star._id !== uid), + }), ); }; diff --git a/apps/meteor/client/lib/mutationEffects/updatePinMessage.ts b/apps/meteor/client/lib/mutationEffects/updatePinMessage.ts index 0e2c2d9cdc03c..36215300cbe0a 100644 --- a/apps/meteor/client/lib/mutationEffects/updatePinMessage.ts +++ b/apps/meteor/client/lib/mutationEffects/updatePinMessage.ts @@ -4,7 +4,7 @@ import { Messages } from '../../../app/models/client'; import { PinMessagesNotAllowed } from '../errors/PinMessagesNotAllowed'; export const updatePinMessage = (message: IMessage, data: Partial) => { - const msg = Messages.findOne({ _id: message._id }); + const msg = Messages.state.get(message._id); if (!msg) { throw new PinMessagesNotAllowed('Error pinning message', { @@ -12,11 +12,8 @@ export const updatePinMessage = (message: IMessage, data: Partial) => }); } - Messages.update( - { - _id: message._id, - rid: message.rid, - }, - { $set: data }, + Messages.state.update( + (record) => record._id === message._id && record.rid === message.rid, + (record) => ({ ...record, ...data }), ); }; diff --git a/apps/meteor/client/providers/UserProvider/hooks/useDeleteUser.ts b/apps/meteor/client/providers/UserProvider/hooks/useDeleteUser.ts index d292b1a5b31f1..2a88ae4f2c5a3 100644 --- a/apps/meteor/client/providers/UserProvider/hooks/useDeleteUser.ts +++ b/apps/meteor/client/providers/UserProvider/hooks/useDeleteUser.ts @@ -7,30 +7,26 @@ export const useDeleteUser = () => { const notify = useStream('notify-logged'); const uid = useUserId(); + + const updateMessages = Messages.use((state) => state.update); + const removeMessages = Messages.use((state) => state.remove); + useEffect(() => { if (!uid) { return; } return notify('Users:Deleted', ({ userId, messageErasureType, replaceByUser }) => { if (messageErasureType === 'Unlink' && replaceByUser) { - return Messages.update( - { - 'u._id': userId, - }, - { - $set: { - 'alias': replaceByUser.alias, - 'u._id': replaceByUser._id, - 'u.username': replaceByUser.username, - 'u.name': undefined, - }, - }, - { multi: true }, + return updateMessages( + (record) => record.u._id === userId, + (record) => ({ + ...record, + alias: replaceByUser.alias, + u: { ...record.u, _id: replaceByUser._id, username: replaceByUser.username ?? record.u.username, name: undefined }, + }), ); } - Messages.remove({ - 'u._id': userId, - }); + removeMessages((record) => record.u._id === userId); }); - }, [notify, uid]); + }, [notify, removeMessages, uid, updateMessages]); }; diff --git a/apps/meteor/client/startup/incomingMessages.ts b/apps/meteor/client/startup/incomingMessages.ts index 8f4149e9de94d..cdad11264a12b 100644 --- a/apps/meteor/client/startup/incomingMessages.ts +++ b/apps/meteor/client/startup/incomingMessages.ts @@ -13,25 +13,20 @@ Meteor.startup(() => { msg.u = msg.u || { username: 'rocket.cat' }; msg.private = true; - return Messages.upsert({ _id: msg._id }, msg); + return Messages.state.store(msg); }); }); onLoggedIn(() => { return sdk.stream('notify-user', [`${Meteor.userId()}/subscriptions-changed`], (_action, sub) => { - Messages.update( - { - rid: sub.rid, - ...('ignored' in sub && sub.ignored ? { 'u._id': { $nin: sub.ignored } } : { ignored: { $exists: true } }), - }, - { $unset: { ignored: true } }, - { multi: true }, + Messages.state.update( + (record) => record.rid === sub.rid && ('ignored' in sub && sub.ignored ? !sub.ignored.includes(record.u._id) : 'ignored' in record), + ({ ignored: _, ...record }) => record, ); if ('ignored' in sub && sub.ignored) { - Messages.update( - { 'rid': sub.rid, 't': { $ne: 'command' }, 'u._id': { $in: sub.ignored } }, - { $set: { ignored: true } }, - { multi: true }, + Messages.state.update( + (record) => record.rid === sub.rid && record.t !== 'command' && (sub.ignored?.includes(record.u._id) ?? false), + (record) => ({ ...record, ignored: true }), ); } }); diff --git a/apps/meteor/client/startup/index.ts b/apps/meteor/client/startup/index.ts index 57ea02eef704d..e085499aad01a 100644 --- a/apps/meteor/client/startup/index.ts +++ b/apps/meteor/client/startup/index.ts @@ -8,7 +8,6 @@ import './deviceManagement'; import './e2e'; import './iframeCommands'; import './incomingMessages'; -import './messageObserve'; import './messageTypes'; import './roles'; import './routes'; diff --git a/apps/meteor/client/startup/messageObserve.ts b/apps/meteor/client/startup/messageObserve.ts deleted file mode 100644 index 760c8b76e5c64..0000000000000 --- a/apps/meteor/client/startup/messageObserve.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { Messages } from '../../app/models/client'; -import { LegacyRoomManager } from '../../app/ui-utils/client'; - -Meteor.startup(() => { - Messages.find().observe({ - removed(record) { - if (!LegacyRoomManager.getOpenedRoomByRid(record.rid)) { - return; - } - - const recordBefore = Messages.findOne({ ts: { $lt: record.ts } }, { sort: { ts: -1 } }); - if (recordBefore) { - Messages.update({ _id: recordBefore._id }, { $set: { tick: new Date() } }); - } - - const recordAfter = Messages.findOne({ ts: { $gt: record.ts } }, { sort: { ts: 1 } }); - if (recordAfter) { - return Messages.update({ _id: recordAfter._id }, { $set: { tick: new Date() } }); - } - }, - }); -}); diff --git a/apps/meteor/client/views/room/MessageList/hooks/useMessages.ts b/apps/meteor/client/views/room/MessageList/hooks/useMessages.ts index 1defd0a455390..166ab15949471 100644 --- a/apps/meteor/client/views/room/MessageList/hooks/useMessages.ts +++ b/apps/meteor/client/views/room/MessageList/hooks/useMessages.ts @@ -1,11 +1,11 @@ import type { IRoom, IMessage, MessageTypesValues } from '@rocket.chat/core-typings'; import { useStableArray } from '@rocket.chat/fuselage-hooks'; +import { createPredicateFromFilter } from '@rocket.chat/mongo-adapter'; import { useSetting, useUserPreference } from '@rocket.chat/ui-contexts'; -import type { Filter } from 'mongodb'; -import { useCallback, useMemo } from 'react'; +import { useMemo } from 'react'; +import { useShallow } from 'zustand/shallow'; import { Messages } from '../../../../../app/models/client'; -import { useReactiveValue } from '../../../../hooks/useReactiveValue'; import { useRoom } from '../../contexts/RoomContext'; const mergeHideSysMessages = ( @@ -23,27 +23,18 @@ export const useMessages = ({ rid }: { rid: IRoom['_id'] }): IMessage[] => { const hideSysMessages = useStableArray(mergeHideSysMessages(hideSysMesSetting, hideRoomSysMes)); - const query: Filter = useMemo( - () => ({ - rid, - _hidden: { $ne: true }, - t: { $nin: hideSysMessages }, - ...(!showThreadsInMainChannel && { - $or: [{ tmid: { $exists: false } }, { tshow: { $eq: true } }], + const predicate = useMemo( + () => + createPredicateFromFilter({ + rid, + _hidden: { $ne: true }, + t: { $nin: hideSysMessages }, + ...(!showThreadsInMainChannel && { + $or: [{ tmid: { $exists: false } }, { tshow: { $eq: true } }], + }), }), - }), [rid, hideSysMessages, showThreadsInMainChannel], ); - return useReactiveValue( - useCallback( - () => - Messages.find(query, { - sort: { - ts: 1, - }, - }).fetch(), - [query], - ), - ); + return Messages.use(useShallow((state) => state.filter(predicate).sort((a, b) => a.ts.getTime() - b.ts.getTime()))); }; diff --git a/apps/meteor/client/views/room/body/hooks/useUnreadMessages.ts b/apps/meteor/client/views/room/body/hooks/useUnreadMessages.ts index 59b0e59f1337a..ddae689fdd2a3 100644 --- a/apps/meteor/client/views/room/body/hooks/useUnreadMessages.ts +++ b/apps/meteor/client/views/room/body/hooks/useUnreadMessages.ts @@ -52,6 +52,10 @@ export const useHandleUnread = ( const chat = useChat(); + const getMessage = Messages.use((state) => state.get); + const findFirstMessage = Messages.use((state) => state.findFirst); + const filterMessages = Messages.use((state) => state.filter); + if (!chat) { throw new Error('No ChatContext provided'); } @@ -60,14 +64,17 @@ export const useHandleUnread = ( const { firstUnread } = RoomHistoryManager.getRoom(rid); let message = firstUnread?.get(); if (!message) { - message = Messages.findOne({ rid, ts: { $gt: unread?.since } }, { sort: { ts: 1 }, limit: 1 }); + message = findFirstMessage( + (record) => record.rid === rid && record.ts.getTime() > (unread?.since.getTime() ?? -Infinity), + (a, b) => a.ts.getTime() - b.ts.getTime(), + ); } if (!message) { return; } setMessageJumpQueryStringParameter(message?._id); setUnreadCount(0); - }, [room._id, unread?.since, setUnreadCount]); + }, [room._id, setUnreadCount, findFirstMessage, unread?.since]); const handleMarkAsReadButtonClick = useCallback(() => { chat.readStateManager.markAsRead(); @@ -80,13 +87,15 @@ export const useHandleUnread = ( return; } - const count = Messages.find({ - rid: room._id, - ts: { $lte: lastMessageDate, $gt: subscription?.ls }, - }).count(); + const count = filterMessages( + (record) => + record.rid === room._id && + record.ts.getTime() <= (lastMessageDate?.getTime() ?? Infinity) && + record.ts.getTime() > (subscription?.ls?.getTime() ?? -Infinity), + ).length; setUnreadCount(count); - }, [lastMessageDate, room._id, setUnreadCount, subscribed, subscription?.ls]); + }, [filterMessages, lastMessageDate, room._id, setUnreadCount, subscribed, subscription?.ls]); const router = useRouter(); @@ -158,7 +167,7 @@ export const useHandleUnread = ( return; } - const lastMessage = Messages.findOne(lastInvisibleMessageOnScreen.id); + const lastMessage = getMessage(lastInvisibleMessageOnScreen.id); if (!lastMessage) { setUnreadCount(0); return; @@ -169,7 +178,7 @@ export const useHandleUnread = ( }), ); }, - [setUnreadCount], + [getMessage, setUnreadCount], ); return { diff --git a/apps/meteor/client/views/room/contextualBar/ExportMessages/useDownloadExportMutation.ts b/apps/meteor/client/views/room/contextualBar/ExportMessages/useDownloadExportMutation.ts index c1ce80e50e34f..40712a5b60dd6 100644 --- a/apps/meteor/client/views/room/contextualBar/ExportMessages/useDownloadExportMutation.ts +++ b/apps/meteor/client/views/room/contextualBar/ExportMessages/useDownloadExportMutation.ts @@ -1,51 +1,48 @@ import type { IMessage } from '@rocket.chat/core-typings'; import { useToastMessageDispatch, useUser } from '@rocket.chat/ui-contexts'; import { useMutation } from '@tanstack/react-query'; -import type { FindOptions } from 'mongodb'; import { useTranslation } from 'react-i18next'; import { Messages } from '../../../../../app/models/client'; import { downloadJsonAs } from '../../../../lib/download'; import { useRoom } from '../../contexts/RoomContext'; -const messagesFields: FindOptions = { - projection: { - '_id': 1, - 'ts': 1, - 'u': 1, - 'msg': 1, - '_updatedAt': 1, - 'tlm': 1, - 'replies': 1, - 'tmid': 1, - 'attachments.ts': 1, - 'attachments.title': 1, - 'attachments.title_link': 1, - 'attachments.title_link_download': 1, - 'attachments.image_dimensions': 1, - 'attachments.image_preview': 1, - 'attachments.image_url': 1, - 'attachments.image_type': 1, - 'attachments.image_size': 1, - 'attachments.type': 1, - 'attachments.description': 1, - }, -}; - export const useDownloadExportMutation = () => { const { t } = useTranslation(); const room = useRoom(); const user = useUser(); const dispatchToastMessage = useToastMessageDispatch(); + const filterMessages = Messages.use((state) => state.filter); + return useMutation({ mutationFn: async ({ mids }: { mids: IMessage['_id'][] }) => { - const messages = Messages.find( - { - $or: [{ _id: { $in: mids } }, { tmid: { $in: mids } }], - }, - messagesFields, - ).fetch(); + const messages = filterMessages((record) => mids.includes(record._id) || (!!record.tmid && mids.includes(record.tmid))).map( + ({ _id, ts, u, msg, _updatedAt, tlm, replies, tmid, attachments }) => ({ + _id, + ts, + u, + msg, + _updatedAt, + tlm, + replies, + tmid, + attachments: + attachments?.map((attachment) => ({ + ts: attachment.ts, + title: attachment.title, + title_link: attachment.title_link, + title_link_download: attachment.title_link_download, + ...('image_dimensions' in attachment && { image_dimensions: attachment.image_dimensions }), + ...('image_preview' in attachment && { image_preview: attachment.image_preview }), + ...('image_url' in attachment && { image_url: attachment.image_url }), + ...('image_type' in attachment && { image_type: attachment.image_type }), + ...('image_size' in attachment && { image_size: attachment.image_size }), + ...('type' in attachment && { type: attachment.type }), + description: attachment.description, + })) ?? [], + }), + ); const fileData = { roomId: room._id, diff --git a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useGetMessageByID.ts b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useGetMessageByID.ts index c9898a172495d..7ee2491bb86a3 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useGetMessageByID.ts +++ b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useGetMessageByID.ts @@ -8,6 +8,7 @@ import { mapMessageFromApi } from '../../../../../lib/utils/mapMessageFromApi'; export const useGetMessageByID = () => { const getMessage = useEndpoint('GET', '/v1/chat.getMessage'); + const storeMessage = Messages.use((state) => state.store); return useCallback( async (mid: IMessage['_id']) => { @@ -15,7 +16,7 @@ export const useGetMessageByID = () => { const { message: rawMessage } = await getMessage({ msgId: mid }); const mappedMessage = mapMessageFromApi(rawMessage); const message = (await onClientMessageReceived(mappedMessage)) || mappedMessage; - Messages.upsert({ _id: message._id }, { $set: message as any }); + storeMessage(message); return message; } catch (error) { if (typeof error === 'object' && error !== null && 'success' in error) { @@ -25,6 +26,6 @@ export const useGetMessageByID = () => { throw error; } }, - [getMessage], + [getMessage, storeMessage], ); }; diff --git a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useLegacyThreadMessages.ts b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useLegacyThreadMessages.ts index cae266be653c4..8ba4e73762095 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useLegacyThreadMessages.ts +++ b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useLegacyThreadMessages.ts @@ -1,11 +1,11 @@ import type { IThreadMessage, IThreadMainMessage } from '@rocket.chat/core-typings'; import { isThreadMessage } from '@rocket.chat/core-typings'; import { useMethod } from '@rocket.chat/ui-contexts'; -import { useEffect, useState, useCallback } from 'react'; +import { useEffect, useState } from 'react'; +import { useShallow } from 'zustand/shallow'; import { Messages } from '../../../../../../app/models/client'; import { upsertMessageBulk } from '../../../../../../app/ui-utils/client/lib/RoomHistoryManager'; -import { useReactiveValue } from '../../../../../hooks/useReactiveValue'; export const useLegacyThreadMessages = ( tmid: IThreadMainMessage['_id'], @@ -13,27 +13,19 @@ export const useLegacyThreadMessages = ( messages: Array; loading: boolean; } => { - const messages = useReactiveValue( - useCallback(() => { - return Messages.find( - { - $or: [{ tmid }, { _id: tmid }], - _hidden: { $ne: true }, - tmid, - _id: { $ne: tmid }, - }, - { - fields: { - collapsed: 0, - threadMsg: 0, - repliesCount: 0, - }, - sort: { ts: 1 }, - }, - ) - .fetch() - .filter(isThreadMessage); - }, [tmid]), + const messages = Messages.use( + useShallow((state) => + state + .filter( + (record): record is IThreadMessage => + (record.tmid === tmid || record._id === tmid) && + record._hidden !== true && + record.tmid === tmid && + record._id !== tmid && + isThreadMessage(record), + ) + .sort((a, b) => a.ts.getTime() - b.ts.getTime()), + ), ); const [loading, setLoading] = useState(messages.length === 0); diff --git a/apps/meteor/client/views/room/providers/ComposerPopupProvider.tsx b/apps/meteor/client/views/room/providers/ComposerPopupProvider.tsx index 08f4a60b88737..660692d6d65c5 100644 --- a/apps/meteor/client/views/room/providers/ComposerPopupProvider.tsx +++ b/apps/meteor/client/views/room/providers/ComposerPopupProvider.tsx @@ -44,32 +44,19 @@ const getLastRecentUsers = (rid: string, uid: string) => { suggestion?: boolean; } >(); - Messages.find( - { - rid, - 'u._id': { $ne: uid }, - 't': { $exists: false }, - 'ts': { $exists: true }, - }, - { - fields: { - 'u.username': 1, - 'u.name': 1, - 'u._id': 1, - 'ts': 1, - }, - sort: { ts: -1 }, - }, - ).forEach(({ u: { username, name, _id }, ts }) => { - if (!uniqueUsers.has(username)) { - uniqueUsers.set(username, { - _id, - username, - name, - ts, - }); - } - }); + Messages.state + .filter((record) => record.rid === rid && record.u && record.u._id !== uid && !record.t && !!record.ts) + .sort((a, b) => b.ts.getTime() - a.ts.getTime()) + .forEach(({ u: { username, name, _id }, ts }) => { + if (!uniqueUsers.has(username)) { + uniqueUsers.set(username, { + _id, + username, + name, + ts, + }); + } + }); return Array.from(uniqueUsers.values()); }; diff --git a/apps/meteor/client/views/room/providers/hooks/useUsersNameChanged.ts b/apps/meteor/client/views/room/providers/hooks/useUsersNameChanged.ts index 80cdc2dd9f042..840c1e07bc41e 100644 --- a/apps/meteor/client/views/room/providers/hooks/useUsersNameChanged.ts +++ b/apps/meteor/client/views/room/providers/hooks/useUsersNameChanged.ts @@ -1,3 +1,5 @@ +import type { IEditedMessage } from '@rocket.chat/core-typings'; +import { isEditedMessage } from '@rocket.chat/core-typings'; import { useStream } from '@rocket.chat/ui-contexts'; import { useEffect } from 'react'; @@ -5,52 +7,38 @@ import { Messages, Subscriptions } from '../../../../../app/models/client'; export const useUsersNameChanged = () => { const notify = useStream('notify-logged'); + const updateMessages = Messages.use((state) => state.update); useEffect(() => { return notify('Users:NameChanged', ({ _id, name, username }) => { - Messages.update( - { - 'u._id': _id, - }, - { - $set: { - 'u.username': username, - 'u.name': name, + updateMessages( + (record) => record.u._id === _id, + (record) => ({ + ...record, + u: { + ...record.u, + username: username ?? record.u.username, + name, }, - }, - { - multi: true, - }, + }), ); - Messages.update( - { - 'editedBy._id': _id, - }, - { - $set: { - 'editedBy.username': username, + updateMessages( + (record): record is IEditedMessage => isEditedMessage(record) && record.editedBy?._id === _id, + (record) => ({ + ...record, + editedBy: { + ...record.editedBy, + username, }, - }, - { - multi: true, - }, + }), ); - Messages.update( - { - mentions: { - $elemMatch: { _id }, - }, - }, - { - $set: { - 'mentions.$.username': username, - 'mentions.$.name': name, - }, - }, - { - multi: true, - }, + updateMessages( + (record) => record.mentions?.some((mention) => mention._id === _id) ?? false, + (record) => ({ + ...record, + mentions: record.mentions?.map((mention) => (mention._id === _id ? { ...mention, username, name } : mention)), + }), ); Subscriptions.update( @@ -65,5 +53,5 @@ export const useUsersNameChanged = () => { }, ); }); - }, [notify]); + }, [notify, updateMessages]); }; diff --git a/apps/meteor/client/views/root/hooks/useLoadMissedMessages.ts b/apps/meteor/client/views/root/hooks/useLoadMissedMessages.ts index 976b1b2bafbbc..ac7bc56743a63 100644 --- a/apps/meteor/client/views/root/hooks/useLoadMissedMessages.ts +++ b/apps/meteor/client/views/root/hooks/useLoadMissedMessages.ts @@ -11,7 +11,10 @@ import { callWithErrorHandling } from '../../../lib/utils/callWithErrorHandling' * @param rid - Room ID */ const loadMissedMessages = async (rid: IRoom['_id']): Promise => { - const lastMessage = Messages.findOne({ rid, _hidden: { $ne: true }, temp: { $exists: false } }, { sort: { ts: -1 }, limit: 1 }); + const lastMessage = Messages.state.findFirst( + (record) => record.rid === rid && record._hidden !== true && !record.temp, + (a, b) => b.ts.getTime() - a.ts.getTime(), + ); if (!lastMessage) { return;