diff --git a/apps/meteor/app/api/server/v1/subscriptions.ts b/apps/meteor/app/api/server/v1/subscriptions.ts index 9d81fe6bef65..b92d9ba572fd 100644 --- a/apps/meteor/app/api/server/v1/subscriptions.ts +++ b/apps/meteor/app/api/server/v1/subscriptions.ts @@ -82,6 +82,7 @@ API.v1.addRoute( async post() { const { readThreads = false } = this.bodyParams; const roomId = 'rid' in this.bodyParams ? this.bodyParams.rid : this.bodyParams.roomId; + await readMessages(roomId, this.userId, readThreads); return API.v1.success(); diff --git a/apps/meteor/app/autotranslate/server/methods/saveSettings.ts b/apps/meteor/app/autotranslate/server/methods/saveSettings.ts index 2f119c948263..3d4d15c0316e 100644 --- a/apps/meteor/app/autotranslate/server/methods/saveSettings.ts +++ b/apps/meteor/app/autotranslate/server/methods/saveSettings.ts @@ -4,6 +4,7 @@ import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { notifyOnSubscriptionChangedById } from '../../../lib/server/lib/notifyListener'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -44,6 +45,8 @@ Meteor.methods({ }); } + let shouldNotifySubscriptionChanged = false; + switch (field) { case 'autoTranslate': const room = await Rooms.findE2ERoomById(rid, { projection: { _id: 1 } }); @@ -53,16 +56,34 @@ Meteor.methods({ }); } - await Subscriptions.updateAutoTranslateById(subscription._id, value === '1'); + const updateAutoTranslateResponse = await Subscriptions.updateAutoTranslateById(subscription._id, value === '1'); + if (updateAutoTranslateResponse.modifiedCount) { + shouldNotifySubscriptionChanged = true; + } + if (!subscription.autoTranslateLanguage && options.defaultLanguage) { - await Subscriptions.updateAutoTranslateLanguageById(subscription._id, options.defaultLanguage); + const updateAutoTranslateLanguageResponse = await Subscriptions.updateAutoTranslateLanguageById( + subscription._id, + options.defaultLanguage, + ); + if (updateAutoTranslateLanguageResponse.modifiedCount) { + shouldNotifySubscriptionChanged = true; + } } + break; case 'autoTranslateLanguage': - await Subscriptions.updateAutoTranslateLanguageById(subscription._id, value); + const updateAutoTranslateLanguage = await Subscriptions.updateAutoTranslateLanguageById(subscription._id, value); + if (updateAutoTranslateLanguage.modifiedCount) { + shouldNotifySubscriptionChanged = true; + } break; } + if (shouldNotifySubscriptionChanged) { + void notifyOnSubscriptionChangedById(subscription._id); + } + return true; }, }); diff --git a/apps/meteor/app/channel-settings/server/functions/saveRoomCustomFields.ts b/apps/meteor/app/channel-settings/server/functions/saveRoomCustomFields.ts index 55d40cf3d7e6..ef70ff65c067 100644 --- a/apps/meteor/app/channel-settings/server/functions/saveRoomCustomFields.ts +++ b/apps/meteor/app/channel-settings/server/functions/saveRoomCustomFields.ts @@ -3,21 +3,28 @@ import { Match } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import type { UpdateResult } from 'mongodb'; +import { notifyOnSubscriptionChangedByRoomId } from '../../../lib/server/lib/notifyListener'; + export const saveRoomCustomFields = async function (rid: string, roomCustomFields: Record): Promise { if (!Match.test(rid, String)) { throw new Meteor.Error('invalid-room', 'Invalid room', { function: 'RocketChat.saveRoomCustomFields', }); } + if (!Match.test(roomCustomFields, Object)) { throw new Meteor.Error('invalid-roomCustomFields-type', 'Invalid roomCustomFields type', { function: 'RocketChat.saveRoomCustomFields', }); } + const ret = await Rooms.setCustomFieldsById(rid, roomCustomFields); // Update customFields of any user's Subscription related with this rid - await Subscriptions.updateCustomFieldsByRoomId(rid, roomCustomFields); + const { modifiedCount } = await Subscriptions.updateCustomFieldsByRoomId(rid, roomCustomFields); + if (modifiedCount) { + void notifyOnSubscriptionChangedByRoomId(rid); + } return ret; }; diff --git a/apps/meteor/app/channel-settings/server/functions/saveRoomEncrypted.ts b/apps/meteor/app/channel-settings/server/functions/saveRoomEncrypted.ts index ed07540ba2b0..c1a441463a98 100644 --- a/apps/meteor/app/channel-settings/server/functions/saveRoomEncrypted.ts +++ b/apps/meteor/app/channel-settings/server/functions/saveRoomEncrypted.ts @@ -6,6 +6,8 @@ import { Match } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import type { UpdateResult } from 'mongodb'; +import { notifyOnSubscriptionChangedByRoomId } from '../../../lib/server/lib/notifyListener'; + export const saveRoomEncrypted = async function (rid: string, encrypted: boolean, user: IUser, sendMessage = true): Promise { if (!Match.test(rid, String)) { throw new Meteor.Error('invalid-room', 'Invalid room', { @@ -27,7 +29,10 @@ export const saveRoomEncrypted = async function (rid: string, encrypted: boolean } if (encrypted) { - await Subscriptions.disableAutoTranslateByRoomId(rid); + const { modifiedCount } = await Subscriptions.disableAutoTranslateByRoomId(rid); + if (modifiedCount) { + void notifyOnSubscriptionChangedByRoomId(rid); + } } return update; }; diff --git a/apps/meteor/app/channel-settings/server/functions/saveRoomName.ts b/apps/meteor/app/channel-settings/server/functions/saveRoomName.ts index c2af750ffa13..f4a5afbb6380 100644 --- a/apps/meteor/app/channel-settings/server/functions/saveRoomName.ts +++ b/apps/meteor/app/channel-settings/server/functions/saveRoomName.ts @@ -8,11 +8,17 @@ import type { Document, UpdateResult } from 'mongodb'; import { callbacks } from '../../../../lib/callbacks'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; import { checkUsernameAvailability } from '../../../lib/server/functions/checkUsernameAvailability'; -import { notifyOnIntegrationChangedByChannels } from '../../../lib/server/lib/notifyListener'; +import { notifyOnIntegrationChangedByChannels, notifyOnSubscriptionChangedByRoomId } from '../../../lib/server/lib/notifyListener'; import { getValidRoomName } from '../../../utils/server/lib/getValidRoomName'; const updateFName = async (rid: string, displayName: string): Promise<(UpdateResult | Document)[]> => { - return Promise.all([Rooms.setFnameById(rid, displayName), Subscriptions.updateFnameByRoomId(rid, displayName)]); + const responses = await Promise.all([Rooms.setFnameById(rid, displayName), Subscriptions.updateFnameByRoomId(rid, displayName)]); + + if (responses[1]?.modifiedCount) { + void notifyOnSubscriptionChangedByRoomId(rid); + } + + return responses; }; const updateRoomName = async (rid: string, displayName: string, slugifiedRoomName: string) => { @@ -24,10 +30,16 @@ const updateRoomName = async (rid: string, displayName: string, slugifiedRoomNam }); } - return Promise.all([ + const responses = await Promise.all([ Rooms.setNameById(rid, slugifiedRoomName, displayName), Subscriptions.updateNameAndAlertByRoomId(rid, slugifiedRoomName, displayName), ]); + + if (responses[1]?.modifiedCount) { + void notifyOnSubscriptionChangedByRoomId(rid); + } + + return responses; }; export async function saveRoomName( diff --git a/apps/meteor/app/channel-settings/server/functions/saveRoomType.ts b/apps/meteor/app/channel-settings/server/functions/saveRoomType.ts index e8a60d1ea0eb..4600d1d46a80 100644 --- a/apps/meteor/app/channel-settings/server/functions/saveRoomType.ts +++ b/apps/meteor/app/channel-settings/server/functions/saveRoomType.ts @@ -8,6 +8,7 @@ import type { UpdateResult, Document } from 'mongodb'; import { RoomSettingsEnum } from '../../../../definition/IRoomTypeConfig'; import { i18n } from '../../../../server/lib/i18n'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; +import { notifyOnSubscriptionChangedByRoomId } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; export const saveRoomType = async function ( @@ -41,11 +42,16 @@ export const saveRoomType = async function ( }); } - const result = (await Rooms.setTypeById(rid, roomType)) && (await Subscriptions.updateTypeByRoomId(rid, roomType)); + const result = await Promise.all([Rooms.setTypeById(rid, roomType), Subscriptions.updateTypeByRoomId(rid, roomType)]); + if (!result) { return result; } + if (result[1]?.modifiedCount) { + void notifyOnSubscriptionChangedByRoomId(rid); + } + if (sendMessage) { let message; if (roomType === 'c') { @@ -59,5 +65,6 @@ export const saveRoomType = async function ( } await Message.saveSystemMessage('room_changed_privacy', rid, message, user); } + return result; }; diff --git a/apps/meteor/app/e2e/server/functions/handleSuggestedGroupKey.ts b/apps/meteor/app/e2e/server/functions/handleSuggestedGroupKey.ts index 860051c04d4d..22eccf03f407 100644 --- a/apps/meteor/app/e2e/server/functions/handleSuggestedGroupKey.ts +++ b/apps/meteor/app/e2e/server/functions/handleSuggestedGroupKey.ts @@ -1,6 +1,8 @@ import { Rooms, Subscriptions } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; +import { notifyOnSubscriptionChangedById } from '../../../lib/server/lib/notifyListener'; + export async function handleSuggestedGroupKey( handle: 'accept' | 'reject', rid: string, @@ -30,5 +32,8 @@ export async function handleSuggestedGroupKey( await Rooms.addUserIdToE2EEQueueByRoomIds([sub.rid], userId); } - await Subscriptions.unsetGroupE2ESuggestedKey(sub._id); + const { modifiedCount } = await Subscriptions.unsetGroupE2ESuggestedKey(sub._id); + if (modifiedCount) { + void notifyOnSubscriptionChangedById(sub._id); + } } diff --git a/apps/meteor/app/e2e/server/methods/updateGroupKey.ts b/apps/meteor/app/e2e/server/methods/updateGroupKey.ts index 5764a021f54c..87182f723e7d 100644 --- a/apps/meteor/app/e2e/server/methods/updateGroupKey.ts +++ b/apps/meteor/app/e2e/server/methods/updateGroupKey.ts @@ -3,6 +3,7 @@ import { Subscriptions } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; +import { notifyOnSubscriptionChangedById, notifyOnSubscriptionChangedByRoomIdAndUserId } from '../../../lib/server/lib/notifyListener'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -25,12 +26,18 @@ Meteor.methods({ if (mySub) { // Setting the key to myself, can set directly to the final field if (userId === uid) { - await Subscriptions.setGroupE2EKey(mySub._id, key); + const setGroupE2EKeyResponse = await Subscriptions.setGroupE2EKey(mySub._id, key); + if (setGroupE2EKeyResponse.modifiedCount) { + void notifyOnSubscriptionChangedById(mySub._id); + } return; } // uid also has subscription to this room - await Subscriptions.setGroupE2ESuggestedKey(uid, rid, key); + const { modifiedCount } = await Subscriptions.setGroupE2ESuggestedKey(uid, rid, key); + if (modifiedCount) { + void notifyOnSubscriptionChangedByRoomIdAndUserId(rid, uid); + } } }, }); diff --git a/apps/meteor/app/federation/server/endpoints/dispatch.js b/apps/meteor/app/federation/server/endpoints/dispatch.js index 7090f053a22b..4f2a197b25ee 100644 --- a/apps/meteor/app/federation/server/endpoints/dispatch.js +++ b/apps/meteor/app/federation/server/endpoints/dispatch.js @@ -6,7 +6,13 @@ import EJSON from 'ejson'; import { API } from '../../../api/server'; import { FileUpload } from '../../../file-upload/server'; import { deleteRoom } from '../../../lib/server/functions/deleteRoom'; -import { notifyOnMessageChange, notifyOnRoomChanged, notifyOnRoomChangedById } from '../../../lib/server/lib/notifyListener'; +import { + notifyOnMessageChange, + notifyOnRoomChanged, + notifyOnRoomChangedById, + notifyOnSubscriptionChanged, + notifyOnSubscriptionChangedById, +} from '../../../lib/server/lib/notifyListener'; import { notifyUsersOnMessage } from '../../../lib/server/lib/notifyUsersOnMessage'; import { sendAllNotifications } from '../../../lib/server/lib/sendNotificationsOnMessage'; import { processThreads } from '../../../threads/server/hooks/aftersavemessage'; @@ -141,7 +147,10 @@ const eventHandlers = { const denormalizedSubscription = normalizers.denormalizeSubscription(subscription); // Create the subscription - await Subscriptions.insertOne(denormalizedSubscription); + const { insertedId } = await Subscriptions.insertOne(denormalizedSubscription); + if (insertedId) { + void notifyOnSubscriptionChangedById(insertedId); + } federationAltered = true; } } catch (ex) { @@ -176,7 +185,10 @@ const eventHandlers = { } = event; // Remove the user's subscription - await Subscriptions.removeByRoomIdAndUserId(roomId, user._id); + const deletedSubscription = await Subscriptions.removeByRoomIdAndUserId(roomId, user._id); + if (deletedSubscription) { + void notifyOnSubscriptionChanged(deletedSubscription, 'removed'); + } // Refresh the servers list await FederationServers.refreshServers(); @@ -204,7 +216,10 @@ const eventHandlers = { } = event; // Remove the user's subscription - await Subscriptions.removeByRoomIdAndUserId(roomId, user._id); + const deletedSubscription = await Subscriptions.removeByRoomIdAndUserId(roomId, user._id); + if (deletedSubscription) { + void notifyOnSubscriptionChanged(deletedSubscription, 'removed'); + } // Refresh the servers list await FederationServers.refreshServers(); diff --git a/apps/meteor/app/importer/server/classes/ImportDataConverter.ts b/apps/meteor/app/importer/server/classes/ImportDataConverter.ts index 7b1e71eaa0f0..6de47e33b2b6 100644 --- a/apps/meteor/app/importer/server/classes/ImportDataConverter.ts +++ b/apps/meteor/app/importer/server/classes/ImportDataConverter.ts @@ -28,7 +28,7 @@ import { generateUsernameSuggestion } from '../../../lib/server/functions/getUse import { insertMessage } from '../../../lib/server/functions/insertMessage'; import { saveUserIdentity } from '../../../lib/server/functions/saveUserIdentity'; import { setUserActiveStatus } from '../../../lib/server/functions/setUserActiveStatus'; -import { notifyOnUserChange } from '../../../lib/server/lib/notifyListener'; +import { notifyOnSubscriptionChangedByRoomId, notifyOnUserChange } from '../../../lib/server/lib/notifyListener'; import { createChannelMethod } from '../../../lib/server/methods/createChannel'; import { createPrivateGroupMethod } from '../../../lib/server/methods/createPrivateGroup'; import { getValidRoomName } from '../../../utils/server/lib/getValidRoomName'; @@ -1161,8 +1161,11 @@ export class ImportDataConverter { } async archiveRoomById(rid: string) { - await Rooms.archiveById(rid); - await Subscriptions.archiveByRoomId(rid); + const responses = await Promise.all([Rooms.archiveById(rid), Subscriptions.archiveByRoomId(rid)]); + + if (responses[1]?.modifiedCount) { + void notifyOnSubscriptionChangedByRoomId(rid); + } } async convertData(startedByUserId: string, callbacks: IConversionCallbacks = {}): Promise { diff --git a/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts b/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts index b6d977dc36e2..3fb9c419aa5f 100644 --- a/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts +++ b/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts @@ -5,6 +5,7 @@ import { Subscriptions } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; import { getSubscriptionAutotranslateDefaultConfig } from '../../../../server/lib/getSubscriptionAutotranslateDefaultConfig'; import { getDefaultSubscriptionPref } from '../../../utils/lib/getDefaultSubscriptionPref'; +import { notifyOnSubscriptionChangedById } from '../lib/notifyListener'; import { getDefaultChannels } from './getDefaultChannels'; export const addUserToDefaultChannels = async function (user: IUser, silenced?: boolean): Promise { @@ -14,8 +15,9 @@ export const addUserToDefaultChannels = async function (user: IUser, silenced?: for await (const room of defaultRooms) { if (!(await Subscriptions.findOneByRoomIdAndUserId(room._id, user._id, { projection: { _id: 1 } }))) { const autoTranslateConfig = getSubscriptionAutotranslateDefaultConfig(user); + // Add a subscription to this user - await Subscriptions.createWithRoomAndUser(room, user, { + const { insertedId } = await Subscriptions.createWithRoomAndUser(room, user, { ts: new Date(), open: true, alert: true, @@ -27,6 +29,10 @@ export const addUserToDefaultChannels = async function (user: IUser, silenced?: ...getDefaultSubscriptionPref(user), }); + if (insertedId) { + void notifyOnSubscriptionChangedById(insertedId, 'inserted'); + } + // Insert user joined message if (!silenced) { await Message.saveSystemMessage('uj', room._id, user.username || '', user); diff --git a/apps/meteor/app/lib/server/functions/addUserToRoom.ts b/apps/meteor/app/lib/server/functions/addUserToRoom.ts index b6ffc0ca4629..e6ca7b2a8b4d 100644 --- a/apps/meteor/app/lib/server/functions/addUserToRoom.ts +++ b/apps/meteor/app/lib/server/functions/addUserToRoom.ts @@ -11,7 +11,7 @@ import { getSubscriptionAutotranslateDefaultConfig } from '../../../../server/li import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; import { settings } from '../../../settings/server'; import { getDefaultSubscriptionPref } from '../../../utils/lib/getDefaultSubscriptionPref'; -import { notifyOnRoomChangedById } from '../lib/notifyListener'; +import { notifyOnRoomChangedById, notifyOnSubscriptionChangedById } from '../lib/notifyListener'; export const addUserToRoom = async function ( rid: string, @@ -82,7 +82,7 @@ export const addUserToRoom = async function ( const autoTranslateConfig = getSubscriptionAutotranslateDefaultConfig(userToBeAdded); - await Subscriptions.createWithRoomAndUser(room, userToBeAdded as IUser, { + const { insertedId } = await Subscriptions.createWithRoomAndUser(room, userToBeAdded as IUser, { ts: now, open: true, alert: !skipAlertSound, @@ -93,6 +93,10 @@ export const addUserToRoom = async function ( ...getDefaultSubscriptionPref(userToBeAdded as IUser), }); + if (insertedId) { + void notifyOnSubscriptionChangedById(insertedId, 'inserted'); + } + void notifyOnRoomChangedById(rid); if (!userToBeAdded.username) { diff --git a/apps/meteor/app/lib/server/functions/archiveRoom.ts b/apps/meteor/app/lib/server/functions/archiveRoom.ts index 3378d69f99ff..46fd7a1ac35b 100644 --- a/apps/meteor/app/lib/server/functions/archiveRoom.ts +++ b/apps/meteor/app/lib/server/functions/archiveRoom.ts @@ -3,11 +3,16 @@ import type { IMessage } from '@rocket.chat/core-typings'; import { Rooms, Subscriptions } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; -import { notifyOnRoomChanged } from '../lib/notifyListener'; +import { notifyOnRoomChanged, notifyOnSubscriptionChangedByRoomId } from '../lib/notifyListener'; export const archiveRoom = async function (rid: string, user: IMessage['u']): Promise { await Rooms.archiveById(rid); - await Subscriptions.archiveByRoomId(rid); + + const archiveResponse = await Subscriptions.archiveByRoomId(rid); + if (archiveResponse.modifiedCount) { + void notifyOnSubscriptionChangedByRoomId(rid); + } + await Message.saveSystemMessage('room-archived', rid, '', user); const room = await Rooms.findOneById(rid); diff --git a/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts b/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts index 2bfb1086c635..765a03cad87b 100644 --- a/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts +++ b/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts @@ -4,7 +4,7 @@ import { Messages, Rooms, Subscriptions, ReadReceipts, Users } from '@rocket.cha import { i18n } from '../../../../server/lib/i18n'; import { FileUpload } from '../../../file-upload/server'; -import { notifyOnRoomChangedById } from '../lib/notifyListener'; +import { notifyOnRoomChangedById, notifyOnSubscriptionChangedById } from '../lib/notifyListener'; import { deleteRoom } from './deleteRoom'; export async function cleanRoomHistory({ @@ -75,6 +75,7 @@ export async function cleanRoomHistory({ if (!ignoreThreads) { const threads = new Set(); + await Messages.findThreadsByRoomIdPinnedTimestampAndUsers( { rid, pinned: excludePinned, ignoreDiscussion, ts, users: fromUsers }, { projection: { _id: 1 } }, @@ -83,7 +84,14 @@ export async function cleanRoomHistory({ }); if (threads.size > 0) { - await Subscriptions.removeUnreadThreadsByRoomId(rid, [...threads]); + const subscriptionIds: string[] = ( + await Subscriptions.findUnreadThreadsByRoomId(rid, [...threads], { projection: { _id: 1 } }).toArray() + ).map(({ _id }) => _id); + + const { modifiedCount } = await Subscriptions.removeUnreadThreadsByRoomId(rid, [...threads]); + if (modifiedCount) { + subscriptionIds.forEach((id) => notifyOnSubscriptionChangedById(id)); + } } } diff --git a/apps/meteor/app/lib/server/functions/closeLivechatRoom.ts b/apps/meteor/app/lib/server/functions/closeLivechatRoom.ts index b716be044d57..263b137ae00c 100644 --- a/apps/meteor/app/lib/server/functions/closeLivechatRoom.ts +++ b/apps/meteor/app/lib/server/functions/closeLivechatRoom.ts @@ -5,6 +5,7 @@ import { LivechatRooms, Subscriptions } from '@rocket.chat/models'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import type { CloseRoomParams } from '../../../livechat/server/lib/LivechatTyped'; import { Livechat } from '../../../livechat/server/lib/LivechatTyped'; +import { notifyOnSubscriptionChanged } from '../lib/notifyListener'; export const closeLivechatRoom = async ( user: IUser, @@ -34,9 +35,12 @@ export const closeLivechatRoom = async ( } if (!room.open) { - const subscriptionsLeft = await Subscriptions.countByRoomId(roomId); - if (subscriptionsLeft) { - await Subscriptions.removeByRoomId(roomId); + const { deletedCount } = await Subscriptions.removeByRoomId(roomId, { + async onTrash(doc) { + void notifyOnSubscriptionChanged(doc, 'removed'); + }, + }); + if (deletedCount) { return; } throw new Error('error-room-already-closed'); diff --git a/apps/meteor/app/lib/server/functions/createDirectRoom.ts b/apps/meteor/app/lib/server/functions/createDirectRoom.ts index 67c6328f38f4..f77ee1f55901 100644 --- a/apps/meteor/app/lib/server/functions/createDirectRoom.ts +++ b/apps/meteor/app/lib/server/functions/createDirectRoom.ts @@ -11,7 +11,7 @@ import { callbacks } from '../../../../lib/callbacks'; import { isTruthy } from '../../../../lib/isTruthy'; import { settings } from '../../../settings/server'; import { getDefaultSubscriptionPref } from '../../../utils/lib/getDefaultSubscriptionPref'; -import { notifyOnRoomChangedById } from '../lib/notifyListener'; +import { notifyOnRoomChangedById, notifyOnSubscriptionChangedByRoomIdAndUserId } from '../lib/notifyListener'; const generateSubscription = ( fname: string, @@ -135,7 +135,7 @@ export async function createDirectRoom( if (roomMembers.length === 1) { // dm to yourself - await Subscriptions.updateOne( + const { modifiedCount, upsertedCount } = await Subscriptions.updateOne( { rid, 'u._id': roomMembers[0]._id }, { $set: { open: true }, @@ -146,6 +146,9 @@ export async function createDirectRoom( }, { upsert: true }, ); + if (modifiedCount || upsertedCount) { + void notifyOnSubscriptionChangedByRoomIdAndUserId(rid, roomMembers[0]._id, modifiedCount ? 'updated' : 'inserted'); + } } else { const memberIds = roomMembers.map((member) => member._id); const membersWithPreferences: IUser[] = await Users.find( @@ -155,7 +158,7 @@ export async function createDirectRoom( for await (const member of membersWithPreferences) { const otherMembers = sortedMembers.filter(({ _id }) => _id !== member._id); - await Subscriptions.updateOne( + const { modifiedCount, upsertedCount } = await Subscriptions.updateOne( { rid, 'u._id': member._id }, { ...(options?.creator === member._id && { $set: { open: true } }), @@ -166,6 +169,9 @@ export async function createDirectRoom( }, { upsert: true }, ); + if (modifiedCount || upsertedCount) { + void notifyOnSubscriptionChangedByRoomIdAndUserId(rid, member._id, modifiedCount ? 'updated' : 'inserted'); + } } } diff --git a/apps/meteor/app/lib/server/functions/createRoom.ts b/apps/meteor/app/lib/server/functions/createRoom.ts index b339155775e6..769155b66b60 100644 --- a/apps/meteor/app/lib/server/functions/createRoom.ts +++ b/apps/meteor/app/lib/server/functions/createRoom.ts @@ -12,7 +12,7 @@ import { beforeCreateRoomCallback } from '../../../../lib/callbacks/beforeCreate import { getSubscriptionAutotranslateDefaultConfig } from '../../../../server/lib/getSubscriptionAutotranslateDefaultConfig'; import { getDefaultSubscriptionPref } from '../../../utils/lib/getDefaultSubscriptionPref'; import { getValidRoomName } from '../../../utils/server/lib/getValidRoomName'; -import { notifyOnRoomChanged } from '../lib/notifyListener'; +import { notifyOnRoomChanged, notifyOnSubscriptionChangedById } from '../lib/notifyListener'; import { createDirectRoom } from './createDirectRoom'; const isValidName = (name: unknown): name is string => { @@ -47,7 +47,11 @@ async function createUsersSubscriptions({ ...getDefaultSubscriptionPref(owner), }; - await Subscriptions.createWithRoomAndUser(room, owner, extra); + const { insertedId } = await Subscriptions.createWithRoomAndUser(room, owner, extra); + + if (insertedId) { + await notifyOnRoomChanged(room, 'inserted'); + } return; } @@ -98,7 +102,9 @@ async function createUsersSubscriptions({ await Users.addRoomByUserIds(memberIds, room._id); } - await Subscriptions.createWithRoomAndManyUsers(room, subs); + const { insertedIds } = await Subscriptions.createWithRoomAndManyUsers(room, subs); + + Object.values(insertedIds).forEach((subId) => notifyOnSubscriptionChangedById(subId, 'inserted')); await Rooms.incUsersCountById(room._id, subs.length); } diff --git a/apps/meteor/app/lib/server/functions/deleteRoom.ts b/apps/meteor/app/lib/server/functions/deleteRoom.ts index a605d82b08c2..386ba8da8b94 100644 --- a/apps/meteor/app/lib/server/functions/deleteRoom.ts +++ b/apps/meteor/app/lib/server/functions/deleteRoom.ts @@ -2,16 +2,27 @@ import { Messages, Rooms, Subscriptions } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; import { FileUpload } from '../../../file-upload/server'; -import { notifyOnRoomChangedById } from '../lib/notifyListener'; +import { notifyOnRoomChangedById, notifyOnSubscriptionChanged } from '../lib/notifyListener'; export const deleteRoom = async function (rid: string): Promise { await FileUpload.removeFilesByRoomId(rid); + await Messages.removeByRoomId(rid); + await callbacks.run('beforeDeleteRoom', rid); - await Subscriptions.removeByRoomId(rid); + + await Subscriptions.removeByRoomId(rid, { + async onTrash(doc) { + void notifyOnSubscriptionChanged(doc, 'removed'); + }, + }); + await FileUpload.getStore('Avatars').deleteByRoomId(rid); + await callbacks.run('afterDeleteRoom', rid); - await Rooms.removeById(rid); - void notifyOnRoomChangedById(rid, 'removed'); + const { deletedCount } = await Rooms.removeById(rid); + if (deletedCount) { + void notifyOnRoomChangedById(rid, 'removed'); + } }; diff --git a/apps/meteor/app/lib/server/functions/deleteUser.ts b/apps/meteor/app/lib/server/functions/deleteUser.ts index e66c8c2d5eef..483085d40811 100644 --- a/apps/meteor/app/lib/server/functions/deleteUser.ts +++ b/apps/meteor/app/lib/server/functions/deleteUser.ts @@ -112,7 +112,7 @@ export async function deleteUser(userId: string, confirmRelinquish = false, dele const rids = subscribedRooms.map((room) => room.rid); void notifyOnRoomChangedById(rids); - await Subscriptions.removeByUserId(userId); // Remove user subscriptions + await Subscriptions.removeByUserId(userId); // Remove user as livechat agent if (user.roles.includes('livechat-agent')) { diff --git a/apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts b/apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts index 75b232462077..8f1981ca386d 100644 --- a/apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts +++ b/apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts @@ -1,6 +1,7 @@ import { Messages, Roles, Rooms, Subscriptions, ReadReceipts } from '@rocket.chat/models'; import { FileUpload } from '../../../file-upload/server'; +import { notifyOnSubscriptionChanged } from '../lib/notifyListener'; import type { SubscribedRoomsForUserWithDetails } from './getRoomsWithSingleOwner'; const bulkRoomCleanUp = async (rids: string[]): Promise => { @@ -8,7 +9,11 @@ const bulkRoomCleanUp = async (rids: string[]): Promise => { await Promise.all(rids.map((rid) => FileUpload.removeFilesByRoomId(rid))); return Promise.all([ - Subscriptions.removeByRoomIds(rids), + Subscriptions.removeByRoomIds(rids, { + async onTrash(doc) { + void notifyOnSubscriptionChanged(doc, 'removed'); + }, + }), Messages.removeByRoomIds(rids), ReadReceipts.removeByRoomIds(rids), Rooms.removeByIds(rids), diff --git a/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts b/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts index 686fdd9c0317..5800cb68af81 100644 --- a/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts +++ b/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts @@ -8,7 +8,7 @@ import { Meteor } from 'meteor/meteor'; import { afterLeaveRoomCallback } from '../../../../lib/callbacks/afterLeaveRoomCallback'; import { beforeLeaveRoomCallback } from '../../../../lib/callbacks/beforeLeaveRoomCallback'; import { settings } from '../../../settings/server'; -import { notifyOnRoomChangedById } from '../lib/notifyListener'; +import { notifyOnRoomChangedById, notifyOnSubscriptionChanged } from '../lib/notifyListener'; export const removeUserFromRoom = async function (rid: string, user: IUser, options?: { byUser: IUser }): Promise { const room = await Rooms.findOneById(rid); @@ -59,7 +59,10 @@ export const removeUserFromRoom = async function (rid: string, user: IUser, opti await Message.saveSystemMessage('command', rid, 'survey', user); } - await Subscriptions.removeByRoomIdAndUserId(rid, user._id); + const deletedSubscription = await Subscriptions.removeByRoomIdAndUserId(rid, user._id); + if (deletedSubscription) { + void notifyOnSubscriptionChanged(deletedSubscription, 'removed'); + } if (room.teamId && room.teamMain) { await Team.removeMember(room.teamId, user._id); diff --git a/apps/meteor/app/lib/server/functions/saveCustomFieldsWithoutValidation.ts b/apps/meteor/app/lib/server/functions/saveCustomFieldsWithoutValidation.ts index 4a0ac005e55c..5383048f13bd 100644 --- a/apps/meteor/app/lib/server/functions/saveCustomFieldsWithoutValidation.ts +++ b/apps/meteor/app/lib/server/functions/saveCustomFieldsWithoutValidation.ts @@ -5,6 +5,7 @@ import type { UpdateFilter } from 'mongodb'; import { trim } from '../../../../lib/utils/stringUtils'; import { settings } from '../../../settings/server'; +import { notifyOnSubscriptionChangedByUserIdAndRoomType } from '../lib/notifyListener'; export const saveCustomFieldsWithoutValidation = async function (userId: string, formData: Record): Promise { if (trim(settings.get('Accounts_CustomFields')) !== '') { @@ -22,7 +23,10 @@ export const saveCustomFieldsWithoutValidation = async function (userId: string, await Users.setCustomFields(userId, customFields); // Update customFields of all Direct Messages' Rooms for userId - await Subscriptions.setCustomFieldsDirectMessagesByUserId(userId, customFields); + const setCustomFieldsResponse = await Subscriptions.setCustomFieldsDirectMessagesByUserId(userId, customFields); + if (setCustomFieldsResponse.modifiedCount) { + void notifyOnSubscriptionChangedByUserIdAndRoomType(userId, 'd'); + } for await (const fieldName of Object.keys(customFields)) { if (!customFieldsMeta[fieldName].modifyRecordField) { diff --git a/apps/meteor/app/lib/server/functions/saveUserIdentity.ts b/apps/meteor/app/lib/server/functions/saveUserIdentity.ts index 0b9ff21e53e3..1729a1ba8abd 100644 --- a/apps/meteor/app/lib/server/functions/saveUserIdentity.ts +++ b/apps/meteor/app/lib/server/functions/saveUserIdentity.ts @@ -3,7 +3,11 @@ import { Messages, VideoConference, LivechatDepartmentAgents, Rooms, Subscriptio import { SystemLogger } from '../../../../server/lib/logger/system'; import { FileUpload } from '../../../file-upload/server'; -import { notifyOnRoomChangedByUsernamesOrUids } from '../lib/notifyListener'; +import { + notifyOnRoomChangedByUsernamesOrUids, + notifyOnSubscriptionChangedByUserId, + notifyOnSubscriptionChangedByNameAndRoomType, +} from '../lib/notifyListener'; import { _setRealName } from './setRealName'; import { _setUsername } from './setUsername'; import { updateGroupDMsName } from './updateGroupDMsName'; @@ -129,20 +133,38 @@ async function updateUsernameReferences({ await Messages.updateUsernameAndMessageOfMentionByIdAndOldUsername(msg._id, previousUsername, username, updatedMsg); } - await Rooms.replaceUsername(previousUsername, username); - await Rooms.replaceMutedUsername(previousUsername, username); - await Rooms.replaceUsernameOfUserByUserId(user._id, username); - await Subscriptions.setUserUsernameByUserId(user._id, username); + const responses = await Promise.all([ + Rooms.replaceUsername(previousUsername, username), + Rooms.replaceMutedUsername(previousUsername, username), + Rooms.replaceUsernameOfUserByUserId(user._id, username), + Subscriptions.setUserUsernameByUserId(user._id, username), + LivechatDepartmentAgents.replaceUsernameOfAgentByUserId(user._id, username), + ]); - await LivechatDepartmentAgents.replaceUsernameOfAgentByUserId(user._id, username); + if (responses[3]?.modifiedCount) { + void notifyOnSubscriptionChangedByUserId(user._id); + } - void notifyOnRoomChangedByUsernamesOrUids([user._id], [previousUsername, username]); + if (responses[0]?.modifiedCount || responses[1]?.modifiedCount || responses[2]?.modifiedCount) { + void notifyOnRoomChangedByUsernamesOrUids([user._id], [previousUsername, username]); + } } // update other references if either the name or username has changed if (usernameChanged || nameChanged) { // update name and fname of 1-on-1 direct messages - await Subscriptions.updateDirectNameAndFnameByName(previousUsername, rawUsername && username, rawName && name); + const updateDirectNameResponse = await Subscriptions.updateDirectNameAndFnameByName( + previousUsername, + rawUsername && username, + rawName && name, + ); + + if (updateDirectNameResponse?.modifiedCount) { + void notifyOnSubscriptionChangedByNameAndRoomType({ + t: 'd', + name: username, + }); + } // update name and fname of group direct messages await updateGroupDMsName(user); diff --git a/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts b/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts index 9d7a3e113fc4..929c24210d2d 100644 --- a/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts +++ b/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts @@ -9,7 +9,12 @@ import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../../lib/callbacks'; import * as Mailer from '../../../mailer/server/api'; import { settings } from '../../../settings/server'; -import { notifyOnRoomChangedById, notifyOnRoomChangedByUserDM, notifyOnUserChange } from '../lib/notifyListener'; +import { + notifyOnRoomChangedById, + notifyOnRoomChangedByUserDM, + notifyOnSubscriptionChangedByNameAndRoomType, + notifyOnUserChange, +} from '../lib/notifyListener'; import { closeOmnichannelConversations } from './closeOmnichannelConversations'; import { shouldRemoveOrChangeOwner, getSubscribedRoomsForUserWithDetails } from './getRoomsWithSingleOwner'; import { getUserSingleOwnedRooms } from './getUserSingleOwnedRooms'; @@ -39,8 +44,10 @@ async function reactivateDirectConversations(userId: string) { return acc; }, []); - await Rooms.setDmReadOnlyByUserId(userId, roomsToReactivate, false, false); - void notifyOnRoomChangedById(roomsToReactivate); + const setDmReadOnlyResponse = await Rooms.setDmReadOnlyByUserId(userId, roomsToReactivate, false, false); + if (setDmReadOnlyResponse.modifiedCount) { + void notifyOnRoomChangedById(roomsToReactivate); + } } export async function setUserActiveStatus(userId: string, active: boolean, confirmRelinquish = false): Promise { @@ -118,7 +125,10 @@ export async function setUserActiveStatus(userId: string, active: boolean, confi } if (user.username) { - await Subscriptions.setArchivedByUsername(user.username, !active); + const { modifiedCount } = await Subscriptions.setArchivedByUsername(user.username, !active); + if (modifiedCount) { + void notifyOnSubscriptionChangedByNameAndRoomType({ t: 'd', name: user.username }); + } } if (active === false) { diff --git a/apps/meteor/app/lib/server/functions/unarchiveRoom.ts b/apps/meteor/app/lib/server/functions/unarchiveRoom.ts index 7db86ed933a3..699f9c3701b1 100644 --- a/apps/meteor/app/lib/server/functions/unarchiveRoom.ts +++ b/apps/meteor/app/lib/server/functions/unarchiveRoom.ts @@ -2,11 +2,16 @@ import { Message } from '@rocket.chat/core-services'; import type { IMessage } from '@rocket.chat/core-typings'; import { Rooms, Subscriptions } from '@rocket.chat/models'; -import { notifyOnRoomChangedById } from '../lib/notifyListener'; +import { notifyOnRoomChangedById, notifyOnSubscriptionChangedByRoomId } from '../lib/notifyListener'; export const unarchiveRoom = async function (rid: string, user: IMessage['u']): Promise { await Rooms.unarchiveById(rid); - await Subscriptions.unarchiveByRoomId(rid); + + const unarchiveResponse = await Subscriptions.unarchiveByRoomId(rid); + if (unarchiveResponse.modifiedCount) { + void notifyOnSubscriptionChangedByRoomId(rid); + } + await Message.saveSystemMessage('room-unarchived', rid, '', user); void notifyOnRoomChangedById(rid); diff --git a/apps/meteor/app/lib/server/functions/updateGroupDMsName.ts b/apps/meteor/app/lib/server/functions/updateGroupDMsName.ts index a0ad2eedcf55..feb26ce6a1b0 100644 --- a/apps/meteor/app/lib/server/functions/updateGroupDMsName.ts +++ b/apps/meteor/app/lib/server/functions/updateGroupDMsName.ts @@ -1,6 +1,8 @@ import type { IUser } from '@rocket.chat/core-typings'; import { Rooms, Subscriptions, Users } from '@rocket.chat/models'; +import { notifyOnSubscriptionChangedByRoomId } from '../lib/notifyListener'; + const getFname = (members: IUser[]): string => members.map(({ name, username }) => name || username).join(', '); const getName = (members: IUser[]): string => members.map(({ username }) => username).join(','); @@ -63,7 +65,10 @@ export const updateGroupDMsName = async (userThatChangedName: IUser): Promise _id !== sub.u._id); - await Subscriptions.updateNameAndFnameById(sub._id, getName(otherMembers), getFname(otherMembers)); + const updateNameRespose = await Subscriptions.updateNameAndFnameById(sub._id, getName(otherMembers), getFname(otherMembers)); + if (updateNameRespose.modifiedCount) { + void notifyOnSubscriptionChangedByRoomId(room._id); + } } } }; diff --git a/apps/meteor/app/lib/server/lib/notifyListener.ts b/apps/meteor/app/lib/server/lib/notifyListener.ts index 934742945f2d..778fe89dbbf4 100644 --- a/apps/meteor/app/lib/server/lib/notifyListener.ts +++ b/apps/meteor/app/lib/server/lib/notifyListener.ts @@ -15,6 +15,7 @@ import type { IEmailInbox, IIntegrationHistory, AtLeast, + ISubscription, ISettingColor, IUser, IMessage, @@ -30,6 +31,7 @@ import { Integrations, LoginServiceConfiguration, IntegrationHistory, + Subscriptions, LivechatInquiry, LivechatDepartmentAgents, Users, @@ -37,6 +39,7 @@ import { } from '@rocket.chat/models'; import mem from 'mem'; +import { subscriptionFields } from '../../../../lib/publishFields'; import { shouldHideSystemMessage } from '../../../../server/lib/systemMessage/hideSystemMessage'; type ClientAction = 'inserted' | 'updated' | 'removed'; @@ -467,3 +470,119 @@ export const notifyOnMessageChange = withDbWatcherCheck(async ({ id, data }: { i } void api.broadcast('watch.messages', { message }); }); + +export const notifyOnSubscriptionChanged = withDbWatcherCheck( + async (subscription: ISubscription, clientAction: ClientAction = 'updated'): Promise => { + void api.broadcast('watch.subscriptions', { clientAction, subscription }); + }, +); + +export const notifyOnSubscriptionChangedByRoomIdAndUserId = withDbWatcherCheck( + async ( + rid: ISubscription['rid'], + uid: ISubscription['u']['_id'], + clientAction: Exclude = 'updated', + ): Promise => { + const cursor = Subscriptions.findByUserIdAndRoomIds(uid, [rid], { projection: subscriptionFields }); + + void cursor.forEach((subscription) => { + void api.broadcast('watch.subscriptions', { clientAction, subscription }); + }); + }, +); + +export const notifyOnSubscriptionChangedById = withDbWatcherCheck( + async (id: ISubscription['_id'], clientAction: Exclude = 'updated'): Promise => { + const subscription = await Subscriptions.findOneById(id); + if (!subscription) { + return; + } + + void api.broadcast('watch.subscriptions', { clientAction, subscription }); + }, +); + +export const notifyOnSubscriptionChangedByUserPreferences = withDbWatcherCheck( + async ( + uid: ISubscription['u']['_id'], + notificationOriginField: keyof ISubscription, + originFieldNotEqualValue: 'user' | 'subscription', + clientAction: Exclude = 'updated', + ): Promise => { + const cursor = Subscriptions.findByUserPreferences(uid, notificationOriginField, originFieldNotEqualValue, { + projection: subscriptionFields, + }); + + void cursor.forEach((subscription) => { + void api.broadcast('watch.subscriptions', { clientAction, subscription }); + }); + }, +); + +export const notifyOnSubscriptionChangedByRoomId = withDbWatcherCheck( + async (rid: ISubscription['rid'], clientAction: Exclude = 'updated'): Promise => { + const cursor = Subscriptions.findByRoomId(rid, { projection: subscriptionFields }); + + void cursor.forEach((subscription) => { + void api.broadcast('watch.subscriptions', { clientAction, subscription }); + }); + }, +); + +export const notifyOnSubscriptionChangedByAutoTranslateAndUserId = withDbWatcherCheck( + async (uid: ISubscription['u']['_id'], clientAction: Exclude = 'updated'): Promise => { + const cursor = Subscriptions.findByAutoTranslateAndUserId(uid, true, { projection: subscriptionFields }); + + void cursor.forEach((subscription) => { + void api.broadcast('watch.subscriptions', { clientAction, subscription }); + }); + }, +); + +export const notifyOnSubscriptionChangedByUserIdAndRoomType = withDbWatcherCheck( + async ( + uid: ISubscription['u']['_id'], + t: ISubscription['t'], + clientAction: Exclude = 'updated', + ): Promise => { + const cursor = Subscriptions.findByUserIdAndRoomType(uid, t, { projection: subscriptionFields }); + + void cursor.forEach((subscription) => { + void api.broadcast('watch.subscriptions', { clientAction, subscription }); + }); + }, +); + +export const notifyOnSubscriptionChangedByNameAndRoomType = withDbWatcherCheck( + async (filter: Partial>, clientAction: Exclude = 'updated'): Promise => { + const cursor = Subscriptions.findByNameAndRoomType(filter, { projection: subscriptionFields }); + + void cursor.forEach((subscription) => { + void api.broadcast('watch.subscriptions', { clientAction, subscription }); + }); + }, +); + +export const notifyOnSubscriptionChangedByUserId = withDbWatcherCheck( + async (uid: ISubscription['u']['_id'], clientAction: Exclude = 'updated'): Promise => { + const cursor = Subscriptions.findByUserId(uid, { projection: subscriptionFields }); + + void cursor.forEach((subscription) => { + void api.broadcast('watch.subscriptions', { clientAction, subscription }); + }); + }, +); + +export const notifyOnSubscriptionChangedByRoomIdAndUserIds = withDbWatcherCheck( + async ( + rid: ISubscription['rid'], + uids: ISubscription['u']['_id'][], + clientAction: Exclude = 'updated', + ): Promise => { + const cursor = Subscriptions.findByRoomIdAndUserIds(rid, uids, { projection: subscriptionFields }); + + void cursor.forEach((subscription) => { + void api.broadcast('watch.subscriptions', { clientAction, subscription }); + }); + }, +); diff --git a/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts b/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts index 85f2ac52b702..7551cabb6e63 100644 --- a/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts +++ b/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts @@ -7,6 +7,11 @@ import moment from 'moment'; import { callbacks } from '../../../../lib/callbacks'; import { settings } from '../../../settings/server'; +import { + notifyOnSubscriptionChanged, + notifyOnSubscriptionChangedByRoomIdAndUserId, + notifyOnSubscriptionChangedByRoomIdAndUserIds, +} from './notifyListener'; function messageContainsHighlight(message: IMessage, highlights: string[]): boolean { if (!highlights || highlights.length === 0) return false; @@ -51,26 +56,14 @@ export async function getMentions(message: IMessage): Promise<{ toAll: boolean; type UnreadCountType = 'all_messages' | 'user_mentions_only' | 'group_mentions_only' | 'user_and_group_mentions_only'; -const incGroupMentions = async ( - rid: IRoom['_id'], - roomType: RoomType, - excludeUserId: IUser['_id'], - unreadCount: Exclude, -): Promise => { +const getGroupMentions = (roomType: RoomType, unreadCount: Exclude): number => { const incUnreadByGroup = ['all_messages', 'group_mentions_only', 'user_and_group_mentions_only'].includes(unreadCount); - const incUnread = roomType === 'd' || roomType === 'l' || incUnreadByGroup ? 1 : 0; - await Subscriptions.incGroupMentionsAndUnreadForRoomIdExcludingUserId(rid, excludeUserId, 1, incUnread); + return roomType === 'd' || roomType === 'l' || incUnreadByGroup ? 1 : 0; }; -const incUserMentions = async ( - rid: IRoom['_id'], - roomType: RoomType, - uids: IUser['_id'][], - unreadCount: Exclude, -): Promise => { - const incUnreadByUser = new Set(['all_messages', 'user_mentions_only', 'user_and_group_mentions_only']).has(unreadCount); - const incUnread = roomType === 'd' || roomType === 'l' || incUnreadByUser ? 1 : 0; - await Subscriptions.incUserMentionsAndUnreadForRoomIdAndUserIds(rid, uids, 1, incUnread); +const getUserMentions = (roomType: RoomType, unreadCount: Exclude): number => { + const incUnreadByUser = ['all_messages', 'user_mentions_only', 'user_and_group_mentions_only'].includes(unreadCount); + return roomType === 'd' || roomType === 'l' || incUnreadByUser ? 1 : 0; }; export const getUserIdsFromHighlights = async (rid: IRoom['_id'], message: IMessage): Promise => { @@ -101,45 +94,77 @@ const getUnreadSettingCount = (roomType: RoomType): UnreadCountType => { }; async function updateUsersSubscriptions(message: IMessage, room: IRoom): Promise { - // Don't increase unread counter on thread messages - if (room != null && !message.tmid) { - const { toAll, toHere, mentionIds } = await getMentions(message); - - const userIds = new Set(mentionIds); - - const unreadCount = getUnreadSettingCount(room.t); + if (!room || message.tmid) { + return; + } - (await getUserIdsFromHighlights(room._id, message)).forEach((uid) => userIds.add(uid)); + const [mentions, highlightIds] = await Promise.all([getMentions(message), getUserIdsFromHighlights(room._id, message)]); + + const { toAll, toHere, mentionIds } = mentions; + const userIds = [...new Set([...mentionIds, ...highlightIds])]; + const unreadCount = getUnreadSettingCount(room.t); + + const userMentionInc = getUserMentions(room.t, unreadCount as Exclude); + const groupMentionInc = getGroupMentions(room.t, unreadCount as Exclude); + + void Subscriptions.findByRoomIdAndNotAlertOrOpenExcludingUserIds({ + roomId: room._id, + uidsExclude: [message.u._id], + uidsInclude: userIds, + onlyRead: !toAll && !toHere, + }).forEach((sub) => { + const hasUserMention = userIds.includes(sub.u._id); + const shouldIncUnread = hasUserMention || toAll || toHere || unreadCount === 'all_messages'; + void notifyOnSubscriptionChanged( + { + ...sub, + alert: true, + open: true, + ...(shouldIncUnread && { unread: sub.unread + 1 }), + ...(hasUserMention && { userMentions: sub.userMentions + 1 }), + ...((toAll || toHere) && { groupMentions: sub.groupMentions + 1 }), + }, + 'updated', + ); + }); - // Give priority to user mentions over group mentions - if (userIds.size > 0) { - await incUserMentions(room._id, room.t, [...userIds], unreadCount as Exclude); - } else if (toAll || toHere) { - await incGroupMentions(room._id, room.t, message.u._id, unreadCount as Exclude); - } + // Give priority to user mentions over group mentions + if (userIds.length) { + await Subscriptions.incUserMentionsAndUnreadForRoomIdAndUserIds(room._id, userIds, 1, userMentionInc); + } else if (toAll || toHere) { + await Subscriptions.incGroupMentionsAndUnreadForRoomIdExcludingUserId(room._id, message.u._id, 1, groupMentionInc); + } - // this shouldn't run only if has group mentions because it will already exclude mentioned users from the query - if (!toAll && !toHere && unreadCount === 'all_messages') { - await Subscriptions.incUnreadForRoomIdExcludingUserIds(room._id, [...userIds, message.u._id], 1); - } + if (!toAll && !toHere && unreadCount === 'all_messages') { + await Subscriptions.incUnreadForRoomIdExcludingUserIds(room._id, [...userIds, message.u._id], 1); } - // Update all other subscriptions to alert their owners but without incrementing - // the unread counter, as it is only for mentions and direct messages - // We now set alert and open properties in two separate update commands. This proved to be more efficient on MongoDB - because it uses a more efficient index. + // update subscriptions of other members of the room await Promise.all([ Subscriptions.setAlertForRoomIdExcludingUserId(message.rid, message.u._id), Subscriptions.setOpenForRoomIdExcludingUserId(message.rid, message.u._id), ]); + + // update subscription of the message sender + await Subscriptions.setAsReadByRoomIdAndUserId(message.rid, message.u._id); + const setAsReadResponse = await Subscriptions.setAsReadByRoomIdAndUserId(message.rid, message.u._id); + if (setAsReadResponse.modifiedCount) { + void notifyOnSubscriptionChangedByRoomIdAndUserId(message.rid, message.u._id); + } } export async function updateThreadUsersSubscriptions(message: IMessage, replies: IUser['_id'][]): Promise { // Don't increase unread counter on thread messages - - await Subscriptions.setAlertForRoomIdAndUserIds(message.rid, replies); const repliesPlusSender = [...new Set([message.u._id, ...replies])]; - await Subscriptions.setOpenForRoomIdAndUserIds(message.rid, repliesPlusSender); - await Subscriptions.setLastReplyForRoomIdAndUserIds(message.rid, repliesPlusSender, new Date()); + + const responses = await Promise.all([ + Subscriptions.setAlertForRoomIdAndUserIds(message.rid, replies), + Subscriptions.setOpenForRoomIdAndUserIds(message.rid, repliesPlusSender), + Subscriptions.setLastReplyForRoomIdAndUserIds(message.rid, repliesPlusSender, new Date()), + ]); + + responses.some((response) => response?.modifiedCount) && + void notifyOnSubscriptionChangedByRoomIdAndUserIds(message.rid, repliesPlusSender); } export async function notifyUsersOnMessage(message: IMessage, room: IRoom, roomUpdater: Updater): Promise { diff --git a/apps/meteor/app/lib/server/methods/blockUser.ts b/apps/meteor/app/lib/server/methods/blockUser.ts index b967e35d7bc1..7fe6ec803dd1 100644 --- a/apps/meteor/app/lib/server/methods/blockUser.ts +++ b/apps/meteor/app/lib/server/methods/blockUser.ts @@ -5,6 +5,7 @@ import { Meteor } from 'meteor/meteor'; import { RoomMemberActions } from '../../../../definition/IRoomTypeConfig'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; +import { notifyOnSubscriptionChangedByRoomIdAndUserIds } from '../lib/notifyListener'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -33,14 +34,22 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'blockUser' }); } - const subscription = await Subscriptions.findOneByRoomIdAndUserId(rid, userId); - const subscription2 = await Subscriptions.findOneByRoomIdAndUserId(rid, blocked); + const [blockedUser, blockerUser] = await Promise.all([ + Subscriptions.findOneByRoomIdAndUserId(rid, blocked, { projection: { _id: 1 } }), + Subscriptions.findOneByRoomIdAndUserId(rid, userId, { projection: { _id: 1 } }), + ]); - if (!subscription || !subscription2) { + if (!blockedUser || !blockerUser) { throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'blockUser' }); } - await Subscriptions.setBlockedByRoomId(rid, blocked, userId); + const [blockedResponse, blockerResponse] = await Subscriptions.setBlockedByRoomId(rid, blocked, userId); + + const listenerUsers = [...(blockedResponse?.modifiedCount ? [blocked] : []), ...(blockerResponse?.modifiedCount ? [userId] : [])]; + + if (listenerUsers.length) { + void notifyOnSubscriptionChangedByRoomIdAndUserIds(rid, listenerUsers); + } return true; }, diff --git a/apps/meteor/app/lib/server/methods/unblockUser.ts b/apps/meteor/app/lib/server/methods/unblockUser.ts index 2eec5a082109..7b4bc5660010 100644 --- a/apps/meteor/app/lib/server/methods/unblockUser.ts +++ b/apps/meteor/app/lib/server/methods/unblockUser.ts @@ -3,6 +3,8 @@ import { Subscriptions } from '@rocket.chat/models'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; +import { notifyOnSubscriptionChangedByRoomIdAndUserIds } from '../lib/notifyListener'; + declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { @@ -20,14 +22,22 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'blockUser' }); } - const subscription = await Subscriptions.findOneByRoomIdAndUserId(rid, userId); - const subscription2 = await Subscriptions.findOneByRoomIdAndUserId(rid, blocked); + const [blockedUser, blockerUser] = await Promise.all([ + Subscriptions.findOneByRoomIdAndUserId(rid, blocked, { projection: { _id: 1 } }), + Subscriptions.findOneByRoomIdAndUserId(rid, userId, { projection: { _id: 1 } }), + ]); - if (!subscription || !subscription2) { + if (!blockedUser || !blockerUser) { throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'blockUser' }); } - await Subscriptions.unsetBlockedByRoomId(rid, blocked, userId); + const [blockedResponse, blockerResponse] = await Subscriptions.unsetBlockedByRoomId(rid, blocked, userId); + + const listenerUsers = [...(blockedResponse?.modifiedCount ? [blocked] : []), ...(blockerResponse?.modifiedCount ? [userId] : [])]; + + if (listenerUsers.length) { + void notifyOnSubscriptionChangedByRoomIdAndUserIds(rid, listenerUsers); + } return true; }, diff --git a/apps/meteor/app/livechat/server/lib/Contacts.ts b/apps/meteor/app/livechat/server/lib/Contacts.ts index 2e648b02f5dd..c20b5dbdb661 100644 --- a/apps/meteor/app/livechat/server/lib/Contacts.ts +++ b/apps/meteor/app/livechat/server/lib/Contacts.ts @@ -6,7 +6,11 @@ import type { MatchKeysAndValues, OnlyFieldsOfType } from 'mongodb'; import { callbacks } from '../../../../lib/callbacks'; import { trim } from '../../../../lib/utils/stringUtils'; -import { notifyOnRoomChangedById, notifyOnLivechatInquiryChangedByRoom } from '../../../lib/server/lib/notifyListener'; +import { + notifyOnRoomChangedById, + notifyOnSubscriptionChangedByRoomId, + notifyOnLivechatInquiryChangedByRoom, +} from '../../../lib/server/lib/notifyListener'; import { i18n } from '../../../utils/lib/i18n'; type RegisterContactProps = { @@ -138,14 +142,23 @@ export const Contacts = { for await (const room of rooms) { const { _id: rid } = room; - await Promise.all([ + const responses = await Promise.all([ Rooms.setFnameById(rid, name), LivechatInquiry.setNameByRoomId(rid, name), Subscriptions.updateDisplayNameByRoomId(rid, name), ]); - void notifyOnLivechatInquiryChangedByRoom(rid, 'updated', { name }); - void notifyOnRoomChangedById(rid); + if (responses[0]?.modifiedCount) { + void notifyOnRoomChangedById(rid); + } + + if (responses[1]?.modifiedCount) { + void notifyOnLivechatInquiryChangedByRoom(rid, 'updated', { name }); + } + + if (responses[2]?.modifiedCount) { + void notifyOnSubscriptionChangedByRoomId(rid); + } } } diff --git a/apps/meteor/app/livechat/server/lib/Helper.ts b/apps/meteor/app/livechat/server/lib/Helper.ts index 1ef572df3068..17f21d8d7b04 100644 --- a/apps/meteor/app/livechat/server/lib/Helper.ts +++ b/apps/meteor/app/livechat/server/lib/Helper.ts @@ -39,6 +39,9 @@ import { sendNotification } from '../../../lib/server'; import { notifyOnLivechatDepartmentAgentChanged, notifyOnLivechatDepartmentAgentChangedByAgentsAndDepartmentId, + notifyOnSubscriptionChangedById, + notifyOnSubscriptionChangedByRoomId, + notifyOnSubscriptionChanged, } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { Livechat as LivechatTyped } from './LivechatTyped'; @@ -285,7 +288,13 @@ export const createLivechatSubscription = async ( ...(department && { department }), } as InsertionModel; - return Subscriptions.insertOne(subscriptionData); + const response = await Subscriptions.insertOne(subscriptionData); + + if (response?.insertedId) { + void notifyOnSubscriptionChangedById(response.insertedId, 'inserted'); + } + + return response; }; export const removeAgentFromSubscription = async (rid: string, { _id, username }: Pick) => { @@ -296,7 +305,11 @@ export const removeAgentFromSubscription = async (rid: string, { _id, username } return; } - await Subscriptions.removeByRoomIdAndUserId(rid, _id); + const deletedSubscription = await Subscriptions.removeByRoomIdAndUserId(rid, _id); + if (deletedSubscription) { + void notifyOnSubscriptionChanged(deletedSubscription, 'removed'); + } + await Message.saveSystemMessage('ul', rid, username || '', { _id: user._id, username: user.username, name: user.name }); setImmediate(() => { @@ -513,12 +526,16 @@ export const updateChatDepartment = async ({ newDepartmentId: string; oldDepartmentId?: string; }) => { - await Promise.all([ + const responses = await Promise.all([ LivechatRooms.changeDepartmentIdByRoomId(rid, newDepartmentId), LivechatInquiry.changeDepartmentIdByRoomId(rid, newDepartmentId), Subscriptions.changeDepartmentByRoomId(rid, newDepartmentId), ]); + if (responses[2].modifiedCount) { + void notifyOnSubscriptionChangedByRoomId(rid); + } + setImmediate(() => { void Apps.self?.triggerEvent(AppEvents.IPostLivechatRoomTransferred, { type: LivechatTransferEventType.DEPARTMENT, diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index bb8a3fd77ba2..2745b37d1501 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -60,8 +60,10 @@ import { notifyOnLivechatInquiryChangedByRoom, notifyOnRoomChangedById, notifyOnLivechatInquiryChangedByToken, - notifyOnLivechatDepartmentAgentChangedByDepartmentId, notifyOnUserChange, + notifyOnLivechatDepartmentAgentChangedByDepartmentId, + notifyOnSubscriptionChangedByRoomId, + notifyOnSubscriptionChanged, } from '../../../lib/server/lib/notifyListener'; import * as Mailer from '../../../mailer/server/api'; import { metrics } from '../../../metrics/server'; @@ -289,7 +291,11 @@ class LivechatClass { throw new Error('Error closing room'); } - await Subscriptions.removeByRoomId(rid); + await Subscriptions.removeByRoomId(rid, { + async onTrash(doc) { + void notifyOnSubscriptionChanged(doc, 'removed'); + }, + }); this.logger.debug(`DB updated for room ${room._id}`); @@ -515,12 +521,16 @@ class LivechatClass { const result = await Promise.allSettled([ Messages.removeByRoomId(rid), ReadReceipts.removeByRoomId(rid), - Subscriptions.removeByRoomId(rid), + Subscriptions.removeByRoomId(rid, { + async onTrash(doc) { + void notifyOnSubscriptionChanged(doc, 'removed'); + }, + }), LivechatInquiry.removeByRoomId(rid), LivechatRooms.removeById(rid), ]); - if (inquiry) { + if (result[3]?.status === 'fulfilled' && result[3].value?.deletedCount && inquiry) { void notifyOnLivechatInquiryChanged(inquiry, 'removed'); } @@ -1143,13 +1153,18 @@ class LivechatClass { const cursor = LivechatRooms.findByVisitorToken(token); for await (const room of cursor) { await Promise.all([ + Subscriptions.removeByRoomId(room._id, { + async onTrash(doc) { + void notifyOnSubscriptionChanged(doc, 'removed'); + }, + }), FileUpload.removeFilesByRoomId(room._id), Messages.removeByRoomId(room._id), ReadReceipts.removeByRoomId(room._id), ]); } - await Promise.all([Subscriptions.removeByVisitorToken(token), LivechatRooms.removeByVisitorToken(token)]); + await LivechatRooms.removeByVisitorToken(token); const livechatInquiries = await LivechatInquiry.findIdsByVisitorToken(token).toArray(); await LivechatInquiry.removeByIds(livechatInquiries.map(({ _id }) => _id)); @@ -1701,13 +1716,19 @@ class LivechatClass { const { _id: rid } = roomData; const { name } = guestData; - await Promise.all([ + const responses = await Promise.all([ Rooms.setFnameById(rid, name), LivechatInquiry.setNameByRoomId(rid, name), Subscriptions.updateDisplayNameByRoomId(rid, name), ]); - void notifyOnLivechatInquiryChangedByRoom(rid, 'updated', { name }); + if (responses[1]?.modifiedCount) { + void notifyOnLivechatInquiryChangedByRoom(rid, 'updated', { name }); + } + + if (responses[2]?.modifiedCount) { + await notifyOnSubscriptionChangedByRoomId(rid); + } } void notifyOnRoomChangedById(roomData._id); diff --git a/apps/meteor/app/message-mark-as-unread/server/unreadMessages.ts b/apps/meteor/app/message-mark-as-unread/server/unreadMessages.ts index 6ef1f5567a20..f213ae4b7243 100644 --- a/apps/meteor/app/message-mark-as-unread/server/unreadMessages.ts +++ b/apps/meteor/app/message-mark-as-unread/server/unreadMessages.ts @@ -3,6 +3,7 @@ import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Messages, Subscriptions } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; +import { notifyOnSubscriptionChangedByRoomIdAndUserId } from '../../lib/server/lib/notifyListener'; import logger from './logger'; declare module '@rocket.chat/ddp-client' { @@ -36,7 +37,11 @@ Meteor.methods({ }); } - await Subscriptions.setAsUnreadByRoomIdAndUserId(lastMessage.rid, userId, lastMessage.ts); + const setAsUnreadResponse = await Subscriptions.setAsUnreadByRoomIdAndUserId(lastMessage.rid, userId, lastMessage.ts); + if (setAsUnreadResponse.modifiedCount) { + void notifyOnSubscriptionChangedByRoomIdAndUserId(lastMessage.rid, userId); + } + return; } @@ -72,7 +77,11 @@ Meteor.methods({ if (firstUnreadMessage.ts >= lastSeen) { return logger.debug('Provided message is already marked as unread'); } - logger.debug(`Updating unread message of ${originalMessage.ts} as the first unread`); - await Subscriptions.setAsUnreadByRoomIdAndUserId(originalMessage.rid, userId, originalMessage.ts); + + logger.debug(`Updating unread message of ${originalMessage.ts} as the first unread`); + const setAsUnreadResponse = await Subscriptions.setAsUnreadByRoomIdAndUserId(originalMessage.rid, userId, originalMessage.ts); + if (setAsUnreadResponse.modifiedCount) { + void notifyOnSubscriptionChangedByRoomIdAndUserId(originalMessage.rid, userId); + } }, }); diff --git a/apps/meteor/app/push-notifications/server/methods/saveNotificationSettings.ts b/apps/meteor/app/push-notifications/server/methods/saveNotificationSettings.ts index a86ded6f24e5..ddac51b4eca9 100644 --- a/apps/meteor/app/push-notifications/server/methods/saveNotificationSettings.ts +++ b/apps/meteor/app/push-notifications/server/methods/saveNotificationSettings.ts @@ -4,6 +4,7 @@ import { Subscriptions } from '@rocket.chat/models'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; +import { notifyOnSubscriptionChangedById } from '../../../lib/server/lib/notifyListener'; import { getUserNotificationPreference } from '../../../utils/server/getUserNotificationPreference'; const saveAudioNotificationValue = (subId: ISubscription['_id'], value: string) => @@ -132,7 +133,10 @@ Meteor.methods({ }); } - await notifications[field].updateMethod(subscription, value); + const updateResponse = await notifications[field].updateMethod(subscription, value); + if (updateResponse.modifiedCount) { + void notifyOnSubscriptionChangedById(subscription._id); + } return true; }, @@ -144,13 +148,19 @@ Meteor.methods({ method: 'saveAudioNotificationValue', }); } + const subscription = await Subscriptions.findOneByRoomIdAndUserId(rid, userId); if (!subscription) { throw new Meteor.Error('error-invalid-subscription', 'Invalid subscription', { method: 'saveAudioNotificationValue', }); } - await saveAudioNotificationValue(subscription._id, value); + + const saveAudioNotificationResponse = await saveAudioNotificationValue(subscription._id, value); + if (saveAudioNotificationResponse.modifiedCount) { + void notifyOnSubscriptionChangedById(subscription._id); + } + return true; }, }); diff --git a/apps/meteor/app/threads/server/functions.ts b/apps/meteor/app/threads/server/functions.ts index 30daef8b8b93..194e482c54ae 100644 --- a/apps/meteor/app/threads/server/functions.ts +++ b/apps/meteor/app/threads/server/functions.ts @@ -2,51 +2,57 @@ import type { IMessage } from '@rocket.chat/core-typings'; import { isEditedMessage } from '@rocket.chat/core-typings'; import { Messages, Subscriptions, ReadReceipts, NotificationQueue } from '@rocket.chat/models'; +import { + notifyOnSubscriptionChangedByRoomIdAndUserIds, + notifyOnSubscriptionChangedByRoomIdAndUserId, +} from '../../lib/server/lib/notifyListener'; import { getMentions, getUserIdsFromHighlights } from '../../lib/server/lib/notifyUsersOnMessage'; export async function reply({ tmid }: { tmid?: string }, message: IMessage, parentMessage: IMessage, followers: string[]) { - const { rid, ts, u } = message; if (!tmid || isEditedMessage(message)) { return false; } - const { toAll, toHere, mentionIds } = await getMentions(message); + const { rid, ts, u } = message; + + const [highlightsUids, threadFollowers, { toAll, toHere, mentionIds }] = await Promise.all([ + getUserIdsFromHighlights(rid, message), + Messages.getThreadFollowsByThreadId(tmid), + getMentions(message), + ]); const addToReplies = [ - ...new Set([ - ...followers, - ...mentionIds, - ...(Array.isArray(parentMessage.replies) && parentMessage.replies.length ? [u._id] : [parentMessage.u._id, u._id]), - ]), + ...new Set([...followers, ...mentionIds, ...(parentMessage.replies?.length ? [u._id] : [parentMessage.u._id, u._id])]), ]; - const highlightedUserIds = new Set(); - (await getUserIdsFromHighlights(rid, message)).forEach((uid) => highlightedUserIds.add(uid)); - await Messages.updateRepliesByThreadId(tmid, addToReplies, ts); - await ReadReceipts.setAsThreadById(tmid); + const threadFollowersUids = threadFollowers?.filter((userId) => userId !== u._id && !mentionIds.includes(userId)) || []; - const replies = await Messages.getThreadFollowsByThreadId(tmid); + // Notify everyone involved in the thread + const notifyOptions = toAll || toHere ? { groupMention: true } : {}; - const repliesFiltered = (replies || []).filter((userId) => userId !== u._id).filter((userId) => !mentionIds.includes(userId)); + // Notify message mentioned users and highlights + const mentionedUsers = [...new Set([...mentionIds, ...highlightsUids])]; - if (toAll || toHere) { - await Subscriptions.addUnreadThreadByRoomIdAndUserIds(rid, repliesFiltered, tmid, { - groupMention: true, - }); - } else { - await Subscriptions.addUnreadThreadByRoomIdAndUserIds(rid, repliesFiltered, tmid, {}); - } + const promises = [ + Messages.updateRepliesByThreadId(tmid, addToReplies, ts), + ReadReceipts.setAsThreadById(tmid), + Subscriptions.addUnreadThreadByRoomIdAndUserIds(rid, threadFollowersUids, tmid, notifyOptions), + ]; - const mentionedUsers = new Set([...mentionIds, ...highlightedUserIds]); - for await (const userId of mentionedUsers) { - await Subscriptions.addUnreadThreadByRoomIdAndUserIds(rid, [userId], tmid, { userMention: true }); + if (mentionedUsers.length) { + promises.push(Subscriptions.addUnreadThreadByRoomIdAndUserIds(rid, mentionedUsers, tmid, { userMention: true })); } - const highlightIds = Array.from(highlightedUserIds); - if (highlightIds.length) { - await Subscriptions.setAlertForRoomIdAndUserIds(rid, highlightIds); - await Subscriptions.setOpenForRoomIdAndUserIds(rid, highlightIds); + if (highlightsUids.length) { + promises.push( + Subscriptions.setAlertForRoomIdAndUserIds(rid, highlightsUids), + Subscriptions.setOpenForRoomIdAndUserIds(rid, highlightsUids), + ); } + + await Promise.allSettled(promises); + + void notifyOnSubscriptionChangedByRoomIdAndUserIds(rid, [...threadFollowersUids, ...mentionedUsers, ...highlightsUids]); } export async function follow({ tmid, uid }: { tmid: string; uid: string }) { @@ -62,20 +68,27 @@ export async function unfollow({ tmid, rid, uid }: { tmid: string; rid: string; return false; } - await Subscriptions.removeUnreadThreadByRoomIdAndUserId(rid, uid, tmid); + const removeUnreadThreadResponse = await Subscriptions.removeUnreadThreadByRoomIdAndUserId(rid, uid, tmid); + if (removeUnreadThreadResponse.modifiedCount) { + void notifyOnSubscriptionChangedByRoomIdAndUserId(rid, uid); + } await Messages.removeThreadFollowerByThreadId(tmid, uid); } export const readThread = async ({ userId, rid, tmid }: { userId: string; rid: string; tmid: string }) => { - const projection = { tunread: 1 }; - const sub = await Subscriptions.findOneByRoomIdAndUserId(rid, userId, { projection }); + const sub = await Subscriptions.findOneByRoomIdAndUserId(rid, userId, { projection: { tunread: 1 } }); if (!sub) { return; } + // if the thread being marked as read is the last one unread also clear the unread subscription flag const clearAlert = sub.tunread && sub.tunread?.length <= 1 && sub.tunread.includes(tmid); - await Subscriptions.removeUnreadThreadByRoomIdAndUserId(rid, userId, tmid, clearAlert); + const removeUnreadThreadResponse = await Subscriptions.removeUnreadThreadByRoomIdAndUserId(rid, userId, tmid, clearAlert); + if (removeUnreadThreadResponse.modifiedCount) { + void notifyOnSubscriptionChangedByRoomIdAndUserId(rid, userId); + } + await NotificationQueue.clearQueueByUserId(userId); }; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.ts index 4e76f396617b..bc2d4fb6a3fb 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.ts @@ -1,6 +1,7 @@ import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; import { LivechatRooms, Subscriptions } from '@rocket.chat/models'; +import { notifyOnSubscriptionChangedByRoomId } from '../../../../../app/lib/server/lib/notifyListener'; import { settings } from '../../../../../app/settings/server'; import { callbacks } from '../../../../../lib/callbacks'; import { AutoCloseOnHoldScheduler } from '../lib/AutoCloseOnHoldScheduler'; @@ -16,12 +17,16 @@ const onCloseLivechat = async (params: LivechatCloseCallbackParams) => { room: { _id: roomId }, } = params; - await Promise.all([ + const responses = await Promise.all([ LivechatRooms.unsetOnHoldByRoomId(roomId), Subscriptions.unsetOnHoldByRoomId(roomId), AutoCloseOnHoldScheduler.unscheduleRoom(roomId), ]); + if (responses[1].modifiedCount) { + void notifyOnSubscriptionChangedByRoomId(roomId); + } + if (!settings.get('Livechat_waiting_queue')) { return params; } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/services/omnichannel.internalService.ts b/apps/meteor/ee/app/livechat-enterprise/server/services/omnichannel.internalService.ts index c3133387865d..f2db61ddbc9a 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/services/omnichannel.internalService.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/services/omnichannel.internalService.ts @@ -5,7 +5,11 @@ import type { IOmnichannelRoom, IUser, ILivechatInquiryRecord, IOmnichannelSyste import { Logger } from '@rocket.chat/logger'; import { LivechatRooms, Subscriptions, LivechatInquiry } from '@rocket.chat/models'; -import { notifyOnLivechatInquiryChangedById, notifyOnRoomChangedById } from '../../../../../app/lib/server/lib/notifyListener'; +import { + notifyOnSubscriptionChangedByRoomId, + notifyOnLivechatInquiryChangedById, + notifyOnRoomChangedById, +} from '../../../../../app/lib/server/lib/notifyListener'; import { dispatchAgentDelegated } from '../../../../../app/livechat/server/lib/Helper'; import { queueInquiry } from '../../../../../app/livechat/server/lib/QueueManager'; import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingManager'; @@ -53,15 +57,21 @@ export class OmnichannelEE extends ServiceClassInternal implements IOmnichannelE throw new Error('error-unserved-rooms-cannot-be-placed-onhold'); } - await Promise.all([ + const [roomResult, subsResult] = await Promise.all([ LivechatRooms.setOnHoldByRoomId(roomId), Subscriptions.setOnHoldByRoomId(roomId), Message.saveSystemMessage('omnichannel_placed_chat_on_hold', roomId, '', onHoldBy, { comment }), ]); - await callbacks.run('livechat:afterOnHold', room); + if (roomResult.modifiedCount) { + void notifyOnRoomChangedById(roomId); + } - void notifyOnRoomChangedById(roomId); + if (subsResult.modifiedCount) { + void notifyOnSubscriptionChangedByRoomId(roomId); + } + + await callbacks.run('livechat:afterOnHold', room); } async resumeRoomOnHold( @@ -104,15 +114,21 @@ export class OmnichannelEE extends ServiceClassInternal implements IOmnichannelE clientAction, }); - await Promise.all([ + const [roomResult, subsResult] = await Promise.all([ LivechatRooms.unsetOnHoldByRoomId(roomId), Subscriptions.unsetOnHoldByRoomId(roomId), Message.saveSystemMessage('omnichannel_on_hold_chat_resumed', roomId, '', resumeBy, { comment }), ]); - await callbacks.run('livechat:afterOnHoldChatResumed', room); + if (roomResult.modifiedCount) { + void notifyOnRoomChangedById(roomId); + } - void notifyOnRoomChangedById(roomId); + if (subsResult.modifiedCount) { + void notifyOnSubscriptionChangedByRoomId(roomId); + } + + await callbacks.run('livechat:afterOnHoldChatResumed', room); } private async attemptToAssignRoomToServingAgentElseQueueIt({ 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 9180632768af..62623b1a4a1c 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,4 @@ -import { isEditedMessage, isOmnichannelRoom } from '@rocket.chat/core-typings'; -import { Subscriptions } from '@rocket.chat/models'; +import { isEditedMessage } from '@rocket.chat/core-typings'; import { callbacks } from '../../../../../lib/callbacks'; import { ReadReceipt } from '../../../../server/lib/message-read-receipt/ReadReceipt'; @@ -12,11 +11,6 @@ callbacks.add( return message; } - if (!isOmnichannelRoom(room) || !room.closedAt) { - // set subscription as read right after message was sent - await Subscriptions.setAsReadByRoomIdAndUserId(room._id, message.u._id); - } - // mark message as read as well await ReadReceipt.markMessageAsReadBySender(message, room, message.u._id); diff --git a/apps/meteor/server/database/watchCollections.ts b/apps/meteor/server/database/watchCollections.ts index 5cd56af62fd6..6dd173d5d323 100644 --- a/apps/meteor/server/database/watchCollections.ts +++ b/apps/meteor/server/database/watchCollections.ts @@ -29,7 +29,7 @@ const onlyCollections = DBWATCHER_ONLY_COLLECTIONS.split(',') .filter(Boolean); export function getWatchCollections(): string[] { - const collections = [InstanceStatus.getCollectionName(), Subscriptions.getCollectionName()]; + const collections = [InstanceStatus.getCollectionName()]; // add back to the list of collections in case db watchers are enabled if (!dbWatchersDisabled) { @@ -45,6 +45,7 @@ export function getWatchCollections(): string[] { collections.push(LoginServiceConfiguration.getCollectionName()); collections.push(EmailInbox.getCollectionName()); collections.push(IntegrationHistory.getCollectionName()); + collections.push(Subscriptions.getCollectionName()); collections.push(Settings.getCollectionName()); collections.push(LivechatDepartmentAgents.getCollectionName()); } diff --git a/apps/meteor/server/lib/readMessages.ts b/apps/meteor/server/lib/readMessages.ts index d7c8cf559288..3be43a875fac 100644 --- a/apps/meteor/server/lib/readMessages.ts +++ b/apps/meteor/server/lib/readMessages.ts @@ -1,6 +1,7 @@ import type { IRoom, IUser } from '@rocket.chat/core-typings'; import { NotificationQueue, Subscriptions } from '@rocket.chat/models'; +import { notifyOnSubscriptionChangedByRoomIdAndUserId } from '../../app/lib/server/lib/notifyListener'; import { callbacks } from '../../lib/callbacks'; export async function readMessages(rid: IRoom['_id'], uid: IUser['_id'], readThreads: boolean): Promise { @@ -15,7 +16,10 @@ export async function readMessages(rid: IRoom['_id'], uid: IUser['_id'], readThr // do not mark room as read if there are still unread threads const alert = !!(sub.alert && !readThreads && sub.tunread && sub.tunread.length > 0); - await Subscriptions.setAsReadByRoomIdAndUserId(rid, uid, readThreads, alert); + const setAsReadResponse = await Subscriptions.setAsReadByRoomIdAndUserId(rid, uid, readThreads, alert); + if (setAsReadResponse.modifiedCount) { + void notifyOnSubscriptionChangedByRoomIdAndUserId(rid, uid); + } await NotificationQueue.clearQueueByUserId(uid); diff --git a/apps/meteor/server/lib/resetUserE2EKey.ts b/apps/meteor/server/lib/resetUserE2EKey.ts index 8535eee9a2cd..85da6e59cf60 100644 --- a/apps/meteor/server/lib/resetUserE2EKey.ts +++ b/apps/meteor/server/lib/resetUserE2EKey.ts @@ -2,7 +2,7 @@ import { api } from '@rocket.chat/core-services'; import { Subscriptions, Users, Rooms } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; -import { notifyOnUserChange } from '../../app/lib/server/lib/notifyListener'; +import { notifyOnUserChange, notifyOnSubscriptionChangedByUserId } from '../../app/lib/server/lib/notifyListener'; import * as Mailer from '../../app/mailer/server/api'; import { settings } from '../../app/settings/server'; import { i18n } from './i18n'; @@ -67,12 +67,13 @@ export async function resetUserE2EEncriptionKey(uid: string, notifyUser: boolean } // force logout the live sessions - await api.broadcast('user.forceLogout', uid); - await Users.resetE2EKey(uid); - await Subscriptions.resetUserE2EKey(uid); - await Rooms.removeUserFromE2EEQueue(uid); + const responses = await Promise.all([Users.resetE2EKey(uid), Subscriptions.resetUserE2EKey(uid), Rooms.removeUserFromE2EEQueue(uid)]); + + if (responses[1]?.modifiedCount) { + void notifyOnSubscriptionChangedByUserId(uid); + } // Force the user to logout, so that the keys can be generated again await Users.unsetLoginTokens(uid); diff --git a/apps/meteor/server/methods/addAllUserToRoom.ts b/apps/meteor/server/methods/addAllUserToRoom.ts index c07bdc48040a..6b1b690b4bfd 100644 --- a/apps/meteor/server/methods/addAllUserToRoom.ts +++ b/apps/meteor/server/methods/addAllUserToRoom.ts @@ -6,6 +6,7 @@ import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../app/authorization/server/functions/hasPermission'; +import { notifyOnSubscriptionChangedById } from '../../app/lib/server/lib/notifyListener'; import { settings } from '../../app/settings/server'; import { getDefaultSubscriptionPref } from '../../app/utils/lib/getDefaultSubscriptionPref'; import { callbacks } from '../../lib/callbacks'; @@ -58,7 +59,7 @@ Meteor.methods({ } await callbacks.run('beforeJoinRoom', user, room); const autoTranslateConfig = getSubscriptionAutotranslateDefaultConfig(user); - await Subscriptions.createWithRoomAndUser(room, user, { + const { insertedId } = await Subscriptions.createWithRoomAndUser(room, user, { ts: now, open: true, alert: true, @@ -68,6 +69,9 @@ Meteor.methods({ ...autoTranslateConfig, ...getDefaultSubscriptionPref(user), }); + if (insertedId) { + void notifyOnSubscriptionChangedById(insertedId, 'inserted'); + } await Message.saveSystemMessage('uj', rid, user.username || '', user, { ts: now }); await callbacks.run('afterJoinRoom', user, room); } diff --git a/apps/meteor/server/methods/addRoomLeader.ts b/apps/meteor/server/methods/addRoomLeader.ts index b8e09b44065a..64240bff65f0 100644 --- a/apps/meteor/server/methods/addRoomLeader.ts +++ b/apps/meteor/server/methods/addRoomLeader.ts @@ -6,6 +6,7 @@ import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../app/authorization/server/functions/hasPermission'; +import { notifyOnSubscriptionChangedById } from '../../app/lib/server/lib/notifyListener'; import { settings } from '../../app/settings/server'; declare module '@rocket.chat/ddp-client' { @@ -56,7 +57,10 @@ Meteor.methods({ }); } - await Subscriptions.addRoleById(subscription._id, 'leader'); + const addRoleResponse = await Subscriptions.addRoleById(subscription._id, 'leader'); + if (addRoleResponse.modifiedCount) { + void notifyOnSubscriptionChangedById(subscription._id); + } const fromUser = await Users.findOneById(uid); diff --git a/apps/meteor/server/methods/addRoomModerator.ts b/apps/meteor/server/methods/addRoomModerator.ts index a9cc21f30e0d..da75038a3688 100644 --- a/apps/meteor/server/methods/addRoomModerator.ts +++ b/apps/meteor/server/methods/addRoomModerator.ts @@ -7,6 +7,7 @@ import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../app/authorization/server/functions/hasPermission'; +import { notifyOnSubscriptionChangedById } from '../../app/lib/server/lib/notifyListener'; import { settings } from '../../app/settings/server'; import { isFederationEnabled, isFederationReady, FederationMatrixInvalidConfigurationError } from '../services/federation/utils'; @@ -71,7 +72,10 @@ Meteor.methods({ }); } - await Subscriptions.addRoleById(subscription._id, 'moderator'); + const addRoleResponse = await Subscriptions.addRoleById(subscription._id, 'moderator'); + if (addRoleResponse.modifiedCount) { + void notifyOnSubscriptionChangedById(subscription._id); + } const fromUser = await Users.findOneById(uid); if (!fromUser) { diff --git a/apps/meteor/server/methods/addRoomOwner.ts b/apps/meteor/server/methods/addRoomOwner.ts index f59267f6719a..d0a23efea024 100644 --- a/apps/meteor/server/methods/addRoomOwner.ts +++ b/apps/meteor/server/methods/addRoomOwner.ts @@ -7,6 +7,7 @@ import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../app/authorization/server/functions/hasPermission'; +import { notifyOnSubscriptionChangedById } from '../../app/lib/server/lib/notifyListener'; import { settings } from '../../app/settings/server'; import { isFederationReady, isFederationEnabled, FederationMatrixInvalidConfigurationError } from '../services/federation/utils'; @@ -71,7 +72,10 @@ Meteor.methods({ }); } - await Subscriptions.addRoleById(subscription._id, 'owner'); + const addRoleResponse = await Subscriptions.addRoleById(subscription._id, 'owner'); + if (addRoleResponse.modifiedCount) { + void notifyOnSubscriptionChangedById(subscription._id); + } const fromUser = await Users.findOneById(uid); if (!fromUser) { diff --git a/apps/meteor/server/methods/hideRoom.ts b/apps/meteor/server/methods/hideRoom.ts index a53a328d549a..1fd4c6b4657e 100644 --- a/apps/meteor/server/methods/hideRoom.ts +++ b/apps/meteor/server/methods/hideRoom.ts @@ -3,6 +3,8 @@ import { Subscriptions } from '@rocket.chat/models'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; +import { notifyOnSubscriptionChangedByRoomIdAndUserId } from '../../app/lib/server/lib/notifyListener'; + declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { @@ -19,7 +21,13 @@ export const hideRoomMethod = async (userId: string, rid: string): Promise({ diff --git a/apps/meteor/server/methods/ignoreUser.ts b/apps/meteor/server/methods/ignoreUser.ts index 358fc3be3d8f..a8739a910b37 100644 --- a/apps/meteor/server/methods/ignoreUser.ts +++ b/apps/meteor/server/methods/ignoreUser.ts @@ -3,6 +3,8 @@ import { Subscriptions } from '@rocket.chat/models'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; +import { notifyOnSubscriptionChangedById } from '../../app/lib/server/lib/notifyListener'; + declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { @@ -23,7 +25,10 @@ Meteor.methods({ }); } - const subscription = await Subscriptions.findOneByRoomIdAndUserId(rid, userId); + const [subscription, subscriptionIgnoredUser] = await Promise.all([ + Subscriptions.findOneByRoomIdAndUserId(rid, userId), + Subscriptions.findOneByRoomIdAndUserId(rid, ignoredUser), + ]); if (!subscription) { throw new Meteor.Error('error-invalid-subscription', 'Invalid subscription', { @@ -31,14 +36,18 @@ Meteor.methods({ }); } - const subscriptionIgnoredUser = await Subscriptions.findOneByRoomIdAndUserId(rid, ignoredUser); - if (!subscriptionIgnoredUser) { throw new Meteor.Error('error-invalid-subscription', 'Invalid subscription', { method: 'ignoreUser', }); } - return !!(await Subscriptions.ignoreUser({ _id: subscription._id, ignoredUser, ignore })); + const result = await Subscriptions.ignoreUser({ _id: subscription._id, ignoredUser, ignore }); + + if (result.modifiedCount) { + void notifyOnSubscriptionChangedById(subscription._id); + } + + return !!result; }, }); diff --git a/apps/meteor/server/methods/openRoom.ts b/apps/meteor/server/methods/openRoom.ts index b2957768f237..440de52b87fb 100644 --- a/apps/meteor/server/methods/openRoom.ts +++ b/apps/meteor/server/methods/openRoom.ts @@ -4,6 +4,8 @@ import { Subscriptions } from '@rocket.chat/models'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; +import { notifyOnSubscriptionChangedByRoomIdAndUserId } from '../../app/lib/server/lib/notifyListener'; + declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { @@ -23,6 +25,12 @@ Meteor.methods({ }); } - return (await Subscriptions.openByRoomIdAndUserId(rid, uid)).modifiedCount; + const openByRoomResponse = await Subscriptions.openByRoomIdAndUserId(rid, uid); + + if (openByRoomResponse.modifiedCount) { + void notifyOnSubscriptionChangedByRoomIdAndUserId(rid, uid); + } + + return openByRoomResponse.modifiedCount; }, }); diff --git a/apps/meteor/server/methods/removeRoomLeader.ts b/apps/meteor/server/methods/removeRoomLeader.ts index 754d68960a4a..8a8f92d08fa0 100644 --- a/apps/meteor/server/methods/removeRoomLeader.ts +++ b/apps/meteor/server/methods/removeRoomLeader.ts @@ -6,6 +6,7 @@ import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../app/authorization/server/functions/hasPermission'; +import { notifyOnSubscriptionChangedById } from '../../app/lib/server/lib/notifyListener'; import { settings } from '../../app/settings/server'; declare module '@rocket.chat/ddp-client' { @@ -56,7 +57,10 @@ Meteor.methods({ }); } - await Subscriptions.removeRoleById(subscription._id, 'leader'); + const removeRoleResponse = await Subscriptions.removeRoleById(subscription._id, 'leader'); + if (removeRoleResponse.modifiedCount) { + void notifyOnSubscriptionChangedById(subscription._id); + } const fromUser = await Users.findOneById(uid); if (!fromUser) { diff --git a/apps/meteor/server/methods/removeRoomModerator.ts b/apps/meteor/server/methods/removeRoomModerator.ts index 291cc294a5fa..bcb50076c834 100644 --- a/apps/meteor/server/methods/removeRoomModerator.ts +++ b/apps/meteor/server/methods/removeRoomModerator.ts @@ -7,6 +7,7 @@ import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../app/authorization/server/functions/hasPermission'; +import { notifyOnSubscriptionChangedById } from '../../app/lib/server/lib/notifyListener'; import { settings } from '../../app/settings/server'; declare module '@rocket.chat/ddp-client' { @@ -64,7 +65,10 @@ Meteor.methods({ }); } - await Subscriptions.removeRoleById(subscription._id, 'moderator'); + const removeRoleResponse = await Subscriptions.removeRoleById(subscription._id, 'moderator'); + if (removeRoleResponse.modifiedCount) { + void notifyOnSubscriptionChangedById(subscription._id); + } const fromUser = await Users.findOneById(uid); if (!fromUser) { diff --git a/apps/meteor/server/methods/removeRoomOwner.ts b/apps/meteor/server/methods/removeRoomOwner.ts index 82ee2c37f9b8..91046655a4a6 100644 --- a/apps/meteor/server/methods/removeRoomOwner.ts +++ b/apps/meteor/server/methods/removeRoomOwner.ts @@ -7,6 +7,7 @@ import { Meteor } from 'meteor/meteor'; import { getUsersInRole } from '../../app/authorization/server'; import { hasPermissionAsync } from '../../app/authorization/server/functions/hasPermission'; +import { notifyOnSubscriptionChangedById } from '../../app/lib/server/lib/notifyListener'; import { settings } from '../../app/settings/server'; declare module '@rocket.chat/ddp-client' { @@ -71,7 +72,10 @@ Meteor.methods({ }); } - await Subscriptions.removeRoleById(subscription._id, 'owner'); + const removeRoleResponse = await Subscriptions.removeRoleById(subscription._id, 'owner'); + if (removeRoleResponse.modifiedCount) { + void notifyOnSubscriptionChangedById(subscription._id); + } const fromUser = await Users.findOneById(uid); if (!fromUser) { diff --git a/apps/meteor/server/methods/removeUserFromRoom.ts b/apps/meteor/server/methods/removeUserFromRoom.ts index b039beb7ce64..781ffe3a2671 100644 --- a/apps/meteor/server/methods/removeUserFromRoom.ts +++ b/apps/meteor/server/methods/removeUserFromRoom.ts @@ -9,7 +9,7 @@ import { Meteor } from 'meteor/meteor'; import { canAccessRoomAsync, getUsersInRole } from '../../app/authorization/server'; import { hasPermissionAsync } from '../../app/authorization/server/functions/hasPermission'; import { hasRoleAsync } from '../../app/authorization/server/functions/hasRole'; -import { notifyOnRoomChanged } from '../../app/lib/server/lib/notifyListener'; +import { notifyOnRoomChanged, notifyOnSubscriptionChanged } from '../../app/lib/server/lib/notifyListener'; import { settings } from '../../app/settings/server'; import { RoomMemberActions } from '../../definition/IRoomTypeConfig'; import { callbacks } from '../../lib/callbacks'; @@ -91,7 +91,10 @@ export const removeUserFromRoomMethod = async (fromId: string, data: { rid: stri await callbacks.run('beforeRemoveFromRoom', { removedUser, userWhoRemoved: fromUser }, room); - await Subscriptions.removeByRoomIdAndUserId(data.rid, removedUser._id); + const deletedSubscription = await Subscriptions.removeByRoomIdAndUserId(data.rid, removedUser._id); + if (deletedSubscription) { + void notifyOnSubscriptionChanged(deletedSubscription, 'removed'); + } if (['c', 'p'].includes(room.t) === true) { await removeUserFromRolesAsync(removedUser._id, ['moderator', 'owner'], data.rid); diff --git a/apps/meteor/server/methods/saveUserPreferences.ts b/apps/meteor/server/methods/saveUserPreferences.ts index f19e653f7ccc..70e3a5cb9ea7 100644 --- a/apps/meteor/server/methods/saveUserPreferences.ts +++ b/apps/meteor/server/methods/saveUserPreferences.ts @@ -1,11 +1,16 @@ -import type { ThemePreference } from '@rocket.chat/core-typings'; +import type { ISubscription, ThemePreference } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Subscriptions, Users } from '@rocket.chat/models'; import type { FontSize } from '@rocket.chat/rest-typings'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { notifyOnUserChange } from '../../app/lib/server/lib/notifyListener'; +import { + notifyOnSubscriptionChangedByAutoTranslateAndUserId, + notifyOnSubscriptionChangedByUserId, + notifyOnSubscriptionChangedByUserPreferences, + notifyOnUserChange, +} from '../../app/lib/server/lib/notifyListener'; import { settings as rcSettings } from '../../app/settings/server'; type UserPreferences = { @@ -54,6 +59,31 @@ declare module '@rocket.chat/ddp-client' { } } +async function updateNotificationPreferences( + userId: ISubscription['u']['_id'], + setting: keyof ISubscription, + newValue: string, + oldValue: string, + preferenceType: keyof ISubscription, +) { + if (newValue === oldValue) { + return; + } + + if (newValue === 'default') { + const clearNotificationResponse = await Subscriptions.clearNotificationUserPreferences(userId, setting, preferenceType); + if (clearNotificationResponse.modifiedCount) { + void notifyOnSubscriptionChangedByUserPreferences(userId, preferenceType, 'user'); + } + return; + } + + const updateNotificationResponse = await Subscriptions.updateNotificationUserPreferences(userId, newValue, setting, preferenceType); + if (updateNotificationResponse.modifiedCount) { + void notifyOnSubscriptionChangedByUserPreferences(userId, preferenceType, 'subscription'); + } +} + export const saveUserPreferences = async (settings: Partial, userId: string): Promise => { const keys = { language: Match.Optional(String), @@ -146,51 +176,41 @@ export const saveUserPreferences = async (settings: Partial, us // propagate changed notification preferences setImmediate(async () => { - if (settings.desktopNotifications && oldDesktopNotifications !== settings.desktopNotifications) { - if (settings.desktopNotifications === 'default') { - await Subscriptions.clearNotificationUserPreferences(user._id, 'desktopNotifications', 'desktopPrefOrigin'); - } else { - await Subscriptions.updateNotificationUserPreferences( - user._id, - settings.desktopNotifications, - 'desktopNotifications', - 'desktopPrefOrigin', - ); - } + const { desktopNotifications, pushNotifications, emailNotificationMode, highlights, language } = settings; + const promises = []; + + if (desktopNotifications) { + promises.push( + updateNotificationPreferences(user._id, 'desktopNotifications', desktopNotifications, oldDesktopNotifications, 'desktopPrefOrigin'), + ); } - if (settings.pushNotifications && oldMobileNotifications !== settings.pushNotifications) { - if (settings.pushNotifications === 'default') { - await Subscriptions.clearNotificationUserPreferences(user._id, 'mobilePushNotifications', 'mobilePrefOrigin'); - } else { - await Subscriptions.updateNotificationUserPreferences( - user._id, - settings.pushNotifications, - 'mobilePushNotifications', - 'mobilePrefOrigin', - ); - } + if (pushNotifications) { + promises.push( + updateNotificationPreferences(user._id, 'mobilePushNotifications', pushNotifications, oldMobileNotifications, 'mobilePrefOrigin'), + ); } - if (settings.emailNotificationMode && oldEmailNotifications !== settings.emailNotificationMode) { - if (settings.emailNotificationMode === 'default') { - await Subscriptions.clearNotificationUserPreferences(user._id, 'emailNotifications', 'emailPrefOrigin'); - } else { - await Subscriptions.updateNotificationUserPreferences( - user._id, - settings.emailNotificationMode, - 'emailNotifications', - 'emailPrefOrigin', - ); - } + if (emailNotificationMode) { + promises.push( + updateNotificationPreferences(user._id, 'emailNotifications', emailNotificationMode, oldEmailNotifications, 'emailPrefOrigin'), + ); } - if (Array.isArray(settings.highlights)) { - await Subscriptions.updateUserHighlights(user._id, settings.highlights); + await Promise.allSettled(promises); + + if (Array.isArray(highlights)) { + const response = await Subscriptions.updateUserHighlights(user._id, highlights); + if (response.modifiedCount) { + void notifyOnSubscriptionChangedByUserId(user._id); + } } - if (settings.language && oldLanguage !== settings.language && rcSettings.get('AutoTranslate_AutoEnableOnJoinRoom')) { - await Subscriptions.updateAllAutoTranslateLanguagesByUserId(user._id, settings.language); + if (language && oldLanguage !== language && rcSettings.get('AutoTranslate_AutoEnableOnJoinRoom')) { + const response = await Subscriptions.updateAllAutoTranslateLanguagesByUserId(user._id, language); + if (response.modifiedCount) { + void notifyOnSubscriptionChangedByAutoTranslateAndUserId(user._id); + } } }); }; diff --git a/apps/meteor/server/methods/toggleFavorite.ts b/apps/meteor/server/methods/toggleFavorite.ts index 36555a4566db..912b9a8f3e5c 100644 --- a/apps/meteor/server/methods/toggleFavorite.ts +++ b/apps/meteor/server/methods/toggleFavorite.ts @@ -4,6 +4,8 @@ import { Subscriptions } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; +import { notifyOnSubscriptionChangedByRoomIdAndUserId } from '../../app/lib/server/lib/notifyListener'; + declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { @@ -28,6 +30,12 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-subscription', 'You must be part of a room to favorite it', { method: 'toggleFavorite' }); } - return (await Subscriptions.setFavoriteByRoomIdAndUserId(rid, userId, f)).modifiedCount; + const { modifiedCount } = await Subscriptions.setFavoriteByRoomIdAndUserId(rid, userId, f); + + if (modifiedCount) { + void notifyOnSubscriptionChangedByRoomIdAndUserId(rid, userId); + } + + return modifiedCount; }, }); diff --git a/apps/meteor/server/models/dummy/BaseDummy.ts b/apps/meteor/server/models/dummy/BaseDummy.ts index c3052ede9487..049295c1a28a 100644 --- a/apps/meteor/server/models/dummy/BaseDummy.ts +++ b/apps/meteor/server/models/dummy/BaseDummy.ts @@ -53,6 +53,13 @@ export class BaseDummy< return this.collectionName; } + async findOneAndDelete(): Promise> { + return { + value: null, + ok: 1, + }; + } + async findOneAndUpdate(): Promise> { return { value: null, diff --git a/apps/meteor/server/models/raw/BaseRaw.ts b/apps/meteor/server/models/raw/BaseRaw.ts index 1a3dd1a3eb4c..fae43f2dbfb0 100644 --- a/apps/meteor/server/models/raw/BaseRaw.ts +++ b/apps/meteor/server/models/raw/BaseRaw.ts @@ -26,6 +26,7 @@ import type { InsertOneResult, DeleteResult, DeleteOptions, + FindOneAndDeleteOptions, } from 'mongodb'; import { setUpdatedAt } from './setUpdatedAt'; @@ -315,7 +316,38 @@ export abstract class BaseRaw< return this.col.deleteOne(filter); } - async deleteMany(filter: Filter, options?: DeleteOptions): Promise { + async findOneAndDelete(filter: Filter, options?: FindOneAndDeleteOptions): Promise> { + if (!this.trash) { + if (options) { + return this.col.findOneAndDelete(filter, options); + } + return this.col.findOneAndDelete(filter); + } + + const result = await this.col.findOneAndDelete(filter); + + const { value: doc } = result; + if (!doc) { + return result; + } + + const { _id, ...record } = doc; + + const trash: TDeleted = { + ...record, + _deletedAt: new Date(), + __collection__: this.name, + } as unknown as TDeleted; + + // since the operation is not atomic, we need to make sure that the record is not already deleted/inserted + await this.trash?.updateOne({ _id } as Filter, { $set: trash } as UpdateFilter, { + upsert: true, + }); + + return result; + } + + async deleteMany(filter: Filter, options?: DeleteOptions & { onTrash?: (record: ResultFields) => void }): Promise { if (!this.trash) { if (options) { return this.col.deleteMany(filter, options); @@ -341,6 +373,8 @@ export abstract class BaseRaw< await this.trash?.updateOne({ _id } as Filter, { $set: trash } as UpdateFilter, { upsert: true, }); + + void options?.onTrash?.(doc); } if (options) { diff --git a/apps/meteor/server/models/raw/Roles.ts b/apps/meteor/server/models/raw/Roles.ts index 84a5b088ea30..4e1cb09348c4 100644 --- a/apps/meteor/server/models/raw/Roles.ts +++ b/apps/meteor/server/models/raw/Roles.ts @@ -3,6 +3,7 @@ import type { IRolesModel } from '@rocket.chat/model-typings'; import { Subscriptions, Users } from '@rocket.chat/models'; import type { Collection, FindCursor, Db, Filter, FindOptions, Document } from 'mongodb'; +import { notifyOnSubscriptionChangedByRoomIdAndUserId } from '../../../app/lib/server/lib/notifyListener'; import { BaseRaw } from './BaseRaw'; export class RolesRaw extends BaseRaw implements IRolesModel { @@ -35,14 +36,15 @@ export class RolesRaw extends BaseRaw implements IRolesModel { process.env.NODE_ENV === 'development' && console.warn(`[WARN] RolesRaw.addUserRoles: role: ${roleId} not found`); continue; } - switch (role.scope) { - case 'Subscriptions': - // TODO remove dependency from other models - this logic should be inside a function/service - await Subscriptions.addRolesByUserId(userId, [role._id], scope); - break; - case 'Users': - default: - await Users.addRolesByUserId(userId, [role._id]); + + if (role.scope === 'Subscriptions' && scope) { + // TODO remove dependency from other models - this logic should be inside a function/service + const addRolesResponse = await Subscriptions.addRolesByUserId(userId, [role._id], scope); + if (addRolesResponse.modifiedCount) { + void notifyOnSubscriptionChangedByRoomIdAndUserId(scope, userId); + } + } else { + await Users.addRolesByUserId(userId, [role._id]); } } return true; @@ -88,13 +90,13 @@ export class RolesRaw extends BaseRaw implements IRolesModel { continue; } - switch (role.scope) { - case 'Subscriptions': - scope && (await Subscriptions.removeRolesByUserId(userId, [roleId], scope)); - break; - case 'Users': - default: - await Users.removeRolesByUserId(userId, [roleId]); + if (role.scope === 'Subscriptions' && scope) { + const removeRolesResponse = await Subscriptions.removeRolesByUserId(userId, [roleId], scope); + if (removeRolesResponse.modifiedCount) { + void notifyOnSubscriptionChangedByRoomIdAndUserId(scope, userId); + } + } else { + await Users.removeRolesByUserId(userId, [roleId]); } } return true; diff --git a/apps/meteor/server/models/raw/Subscriptions.ts b/apps/meteor/server/models/raw/Subscriptions.ts index 01440a179c7f..efb75ed3d17f 100644 --- a/apps/meteor/server/models/raw/Subscriptions.ts +++ b/apps/meteor/server/models/raw/Subscriptions.ts @@ -191,7 +191,7 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri readThreads = false, alert = false, options: FindOptions = {}, - ): ReturnType['update']> { + ): ReturnType['updateOne']> { const query: Filter = { rid, 'u._id': uid, @@ -327,20 +327,62 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return this.find(query, options || {}); } - async removeByRoomId(roomId: string): Promise { + findByRoomIdAndNotAlertOrOpenExcludingUserIds( + { + roomId, + uidsExclude, + uidsInclude, + onlyRead, + }: { + roomId: ISubscription['rid']; + uidsExclude?: ISubscription['u']['_id'][]; + uidsInclude?: ISubscription['u']['_id'][]; + onlyRead: boolean; + }, + options?: FindOptions, + ) { const query = { rid: roomId, + ...(uidsExclude?.length && { + 'u._id': { $nin: uidsExclude }, + }), + ...(onlyRead && { + $or: [...(uidsInclude?.length ? [{ 'u._id': { $in: uidsInclude } }] : []), { alert: { $ne: true } }, { open: { $ne: true } }], + }), }; - const result = (await this.deleteMany(query)).deletedCount; + return this.find(query, options || {}); + } - if (typeof result === 'number' && result > 0) { - await Rooms.incUsersCountByIds([roomId], -result); + async removeByRoomId(roomId: ISubscription['rid'], options?: { onTrash: (doc: ISubscription) => void }): Promise { + const query = { + rid: roomId, + }; + + const deleteResult = await this.deleteMany(query, options); + + if (deleteResult?.deletedCount) { + await Rooms.incUsersCountByIds([roomId], -deleteResult.deletedCount); } await Users.removeRoomByRoomId(roomId); - return result; + return deleteResult; + } + + findByRoomIdExcludingUserIds( + roomId: ISubscription['rid'], + userIds: ISubscription['u']['_id'][], + options: FindOptions = {}, + ): FindCursor { + const query = { + 'rid': roomId, + 'u._id': { + $nin: userIds, + }, + }; + + return this.find(query, options); } async findConnectedUsersExcept( @@ -532,11 +574,10 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return this.updateMany(query, update); } - async setGroupE2EKey(_id: string, key: string): Promise { + async setGroupE2EKey(_id: string, key: string): Promise { const query = { _id }; const update = { $set: { E2EKey: key } }; - await this.updateOne(query, update); - return this.findOneById(_id); + return this.updateOne(query, update); } setGroupE2ESuggestedKey(uid: string, rid: string, key: string): Promise { @@ -558,18 +599,12 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return this.updateOne({ rid }, { $unset: { onHold: 1 } }); } - findByRoomIds(roomIds: string[]): FindCursor { + findByRoomIds(roomIds: ISubscription['u']['_id'][], options?: FindOptions): FindCursor { const query = { rid: { $in: roomIds, }, }; - const options = { - projection: { - 'u._id': 1, - 'rid': 1, - }, - }; return this.find(query, options); } @@ -582,6 +617,14 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return this.deleteMany(query); } + findByToken(token: string, options?: FindOptions): FindCursor { + const query = { + 'v.token': token, + }; + + return this.find(query, options); + } + updateAutoTranslateById(_id: string, autoTranslate: boolean): Promise { const query = { _id, @@ -620,6 +663,19 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return this.updateMany(query, update); } + findByAutoTranslateAndUserId( + userId: ISubscription['u']['_id'], + autoTranslate: ISubscription['autoTranslate'] = true, + options?: FindOptions, + ): FindCursor { + const query = { + 'u._id': userId, + autoTranslate, + }; + + return this.find(query, options); + } + disableAutoTranslateByRoomId(roomId: IRoom['_id']): Promise { const query = { rid: roomId, @@ -1092,7 +1148,11 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return subscription?.ls; } - findByRoomIdAndUserIds(roomId: string, userIds: string[], options?: FindOptions): FindCursor { + findByRoomIdAndUserIds( + roomId: ISubscription['rid'], + userIds: ISubscription['u']['_id'][], + options?: FindOptions, + ): FindCursor { const query = { 'rid': roomId, 'u._id': { @@ -1230,6 +1290,33 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return this.updateMany(query, update); } + findByUserIdAndRoomType( + userId: ISubscription['u']['_id'], + type: ISubscription['t'], + options?: FindOptions, + ): FindCursor { + const query = { + 'u._id': userId, + 't': type, + }; + + return this.find(query, options); + } + + findByNameAndRoomType( + filter: Partial>, + options?: FindOptions, + ): FindCursor { + if (!filter.name && !filter.t) { + throw new Error('invalid filter'); + } + const query: Filter = { + ...(filter.name && { name: filter.name }), + ...(filter.t && { t: filter.t }), + }; + return this.find(query, options); + } + setFavoriteByRoomIdAndUserId(roomId: string, userId: string, favorite?: boolean): Promise { if (favorite == null) { favorite = true; @@ -1411,7 +1498,7 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return this.updateOne(query, update); } - setAlertForRoomIdAndUserIds(roomId: string, uids: string[]): Promise { + setAlertForRoomIdAndUserIds(roomId: ISubscription['rid'], uids: ISubscription['u']['_id'][]): Promise { const query = { 'rid': roomId, 'u._id': { $in: uids }, @@ -1423,6 +1510,7 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri alert: true, }, }; + return this.updateMany(query, update); } @@ -1621,6 +1709,22 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return this.updateMany(query, update); } + findByUserPreferences( + userId: string, + notificationOriginField: keyof ISubscription, + notificationOriginValue: 'user' | 'subscription', + options?: FindOptions, + ): FindCursor { + const value = notificationOriginValue === 'user' ? 'user' : { $ne: 'subscription' }; + + const query: Filter = { + 'u._id': userId, + [notificationOriginField]: value, + }; + + return this.find(query, options); + } + updateUserHighlights(userId: string, userHighlights: any): Promise { const query: Filter = { 'u._id': userId, @@ -1722,9 +1826,7 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri })); // @ts-expect-error - types not good :( - const result = await this.insertMany(subscriptions); - - return result; + return this.insertMany(subscriptions); } // REMOVE @@ -1746,25 +1848,25 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return result; } - async removeByRoomIdAndUserId(roomId: string, userId: string): Promise { + async removeByRoomIdAndUserId(roomId: string, userId: string): Promise { const query = { 'rid': roomId, 'u._id': userId, }; - const result = (await this.deleteMany(query)).deletedCount; + const { value: doc } = await this.findOneAndDelete(query); - if (typeof result === 'number' && result > 0) { - await Rooms.incUsersCountById(roomId, -result); + if (doc) { + await Rooms.incUsersCountById(roomId, -1); } await Users.removeRoomByUserId(userId, roomId); - return result; + return doc; } - async removeByRoomIds(rids: string[]): Promise { - const result = await this.deleteMany({ rid: { $in: rids } }); + async removeByRoomIds(rids: string[], options?: { onTrash: (doc: ISubscription) => void }): Promise { + const result = await this.deleteMany({ rid: { $in: rids } }, options); await Users.removeRoomByRoomIds(rids); @@ -1850,6 +1952,19 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return this.updateMany(query, update); } + findUnreadThreadsByRoomId( + rid: ISubscription['rid'], + tunread: ISubscription['tunread'], + options?: FindOptions, + ): FindCursor { + const query = { + rid, + tunread: { $in: tunread }, + }; + + return this.find(query, options); + } + openByRoomIdAndUserId(roomId: string, userId: string): Promise { const query = { 'rid': roomId, diff --git a/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Room.ts b/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Room.ts index 91f5a6e66b43..9deabb53006e 100644 --- a/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Room.ts +++ b/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Room.ts @@ -7,6 +7,12 @@ import { saveRoomTopic } from '../../../../../../app/channel-settings/server'; import { addUserToRoom } from '../../../../../../app/lib/server/functions/addUserToRoom'; import { createRoom } from '../../../../../../app/lib/server/functions/createRoom'; import { removeUserFromRoom } from '../../../../../../app/lib/server/functions/removeUserFromRoom'; +import { + notifyOnSubscriptionChanged, + notifyOnSubscriptionChangedById, + notifyOnSubscriptionChangedByRoomId, + notifyOnSubscriptionChangedByRoomIdAndUserId, +} from '../../../../../../app/lib/server/lib/notifyListener'; import { settings } from '../../../../../../app/settings/server'; import { getDefaultSubscriptionPref } from '../../../../../../app/utils/lib/getDefaultSubscriptionPref'; import { getValidRoomName } from '../../../../../../app/utils/server/lib/getValidRoomName'; @@ -78,9 +84,16 @@ export class RocketChatRoomAdapter { public async removeDirectMessageRoom(federatedRoom: FederatedRoom): Promise { const roomId = federatedRoom.getInternalId(); - await Rooms.removeById(roomId); - await Subscriptions.removeByRoomId(roomId); - await MatrixBridgedRoom.removeByLocalRoomId(roomId); + + await Promise.all([ + Rooms.removeById(roomId), + Subscriptions.removeByRoomId(roomId, { + async onTrash(doc) { + void notifyOnSubscriptionChanged(doc, 'removed'); + }, + }), + MatrixBridgedRoom.removeByLocalRoomId(roomId), + ]); } public async createFederatedRoomForDirectMessage(federatedRoom: DirectMessageFederatedRoom): Promise { @@ -160,10 +173,13 @@ export class RocketChatRoomAdapter { } const user = federatedUser.getInternalReference(); - return Subscriptions.createWithRoomAndUser(room, user, { + const { insertedId } = await Subscriptions.createWithRoomAndUser(room, user, { ts: new Date(), ...getDefaultSubscriptionPref(user), }); + if (insertedId) { + void notifyOnSubscriptionChangedById(insertedId, 'inserted'); + } }) .filter(Boolean), ); @@ -182,33 +198,37 @@ export class RocketChatRoomAdapter { } public async updateRoomType(federatedRoom: FederatedRoom): Promise { - await Rooms.setRoomTypeById(federatedRoom.getInternalId(), federatedRoom.getRoomType()); - await Subscriptions.updateAllRoomTypesByRoomId(federatedRoom.getRoomType(), federatedRoom.getRoomType()); + const rid = federatedRoom.getInternalId(); + const roomType = federatedRoom.getRoomType(); + + await Rooms.setRoomTypeById(rid, roomType); + await Subscriptions.updateAllRoomTypesByRoomId(rid, roomType); + + void notifyOnSubscriptionChangedByRoomId(rid); } public async updateDisplayRoomName(federatedRoom: FederatedRoom, federatedUser: FederatedUser): Promise { - await Rooms.setFnameById(federatedRoom.getInternalId(), federatedRoom.getDisplayName()); - await Subscriptions.updateNameAndFnameByRoomId( - federatedRoom.getInternalId(), - federatedRoom.getName() || '', - federatedRoom.getDisplayName() || '', - ); + const rid = federatedRoom.getInternalId(); + const roomName = federatedRoom.getName() || ''; + const displayName = federatedRoom.getDisplayName() || ''; + const internalReference = federatedUser.getInternalReference(); - await Message.saveSystemMessage( - 'r', - federatedRoom.getInternalId(), - federatedRoom.getDisplayName() || '', - federatedUser.getInternalReference() as unknown as Required, // TODO fix type - ); + await Rooms.setFnameById(rid, displayName); + await Subscriptions.updateNameAndFnameByRoomId(rid, roomName, displayName); + await Message.saveSystemMessage('r', rid, displayName, internalReference); + + void notifyOnSubscriptionChangedByRoomId(rid); } public async updateRoomName(federatedRoom: FederatedRoom): Promise { - await Rooms.setRoomNameById(federatedRoom.getInternalId(), federatedRoom.getName()); - await Subscriptions.updateNameAndFnameByRoomId( - federatedRoom.getInternalId(), - federatedRoom.getName() || '', - federatedRoom.getDisplayName() || '', - ); + const rid = federatedRoom.getInternalId(); + const roomName = federatedRoom.getName() || ''; + const displayName = federatedRoom.getDisplayName() || ''; + + await Rooms.setRoomNameById(rid, roomName); + await Subscriptions.updateNameAndFnameByRoomId(rid, roomName, displayName); + + void notifyOnSubscriptionChangedByRoomId(rid); } public async updateRoomTopic(federatedRoom: FederatedRoom, federatedUser: FederatedUser): Promise { @@ -262,12 +282,15 @@ export class RocketChatRoomAdapter { rolesToRemove: ROCKET_CHAT_FEDERATION_ROLES[]; notifyChannel: boolean; }): Promise { - const subscription = await Subscriptions.findOneByRoomIdAndUserId(federatedRoom.getInternalId(), targetFederatedUser.getInternalId(), { - projection: { roles: 1 }, - }); + const uid = targetFederatedUser.getInternalId(); + const rid = federatedRoom.getInternalId(); + + const subscription = await Subscriptions.findOneByRoomIdAndUserId(rid, uid, { projection: { roles: 1 } }); + if (!subscription) { return; } + const { roles: currentRoles = [] } = subscription; const toAdd = rolesToAdd.filter((role) => !currentRoles.includes(role)); const toRemove = rolesToRemove.filter((role) => currentRoles.includes(role)); @@ -275,14 +298,19 @@ export class RocketChatRoomAdapter { _id: fromUser.getInternalId(), username: fromUser.getUsername(), }; + if (toAdd.length > 0) { - await Subscriptions.addRolesByUserId(targetFederatedUser.getInternalId(), toAdd, federatedRoom.getInternalId()); + const addRolesResponse = await Subscriptions.addRolesByUserId(uid, toAdd, rid); + if (addRolesResponse.modifiedCount) { + void notifyOnSubscriptionChangedByRoomIdAndUserId(rid, uid); + } + if (notifyChannel) { await Promise.all( toAdd.map((role) => Message.saveSystemMessage( 'subscription-role-added', - federatedRoom.getInternalId(), + rid, targetFederatedUser.getInternalReference().username || '', whoDidTheChange, { role }, @@ -291,14 +319,19 @@ export class RocketChatRoomAdapter { ); } } + if (toRemove.length > 0) { - await Subscriptions.removeRolesByUserId(targetFederatedUser.getInternalId(), toRemove, federatedRoom.getInternalId()); + const removeRolesResponse = await Subscriptions.removeRolesByUserId(uid, toRemove, rid); + if (removeRolesResponse.modifiedCount) { + void notifyOnSubscriptionChangedByRoomIdAndUserId(rid, uid); + } + if (notifyChannel) { await Promise.all( toRemove.map((role) => Message.saveSystemMessage( 'subscription-role-removed', - federatedRoom.getInternalId(), + rid, targetFederatedUser.getInternalReference().username || '', whoDidTheChange, { role }, @@ -307,6 +340,7 @@ export class RocketChatRoomAdapter { ); } } + if (settings.get('UI_DisplayRoles')) { this.notifyUIAboutRoomRolesChange(targetFederatedUser, federatedRoom, toAdd, toRemove); } diff --git a/apps/meteor/server/services/team/service.ts b/apps/meteor/server/services/team/service.ts index bc4211322b66..5ccf32ef779b 100644 --- a/apps/meteor/server/services/team/service.ts +++ b/apps/meteor/server/services/team/service.ts @@ -32,6 +32,7 @@ import { addUserToRoom } from '../../../app/lib/server/functions/addUserToRoom'; import { checkUsernameAvailability } from '../../../app/lib/server/functions/checkUsernameAvailability'; import { getSubscribedRoomsForUserWithDetails } from '../../../app/lib/server/functions/getRoomsWithSingleOwner'; import { removeUserFromRoom } from '../../../app/lib/server/functions/removeUserFromRoom'; +import { notifyOnSubscriptionChangedByRoomIdAndUserId } from '../../../app/lib/server/lib/notifyListener'; import { settings } from '../../../app/settings/server'; export class TeamService extends ServiceClassInternal implements ITeamService { @@ -745,7 +746,7 @@ export class TeamService extends ServiceClassInternal implements ITeamService { throw new Error('invalid-team'); } - await Promise.all([ + const responses = await Promise.all([ TeamMember.updateOneByUserIdAndTeamId(member.userId, teamId, memberUpdate), Subscriptions.updateOne( { @@ -757,6 +758,10 @@ export class TeamService extends ServiceClassInternal implements ITeamService { }, ), ]); + + if (responses[1].modifiedCount) { + void notifyOnSubscriptionChangedByRoomIdAndUserId(team.roomId, member.userId); + } } async removeMember(teamId: string, userId: string): Promise { diff --git a/apps/meteor/tests/unit/app/lib/server/functions/closeLivechatRoom.tests.ts b/apps/meteor/tests/unit/app/lib/server/functions/closeLivechatRoom.tests.ts index 07ee437832d2..b40b971128bc 100644 --- a/apps/meteor/tests/unit/app/lib/server/functions/closeLivechatRoom.tests.ts +++ b/apps/meteor/tests/unit/app/lib/server/functions/closeLivechatRoom.tests.ts @@ -73,7 +73,7 @@ describe('closeLivechatRoom', () => { it('should not perform any operation when a closed room with no subscriptions is provided and the caller is not subscribed to it', async () => { livechatRoomsStub.findOneById.resolves({ ...room, open: false }); - subscriptionsStub.countByRoomId.resolves(0); + subscriptionsStub.removeByRoomId.resolves({ deletedCount: 0 }); subscriptionsStub.findOneByRoomIdAndUserId.resolves(null); hasPermissionStub.resolves(true); @@ -81,13 +81,12 @@ describe('closeLivechatRoom', () => { expect(livechatStub.closeRoom.notCalled).to.be.true; expect(livechatRoomsStub.findOneById.calledOnceWith(room._id)).to.be.true; expect(subscriptionsStub.findOneByRoomIdAndUserId.notCalled).to.be.true; - expect(subscriptionsStub.countByRoomId.calledOnceWith(room._id)).to.be.true; - expect(subscriptionsStub.removeByRoomId.notCalled).to.be.true; + expect(subscriptionsStub.removeByRoomId.calledOnceWith(room._id)).to.be.true; }); it('should remove dangling subscription when a closed room with subscriptions is provided and the caller is not subscribed to it', async () => { livechatRoomsStub.findOneById.resolves({ ...room, open: false }); - subscriptionsStub.countByRoomId.resolves(1); + subscriptionsStub.removeByRoomId.resolves({ deletedCount: 1 }); subscriptionsStub.findOneByRoomIdAndUserId.resolves(null); hasPermissionStub.resolves(true); @@ -95,28 +94,25 @@ describe('closeLivechatRoom', () => { expect(livechatStub.closeRoom.notCalled).to.be.true; expect(livechatRoomsStub.findOneById.calledOnceWith(room._id)).to.be.true; expect(subscriptionsStub.findOneByRoomIdAndUserId.notCalled).to.be.true; - expect(subscriptionsStub.countByRoomId.calledOnceWith(room._id)).to.be.true; expect(subscriptionsStub.removeByRoomId.calledOnceWith(room._id)).to.be.true; }); it('should remove dangling subscription when a closed room is provided but the user is still subscribed to it', async () => { livechatRoomsStub.findOneById.resolves({ ...room, open: false }); subscriptionsStub.findOneByRoomIdAndUserId.resolves(subscription); - subscriptionsStub.countByRoomId.resolves(1); + subscriptionsStub.removeByRoomId.resolves({ deletedCount: 1 }); hasPermissionStub.resolves(true); await closeLivechatRoom(user, room._id, {}); expect(livechatStub.closeRoom.notCalled).to.be.true; expect(livechatRoomsStub.findOneById.calledOnceWith(room._id)).to.be.true; expect(subscriptionsStub.findOneByRoomIdAndUserId.notCalled).to.be.true; - expect(subscriptionsStub.countByRoomId.calledOnceWith(room._id)).to.be.true; expect(subscriptionsStub.removeByRoomId.calledOnceWith(room._id)).to.be.true; }); it('should not perform any operation when the caller is not subscribed to an open room and does not have the permission to close others rooms', async () => { livechatRoomsStub.findOneById.resolves(room); subscriptionsStub.findOneByRoomIdAndUserId.resolves(null); - subscriptionsStub.countByRoomId.resolves(1); hasPermissionStub.resolves(false); await expect(closeLivechatRoom(user, room._id, {})).to.be.rejectedWith('error-not-authorized'); @@ -129,7 +125,6 @@ describe('closeLivechatRoom', () => { it('should close the room when the caller is not subscribed to it but has the permission to close others rooms', async () => { livechatRoomsStub.findOneById.resolves(room); subscriptionsStub.findOneByRoomIdAndUserId.resolves(null); - subscriptionsStub.countByRoomId.resolves(1); hasPermissionStub.resolves(true); await closeLivechatRoom(user, room._id, {}); @@ -142,7 +137,6 @@ describe('closeLivechatRoom', () => { it('should close the room when the caller is subscribed to it and does not have the permission to close others rooms', async () => { livechatRoomsStub.findOneById.resolves(room); subscriptionsStub.findOneByRoomIdAndUserId.resolves(subscription); - subscriptionsStub.countByRoomId.resolves(1); hasPermissionStub.resolves(false); await closeLivechatRoom(user, room._id, {}); diff --git a/packages/model-typings/src/models/IBaseModel.ts b/packages/model-typings/src/models/IBaseModel.ts index 246c3ae253dd..626f91385a04 100644 --- a/packages/model-typings/src/models/IBaseModel.ts +++ b/packages/model-typings/src/models/IBaseModel.ts @@ -9,6 +9,7 @@ import type { EnhancedOmit, Filter, FindCursor, + FindOneAndDeleteOptions, FindOneAndUpdateOptions, FindOptions, InsertManyResult, @@ -53,6 +54,7 @@ export interface IBaseModel< getUpdater(): Updater; updateFromUpdater(query: Filter, updater: Updater): Promise; + findOneAndDelete(filter: Filter, options?: FindOneAndDeleteOptions): Promise>; findOneAndUpdate(query: Filter, update: UpdateFilter | T, options?: FindOneAndUpdateOptions): Promise>; findOneById(_id: T['_id'], options?: FindOptions | undefined): Promise; @@ -93,7 +95,7 @@ export interface IBaseModel< deleteOne(filter: Filter, options?: DeleteOptions & { bypassDocumentValidation?: boolean }): Promise; - deleteMany(filter: Filter, options?: DeleteOptions): Promise; + deleteMany(filter: Filter, options?: DeleteOptions & { onTrash?: (record: ResultFields) => void }): Promise; // Trash trashFind

( diff --git a/packages/model-typings/src/models/ISubscriptionsModel.ts b/packages/model-typings/src/models/ISubscriptionsModel.ts index 91398e77ebe4..3703996898f6 100644 --- a/packages/model-typings/src/models/ISubscriptionsModel.ts +++ b/packages/model-typings/src/models/ISubscriptionsModel.ts @@ -41,7 +41,7 @@ export interface ISubscriptionsModel extends IBaseModel { readThreads?: boolean, alert?: boolean, options?: FindOptions, - ): ReturnType['update']>; + ): ReturnType['updateOne']>; removeRolesByUserId(uid: IUser['_id'], roles: IRole['_id'][], rid: IRoom['_id']): Promise; @@ -73,7 +73,23 @@ export interface ISubscriptionsModel extends IBaseModel { findByUserIdAndTypes(userId: string, types: ISubscription['t'][], options?: FindOptions): FindCursor; - removeByRoomId(roomId: string): Promise; + findByRoomIdAndNotAlertOrOpenExcludingUserIds( + filter: { + roomId: ISubscription['rid']; + uidsExclude?: ISubscription['u']['_id'][]; + uidsInclude?: ISubscription['u']['_id'][]; + onlyRead: boolean; + }, + options?: FindOptions, + ): FindCursor; + + removeByRoomId(roomId: ISubscription['rid'], options?: { onTrash: (doc: ISubscription) => void }): Promise; + + findByRoomIdExcludingUserIds( + roomId: ISubscription['rid'], + userIds: ISubscription['u']['_id'][], + options?: FindOptions, + ): FindCursor; findConnectedUsersExcept( userId: string, @@ -95,7 +111,7 @@ export interface ISubscriptionsModel extends IBaseModel { updateNameAndFnameByRoomId(roomId: string, name: string, fname: string): Promise; - setGroupE2EKey(_id: string, key: string): Promise; + setGroupE2EKey(_id: string, key: string): Promise; setGroupE2ESuggestedKey(uid: string, rid: string, key: string): Promise; @@ -119,9 +135,10 @@ export interface ISubscriptionsModel extends IBaseModel { updateAutoTranslateLanguageById(_id: string, autoTranslateLanguage: string): Promise; removeByVisitorToken(token: string): Promise; + findByToken(token: string, options?: FindOptions): FindCursor; updateMuteGroupMentions(_id: string, muteGroupMentions: boolean): Promise; - findByRoomIds(roomIds: string[]): FindCursor; + findByRoomIds(roomIds: ISubscription['u']['_id'][], options?: FindOptions): FindCursor; changeDepartmentByRoomId(rid: string, department: string): Promise; roleBaseQuery(userId: string, scope?: string): Filter | void; @@ -137,7 +154,24 @@ export interface ISubscriptionsModel extends IBaseModel { findByUserId(userId: string, options?: FindOptions): FindCursor; cachedFindByUserId(userId: string, options?: FindOptions): FindCursor; updateAutoTranslateById(_id: string, autoTranslate: boolean): Promise; + updateAllAutoTranslateLanguagesByUserId(userId: IUser['_id'], language: string): Promise; + findByAutoTranslateAndUserId( + userId: ISubscription['u']['_id'], + autoTranslate?: ISubscription['autoTranslate'], + options?: FindOptions, + ): FindCursor; + + findByUserIdAndRoomType( + userId: ISubscription['u']['_id'], + type: ISubscription['t'], + options?: FindOptions, + ): FindCursor; + findByNameAndRoomType( + filter: Partial>, + options?: FindOptions, + ): FindCursor; + disableAutoTranslateByRoomId(roomId: IRoom['_id']): Promise; findAlwaysNotifyDesktopUsersByRoomId(roomId: string): FindCursor; @@ -166,7 +200,11 @@ export interface ISubscriptionsModel extends IBaseModel { options?: FindOptions, ): FindCursor; findByRoomIdAndRoles(roomId: string, roles: string[], options?: FindOptions): FindCursor; - findByRoomIdAndUserIds(roomId: string, userIds: string[], options?: FindOptions): FindCursor; + findByRoomIdAndUserIds( + roomId: ISubscription['rid'], + userIds: ISubscription['u']['_id'][], + options?: FindOptions, + ): FindCursor; findByUserIdUpdatedAfter(userId: string, updatedAt: Date, options?: FindOptions): FindCursor; findByRoomIdAndUserIdsOrAllMessages(roomId: string, userIds: string[]): FindCursor; @@ -203,7 +241,7 @@ export interface ISubscriptionsModel extends IBaseModel { updateCustomFieldsByRoomId(rid: string, cfields: Record): Promise; setOpenForRoomIdAndUserIds(roomId: string, uids: string[]): Promise; - setAlertForRoomIdAndUserIds(roomId: string, uids: string[]): Promise; + setAlertForRoomIdAndUserIds(roomId: ISubscription['rid'], uids: ISubscription['u']['_id'][]): Promise; updateTypeByRoomId(roomId: string, type: ISubscription['t']): Promise; setBlockedByRoomId(rid: string, blocked: string, blocker: string): Promise; incUserMentionsAndUnreadForRoomIdAndUserIds( @@ -227,6 +265,12 @@ export interface ISubscriptionsModel extends IBaseModel { notificationField: keyof ISubscription, notificationOriginField: keyof ISubscription, ): Promise; + findByUserPreferences( + userId: string, + notificationOriginField: keyof ISubscription, + originFieldNotEqualValue: 'user' | 'subscription', + options?: FindOptions, + ): FindCursor; clearNotificationUserPreferences( userId: string, notificationField: string, @@ -239,9 +283,9 @@ export interface ISubscriptionsModel extends IBaseModel { users: { user: AtLeast; extraData: Record }[], ): Promise>; removeByRoomIdsAndUserId(rids: string[], userId: string): Promise; - removeByRoomIdAndUserId(roomId: string, userId: string): Promise; + removeByRoomIdAndUserId(roomId: string, userId: string): Promise; - removeByRoomIds(rids: string[]): Promise; + removeByRoomIds(rids: string[], options?: { onTrash: (doc: ISubscription) => void }): Promise; addUnreadThreadByRoomIdAndUserIds( rid: string, @@ -252,6 +296,12 @@ export interface ISubscriptionsModel extends IBaseModel { removeUnreadThreadByRoomIdAndUserId(rid: string, userId: string, tmid: string, clearAlert?: boolean): Promise; removeUnreadThreadsByRoomId(rid: string, tunread: string[]): Promise; + findUnreadThreadsByRoomId( + rid: ISubscription['rid'], + tunread: ISubscription['tunread'], + options?: FindOptions, + ): FindCursor; + countByRoomIdAndRoles(roomId: string, roles: string[]): Promise; countByRoomId(roomId: string): Promise; countByUserId(userId: string): Promise;