diff --git a/.changeset/eighty-pumas-float.md b/.changeset/eighty-pumas-float.md new file mode 100644 index 0000000000000..8a8c5726691be --- /dev/null +++ b/.changeset/eighty-pumas-float.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +fix(rooms): Update lastMessage to previous valid message on user deletion diff --git a/apps/meteor/app/lib/server/functions/deleteUser.ts b/apps/meteor/app/lib/server/functions/deleteUser.ts index 620726da5a722..9039de2e6dde5 100644 --- a/apps/meteor/app/lib/server/functions/deleteUser.ts +++ b/apps/meteor/app/lib/server/functions/deleteUser.ts @@ -40,7 +40,7 @@ export async function deleteUser(userId: string, confirmRelinquish = false, dele } const user = await Users.findOneById(userId, { - projection: { username: 1, avatarOrigin: 1, roles: 1, federated: 1 }, + projection: { username: 1, name: 1, avatarOrigin: 1, roles: 1, federated: 1 }, }); if (!user) { @@ -61,18 +61,21 @@ export async function deleteUser(userId: string, confirmRelinquish = false, dele } let deletedRooms: string[] = []; + const affectedRoomIds: string[] = []; + // Users without username can't do anything, so there is nothing to remove if (user.username != null) { let userToReplaceWhenUnlinking: IUser | null = null; const nameAlias = i18n.t('Removed_User'); - deletedRooms = await relinquishRoomOwnerships(userId, subscribedRooms, true); + deletedRooms = await relinquishRoomOwnerships(userId, subscribedRooms); const messageErasureType = settings.get<'Delete' | 'Unlink' | 'Keep'>('Message_ErasureType'); switch (messageErasureType) { - case 'Delete': + case 'Delete': { const store = FileUpload.getStore('Uploads'); const cursor = Messages.findFilesByUserId(userId); + // New Rocket.Chat Logic for File Deletion for await (const { file, files } of cursor) { const fileIds = files?.map(({ _id }) => _id) || []; for await (const fileId of fileIds) { @@ -87,6 +90,20 @@ export async function deleteUser(userId: string, confirmRelinquish = false, dele } await Messages.removeByUserId(userId); + + // Our Fix: Update lastMessage for rooms + const roomsToUpdate = await Rooms.find({ 'lastMessage.u._id': userId }, { projection: { _id: 1 } }).toArray(); + for await (const room of roomsToUpdate) { + affectedRoomIds.push(room._id); + const [newLastMessage] = await Messages.find({ rid: room._id }, { sort: { ts: -1 }, limit: 1 }).toArray(); + const filter = { _id: room._id, 'lastMessage.u._id': userId }; + if (newLastMessage) { + await Rooms.updateOne(filter, { $set: { lastMessage: newLastMessage } }); + } else { + await Rooms.updateOne(filter, { $unset: { lastMessage: 1 } }); + } + } + await ReadReceipts.removeByUserId(userId); await ModerationReports.hideMessageReportsByUserId( @@ -97,20 +114,37 @@ export async function deleteUser(userId: string, confirmRelinquish = false, dele ); break; - case 'Unlink': + } + case 'Unlink': { userToReplaceWhenUnlinking = await Users.findOneById('rocket.cat'); if (!userToReplaceWhenUnlinking?._id || !userToReplaceWhenUnlinking?.username) { break; } await Messages.unlinkUserId(userId, userToReplaceWhenUnlinking?._id, userToReplaceWhenUnlinking?.username, nameAlias); + + // Our Fix: Update lastMessage for rooms in Unlink case too + const roomsToUpdateUnlink = await Rooms.find({ 'lastMessage.u._id': userId }, { projection: { _id: 1 } }).toArray(); + for await (const room of roomsToUpdateUnlink) { + affectedRoomIds.push(room._id); + const [newLastMessage] = await Messages.find({ rid: room._id }, { sort: { ts: -1 }, limit: 1 }).toArray(); + const filter = { _id: room._id, 'lastMessage.u._id': userId }; + if (newLastMessage) { + await Rooms.updateOne(filter, { $set: { lastMessage: newLastMessage } }); + } else { + await Rooms.updateOne(filter, { $unset: { lastMessage: 1 } }); + } + } + break; + } } await Rooms.updateGroupDMsRemovingUsernamesByUsername(user.username, userId); // Remove direct rooms with the user await Rooms.removeDirectRoomContainingUsername(user.username); // Remove direct rooms with the user const rids = subscribedRooms.map((room) => room.rid); - void notifyOnRoomChangedById(rids); + const allAffectedRids = [...new Set([...rids, ...affectedRoomIds])]; + void notifyOnRoomChangedById(allAffectedRids); await Subscriptions.removeByUserId(userId); @@ -185,4 +219,4 @@ export async function deleteUser(userId: string, confirmRelinquish = false, dele await callbacks.run('afterDeleteUser', user); return { deletedRooms }; -} +} \ No newline at end of file diff --git a/packages/models/src/models/Rooms.ts b/packages/models/src/models/Rooms.ts index 9bcac18cf72fe..090a5dcf78e6f 100644 --- a/packages/models/src/models/Rooms.ts +++ b/packages/models/src/models/Rooms.ts @@ -114,6 +114,10 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { key: { teamMain: 1 }, sparse: true, }, + { + key: { 'lastMessage.u._id': 1 }, + sparse: true, + }, ]; } diff --git a/yarn.lock b/yarn.lock index 87ef47de32823..362c367a98294 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8957,7 +8957,7 @@ __metadata: express: "npm:^4.21.2" hono: "npm:^4.10.7" jest: "npm:~30.2.0" - qs: "npm:^6.14.0" + qs: "npm:^6.14.1" supertest: "npm:~7.1.4" ts-jest: "npm:~29.4.5" typescript: "npm:~5.9.3" @@ -9634,7 +9634,7 @@ __metadata: proxy-from-env: "npm:^1.1.0" proxyquire: "npm:^2.1.3" psl: "npm:^1.10.0" - qs: "npm:^6.14.0" + qs: "npm:^6.14.1" query-string: "npm:^7.1.3" queue-fifo: "npm:^0.2.6" raw-loader: "npm:~4.0.2" @@ -31809,7 +31809,7 @@ __metadata: languageName: node linkType: hard -"qs@npm:^6.11.2, qs@npm:^6.12.3, qs@npm:^6.14.0, qs@npm:^6.9.4": +"qs@npm:^6.11.2, qs@npm:^6.12.3, qs@npm:^6.9.4": version: 6.14.0 resolution: "qs@npm:6.14.0" dependencies: @@ -31818,6 +31818,15 @@ __metadata: languageName: node linkType: hard +"qs@npm:^6.14.1": + version: 6.14.1 + resolution: "qs@npm:6.14.1" + dependencies: + side-channel: "npm:^1.1.0" + checksum: 10/34b5ab00a910df432d55180ef39c1d1375e550f098b5ec153b41787f1a6a6d7e5f9495593c3b112b77dbc6709d0ae18e55b82847a4c2bbbb0de1e8ccbb1794c5 + languageName: node + linkType: hard + "qs@npm:~6.5.2": version: 6.5.3 resolution: "qs@npm:6.5.3"