diff --git a/.changeset/cold-insects-guess.md b/.changeset/cold-insects-guess.md new file mode 100644 index 0000000000000..e8be819c54692 --- /dev/null +++ b/.changeset/cold-insects-guess.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/core-services': patch +'@rocket.chat/ddp-client': patch +'@rocket.chat/meteor': patch +--- + +Fixes missing UI updates after pruning messages with "Files only" enabled. diff --git a/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts b/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts index 4d09e09fc2de0..7acc06f2ba3cd 100644 --- a/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts +++ b/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts @@ -1,5 +1,5 @@ import { api } from '@rocket.chat/core-services'; -import type { IRoom, MessageAttachment } from '@rocket.chat/core-typings'; +import type { IRoom } from '@rocket.chat/core-typings'; import { Messages, Rooms, Subscriptions, ReadReceipts, Users } from '@rocket.chat/models'; import { deleteRoom } from './deleteRoom'; @@ -8,14 +8,6 @@ import { FileUpload } from '../../../file-upload/server'; import { notifyOnRoomChangedById, notifyOnSubscriptionChangedById } from '../lib/notifyListener'; const FILE_CLEANUP_BATCH_SIZE = 1000; -async function performFileAttachmentCleanupBatch(idsSet: Set, replaceWith?: MessageAttachment) { - if (idsSet.size === 0) return; - - const ids = [...idsSet]; - await Messages.removeFileAttachmentsByMessageIds(ids, replaceWith); - await Messages.clearFilesByMessageIds(ids); - idsSet.clear(); -} export async function cleanRoomHistory({ rid = '', @@ -55,6 +47,26 @@ export async function cleanRoomHistory({ }); const targetMessageIdsForAttachmentRemoval = new Set(); + const pruneMessageAttachment = { color: '#FD745E', text }; + + async function performFileAttachmentCleanupBatch() { + if (targetMessageIdsForAttachmentRemoval.size === 0) return; + + const ids = [...targetMessageIdsForAttachmentRemoval]; + await Messages.removeFileAttachmentsByMessageIds(ids, pruneMessageAttachment); + await Messages.clearFilesByMessageIds(ids); + void api.broadcast('notify.deleteMessageBulk', rid, { + rid, + excludePinned, + ignoreDiscussion, + ts, + users: fromUsers, + ids, + filesOnly: true, + replaceFileAttachmentsWith: pruneMessageAttachment, + }); + targetMessageIdsForAttachmentRemoval.clear(); + } for await (const document of cursor) { const uploadsStore = FileUpload.getStore('Uploads'); @@ -67,12 +79,12 @@ export async function cleanRoomHistory({ } if (targetMessageIdsForAttachmentRemoval.size >= FILE_CLEANUP_BATCH_SIZE) { - await performFileAttachmentCleanupBatch(targetMessageIdsForAttachmentRemoval, { color: '#FD745E', text }); + await performFileAttachmentCleanupBatch(); } } if (targetMessageIdsForAttachmentRemoval.size > 0) { - await performFileAttachmentCleanupBatch(targetMessageIdsForAttachmentRemoval, { color: '#FD745E', text }); + await performFileAttachmentCleanupBatch(); } if (filesOnly) { diff --git a/apps/meteor/app/ui-utils/client/lib/LegacyRoomManager.ts b/apps/meteor/app/ui-utils/client/lib/LegacyRoomManager.ts index 4709abb67990c..89dc39e07f38d 100644 --- a/apps/meteor/app/ui-utils/client/lib/LegacyRoomManager.ts +++ b/apps/meteor/app/ui-utils/client/lib/LegacyRoomManager.ts @@ -8,6 +8,7 @@ import { RoomManager } from '../../../../client/lib/RoomManager'; import { roomCoordinator } from '../../../../client/lib/rooms/roomCoordinator'; import { fireGlobalEvent } from '../../../../client/lib/utils/fireGlobalEvent'; import { getConfig } from '../../../../client/lib/utils/getConfig'; +import { modifyMessageOnFilesDelete } from '../../../../client/lib/utils/modifyMessageOnFilesDelete'; import { callbacks } from '../../../../lib/callbacks'; import { Messages, Subscriptions } from '../../../models/client'; import { sdk } from '../../../utils/client/lib/SDKClient'; @@ -110,6 +111,41 @@ function getOpenedRoomByRid(rid: IRoom['_id']) { .find((openedRoom) => openedRoom.rid === rid); } +function createDeleteQuery({ + excludePinned, + ignoreDiscussion, + rid, + ts, + users, + ids, +}: { + rid: IMessage['rid']; + excludePinned: boolean; + ignoreDiscussion: boolean; + ts: Record; + users: string[]; + ids?: string[]; +}) { + const query: Filter = { rid }; + + if (ids) { + query._id = { $in: ids }; + } else { + query.ts = ts; + } + if (excludePinned) { + query.pinned = { $ne: true }; + } + if (ignoreDiscussion) { + query.drid = { $exists: false }; + } + if (users?.length) { + query['u.username'] = { $in: users }; + } + + return query; +} + const openRoom = (typeName: string, record: OpenedRoom) => { if (record.ready === true && record.streamActive === true) { return; @@ -174,44 +210,28 @@ const openRoom = (typeName: string, record: OpenedRoom) => { ({ tmid: _, ...record }) => record, ); }), - sdk.stream( - 'notify-room', - [`${record.rid}/deleteMessageBulk`], - ({ rid, ts, excludePinned, ignoreDiscussion, users, ids, showDeletedStatus }) => { - const query: Filter = { rid }; - - if (ids) { - query._id = { $in: ids }; - } else { - query.ts = ts; - } - if (excludePinned) { - query.pinned = { $ne: true }; - } - if (ignoreDiscussion) { - query.drid = { $exists: false }; - } - if (users?.length) { - query['u.username'] = { $in: users }; - } + sdk.stream('notify-room', [`${record.rid}/deleteMessageBulk`], async (params) => { + const query = createDeleteQuery(params); + const predicate = createPredicateFromFilter(query); - const predicate = createPredicateFromFilter(query); - - if (showDeletedStatus) { - return Messages.state.update(predicate, (record) => ({ - ...record, - t: 'rm', - msg: '', - urls: [], - mentions: [], - attachments: [], - reactions: {}, - })); - } + if (params.filesOnly) { + return Messages.state.update(predicate, (record) => modifyMessageOnFilesDelete(record, params.replaceFileAttachmentsWith)); + } + + if (params.showDeletedStatus) { + return Messages.state.update(predicate, (record) => ({ + ...record, + t: 'rm', + msg: '', + urls: [], + mentions: [], + attachments: [], + reactions: {}, + })); + } - return Messages.state.remove(predicate); - }, - ), + return Messages.state.remove(predicate); + }), sdk.stream('notify-room', [`${record.rid}/messagesRead`], ({ tmid, until }) => { if (tmid) { diff --git a/apps/meteor/client/hooks/lists/useStreamUpdatesForMessageList.ts b/apps/meteor/client/hooks/lists/useStreamUpdatesForMessageList.ts index 1ed370ca6b84b..a3675c638d93a 100644 --- a/apps/meteor/client/hooks/lists/useStreamUpdatesForMessageList.ts +++ b/apps/meteor/client/hooks/lists/useStreamUpdatesForMessageList.ts @@ -1,22 +1,30 @@ -import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom, IUser, MessageAttachment } from '@rocket.chat/core-typings'; import { createPredicateFromFilter } from '@rocket.chat/mongo-adapter'; import { useStream } from '@rocket.chat/ui-contexts'; import type { Condition, Filter } from 'mongodb'; import { useEffect } from 'react'; import type { MessageList } from '../../lib/lists/MessageList'; +import { modifyMessageOnFilesDelete } from '../../lib/utils/modifyMessageOnFilesDelete'; -type NotifyRoomRidDeleteMessageBulkEvent = { +type NotifyRoomRidDeleteBulkEvent = { rid: IMessage['rid']; excludePinned: boolean; ignoreDiscussion: boolean; ts: Condition; users: string[]; ids?: string[]; // message ids have priority over ts - showDeletedStatus?: boolean; -}; +} & ( + | { + filesOnly: true; + replaceFileAttachmentsWith?: MessageAttachment; + } + | { + filesOnly?: false; + } +); -const createDeleteCriteria = (params: NotifyRoomRidDeleteMessageBulkEvent): ((message: IMessage) => boolean) => { +const createDeleteCriteria = (params: NotifyRoomRidDeleteBulkEvent): ((message: IMessage) => boolean) => { const query: Filter = {}; if (params.ids) { @@ -59,6 +67,14 @@ export const useStreamUpdatesForMessageList = (messageList: MessageList, uid: IU const unsubscribeFromDeleteMessageBulk = subscribeToNotifyRoom(`${rid}/deleteMessageBulk`, (params) => { const matchDeleteCriteria = createDeleteCriteria(params); + if (params.filesOnly) { + return messageList.batchHandle(async () => { + const items = messageList.items.filter(matchDeleteCriteria).map((message) => { + return modifyMessageOnFilesDelete(message, params.replaceFileAttachmentsWith); + }); + return { items }; + }); + } messageList.prune(matchDeleteCriteria); }); diff --git a/apps/meteor/client/lib/utils/modifyMessageOnFilesDelete.spec.ts b/apps/meteor/client/lib/utils/modifyMessageOnFilesDelete.spec.ts new file mode 100644 index 0000000000000..32985ee2c938c --- /dev/null +++ b/apps/meteor/client/lib/utils/modifyMessageOnFilesDelete.spec.ts @@ -0,0 +1,92 @@ +import type { IMessage, MessageAttachment, FileAttachmentProps, MessageQuoteAttachment } from '@rocket.chat/core-typings'; + +import { modifyMessageOnFilesDelete } from './modifyMessageOnFilesDelete'; + +global.structuredClone = (val: any) => JSON.parse(JSON.stringify(val)); + +const fileAttachment: FileAttachmentProps = { title: 'Image', title_link: 'url', image_url: 'image.png', type: 'file' }; +const nonFileAttachment: MessageAttachment = { text: 'Non-file attachment', color: '#ff0000' }; +const quoteAttachment: MessageQuoteAttachment = { + text: 'Quoted message', + message_link: 'some-link', + attachments: [{ text: 'Quoted inner message' }], + author_icon: 'icon', + author_link: 'link', + author_name: 'name', +}; + +const messageBase: IMessage = { + _id: 'msg1', + msg: 'Here is a file', + file: { + _id: 'file1', + name: 'image.png', + type: '', + format: '', + size: 0, + }, + files: [ + { + _id: 'file1', + name: 'image.png', + type: '', + format: '', + size: 0, + }, + ], + attachments: [fileAttachment, nonFileAttachment, quoteAttachment], + rid: '', + ts: new Date().toISOString() as any, + u: { username: 'username', _id: '12345' }, + _updatedAt: new Date().toISOString() as any, +}; + +const createMessage = () => { + const msg = structuredClone(messageBase); + return msg as IMessage; +}; + +describe('modifyMessageOnFilesDelete', () => { + it('should remove `file`, empty `files`, and remove file-type attachments', () => { + const message = createMessage(); + + const result = modifyMessageOnFilesDelete(message); + + expect(result).not.toHaveProperty('file'); + expect(Array.isArray(result.files)).toBe(true); + expect(result.files).toHaveLength(0); + expect(result.attachments).toHaveLength(2); + expect(result.attachments?.some((att) => att.text === 'Non-file attachment')).toBe(true); + }); + + it('should replace file-type attachments if `replaceFileAttachmentsWith` is provided', () => { + const message = createMessage(); + + const replacement: MessageAttachment = { text: 'File removed by prune' }; + + const result = modifyMessageOnFilesDelete(message, replacement); + + expect(result).not.toHaveProperty('file'); + expect(Array.isArray(result.files)).toBe(true); + expect(result.files).toHaveLength(0); + expect(result.attachments?.some((att) => att.text === 'File removed by prune')).toBe(true); + }); + + it('should not mutate the original message', () => { + const message = createMessage(); + + const original = JSON.parse(JSON.stringify(message)); + modifyMessageOnFilesDelete(message); + + expect(message).toEqual(original); + }); + + it('should not remove non-file attachments such as text and quote', () => { + const message = createMessage(); + + const result = modifyMessageOnFilesDelete(message); + + expect(result.attachments?.some((att) => att.text === 'Non-file attachment')).toBe(true); + expect(result.attachments?.some((att) => att.text === 'Quoted message')).toBe(true); + }); +}); diff --git a/apps/meteor/client/lib/utils/modifyMessageOnFilesDelete.ts b/apps/meteor/client/lib/utils/modifyMessageOnFilesDelete.ts new file mode 100644 index 0000000000000..e280a999556fa --- /dev/null +++ b/apps/meteor/client/lib/utils/modifyMessageOnFilesDelete.ts @@ -0,0 +1,30 @@ +import { isFileAttachment } from '@rocket.chat/core-typings'; +import type { IMessage, MessageAttachment } from '@rocket.chat/core-typings'; + +/** + * Clone a message and clear or replace its file attachments. + * + * - Performs a deep clone via `structuredClone` to avoid mutating the source. + * - Removes the single-file `file` field and empties the `files` array. + * - If `replaceFileAttachmentsWith` is given, swaps out any file-type + * attachments; otherwise filters them out entirely. + * + * @param message The original `IMessage` to copy. + * @param replaceFileAttachmentsWith + * Optional attachment to substitute for each file attachment. + * @returns A new message object with `file`, `files`, and `attachments` updated. + */ + +export const modifyMessageOnFilesDelete = (message: T, replaceFileAttachmentsWith?: MessageAttachment) => { + const { attachments, file: _, ...rest } = message; + + if (!attachments?.length) { + return message; + } + + if (replaceFileAttachmentsWith) { + return { ...rest, files: [], attachments: attachments?.map((att) => (isFileAttachment(att) ? replaceFileAttachmentsWith : att)) }; + } + + return { ...rest, files: [], attachments: attachments?.filter((att) => !isFileAttachment(att)) }; +}; diff --git a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useThreadMainMessageQuery.ts b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useThreadMainMessageQuery.ts index d557803cc9672..f5a24bc618a91 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useThreadMainMessageQuery.ts +++ b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useThreadMainMessageQuery.ts @@ -1,4 +1,4 @@ -import type { IMessage, IThreadMainMessage } from '@rocket.chat/core-typings'; +import type { IMessage, IThreadMainMessage, MessageAttachment } from '@rocket.chat/core-typings'; import { createPredicateFromFilter } from '@rocket.chat/mongo-adapter'; import { useStream } from '@rocket.chat/ui-contexts'; import type { UseQueryResult } from '@tanstack/react-query'; @@ -9,11 +9,12 @@ import { useCallback, useEffect, useRef } from 'react'; import { useGetMessageByID } from './useGetMessageByID'; import { withDebouncing } from '../../../../../../lib/utils/highOrderFunctions'; import { onClientMessageReceived } from '../../../../../lib/onClientMessageReceived'; +import { modifyMessageOnFilesDelete } from '../../../../../lib/utils/modifyMessageOnFilesDelete'; import { useRoom } from '../../../contexts/RoomContext'; type RoomMessagesRidEvent = IMessage; -type NotifyRoomRidDeleteMessageBulkEvent = { +type NotifyRoomRidDeleteBulkEvent = { rid: IMessage['rid']; excludePinned: boolean; ignoreDiscussion: boolean; @@ -21,9 +22,17 @@ type NotifyRoomRidDeleteMessageBulkEvent = { users: string[]; ids?: string[]; // message ids have priority over ts showDeletedStatus?: boolean; -}; - -const createDeleteCriteria = (params: NotifyRoomRidDeleteMessageBulkEvent): ((message: IMessage) => boolean) => { +} & ( + | { + filesOnly: true; + replaceFileAttachmentsWith?: MessageAttachment; + } + | { + filesOnly?: false; + } +); + +const createDeleteCriteria = (params: NotifyRoomRidDeleteBulkEvent): ((message: IMessage) => boolean) => { const query: Filter = {}; if (params.ids) { @@ -51,7 +60,18 @@ const useSubscribeToMessage = () => { const subscribeToNotifyRoom = useStream('notify-room'); return useCallback( - (message: IMessage, { onMutate, onDelete }: { onMutate?: (message: IMessage) => void; onDelete?: () => void }) => { + ( + message: IMessage, + { + onMutate, + onDelete, + onFilesDelete, + }: { + onMutate?: (message: IMessage) => void; + onDelete?: () => void; + onFilesDelete?: (replaceFileAttachmentsWith?: MessageAttachment) => void; + }, + ) => { const unsubscribeFromRoomMessages = subscribeToRoomMessages(message.rid, (event: RoomMessagesRidEvent) => { if (message._id === event._id) onMutate?.(event); }); @@ -62,7 +82,12 @@ const useSubscribeToMessage = () => { const unsubscribeFromDeleteMessageBulk = subscribeToNotifyRoom(`${message.rid}/deleteMessageBulk`, (params) => { const matchDeleteCriteria = createDeleteCriteria(params); - if (matchDeleteCriteria(message)) onDelete?.(); + if (matchDeleteCriteria(message)) { + if (params.filesOnly) { + return onFilesDelete?.(params.replaceFileAttachmentsWith); + } + return onDelete?.(); + } }); return () => { @@ -120,6 +145,17 @@ export const useThreadMainMessageQuery = ( onDelete?.(); queryClient.invalidateQueries({ queryKey, exact: true }); }, + onFilesDelete: async (replaceFileAttachmentsWith?: MessageAttachment) => { + const current = queryClient.getQueryData(queryKey); + if (!current) { + return; + } + const updated = modifyMessageOnFilesDelete(current, replaceFileAttachmentsWith); + + const msg = await onClientMessageReceived(updated); + queryClient.setQueryData(queryKey, () => msg); + debouncedInvalidate(); + }, }); return mainMessage; diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts index 5980445b97e09..cae0a54e89c55 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -184,6 +184,10 @@ export class HomeContent { return this.page.locator('[data-qa-type="message"]:last-child [data-qa-type="attachment-title-link"]'); } + get lastMessageTextAttachment(): Locator { + return this.page.locator('[data-qa-type="message"]:last-child [data-qa-type="message-attachment"]'); + } + get lastMessageTextAttachmentEqualsText(): Locator { return this.page.locator('[data-qa-type="message"]:last-child .rcx-attachment__details .rcx-message-body'); } @@ -260,6 +264,10 @@ export class HomeContent { return this.page.getByRole('menu', { name: 'More', exact: true }); } + get lastThreadMessageTextAttachment(): Locator { + return this.page.locator('div.thread-list ul.thread [data-qa-type="message"]').last().locator('[data-qa-type="message-attachment"]'); + } + get btnOptionEditMessage(): Locator { return this.menuMore.getByRole('menuitem', { name: 'Edit', exact: true }); } diff --git a/apps/meteor/tests/e2e/prune-messages.spec.ts b/apps/meteor/tests/e2e/prune-messages.spec.ts index bdb0c0028aefb..509b0ef34177f 100644 --- a/apps/meteor/tests/e2e/prune-messages.spec.ts +++ b/apps/meteor/tests/e2e/prune-messages.spec.ts @@ -1,7 +1,10 @@ +import type { IRoom } from '@rocket.chat/core-typings'; +import { Random } from '@rocket.chat/random'; + import { Users } from './fixtures/userStates'; import { HomeChannel } from './page-objects/home-channel'; import { ToastBar } from './page-objects/toastBar'; -import { createTargetChannel, sendTargetChannelMessage } from './utils'; +import { sendTargetChannelMessage } from './utils'; import { test, expect } from './utils/test'; test.use({ storageState: Users.admin.state }); @@ -9,21 +12,21 @@ test.use({ storageState: Users.admin.state }); test.describe('prune-messages', () => { let poHomeChannel: HomeChannel; let poToastBar: ToastBar; - let targetChannel: string; + let targetChannel: IRoom; test.beforeAll('create target channel', async ({ api }) => { - targetChannel = await createTargetChannel(api, { members: [Users.admin.data.username] }); + targetChannel = (await (await api.post('/channels.create', { name: Random.id() })).json()).channel; }); test.afterAll('delete target channel', async ({ api }) => { - expect((await api.post('/channels.delete', { roomName: targetChannel })).status()).toBe(200); + expect((await api.post('/channels.delete', { roomId: targetChannel._id })).status()).toBe(200); }); test.beforeEach(async ({ page }) => { poToastBar = new ToastBar(page); poHomeChannel = new HomeChannel(page); - await page.goto(`/channel/${targetChannel}/clean-history`); + await page.goto(`/channel/${targetChannel.fname}/clean-history`); }); test( @@ -47,11 +50,11 @@ test.describe('prune-messages', () => { await content.btnModalConfirm.click(); await expect(content.lastMessageFileName).toHaveText('any_file.txt'); - await sendTargetChannelMessage(api, targetChannel, { + await sendTargetChannelMessage(api, targetChannel.fname as string, { msg: 'a message without files', }); - await sendTargetChannelMessage(api, targetChannel, { + await sendTargetChannelMessage(api, targetChannel.fname as string, { msg: 'a pinned message without files', pinned: true, }); @@ -93,4 +96,82 @@ test.describe('prune-messages', () => { }); }, ); + + test( + 'should update message list on pruning files only', + { + tag: '@channel', + annotation: { + type: 'issue', + description: 'https://rocketchat.atlassian.net/browse/CORE-1168', + }, + }, + async () => { + const { + content, + tabs: { pruneMessages }, + } = poHomeChannel; + const { alert, dismiss } = poToastBar; + + await content.sendFileMessage('any_file.txt'); + await content.descriptionInput.fill('a message with a file'); + await content.btnModalConfirm.click(); + await expect(content.lastMessageFileName).toHaveText('any_file.txt'); + + await test.step('prune files only', async () => { + await pruneMessages.filesOnly.check({ force: true }); + await pruneMessages.prune(); + await expect(alert).toHaveText('1 file pruned'); + await dismiss.click(); + await expect(pruneMessages.filesOnly, 'Checkbox is reset after success').not.toBeChecked(); + await expect(pruneMessages.doNotPrunePinned, 'Checkbox is reset after success').not.toBeChecked(); + }); + + await test.step('check message list for prune message-attachment', async () => { + await expect(content.lastMessageFileName).not.toBeVisible(); + await expect(content.lastMessageTextAttachment, 'Prune message attachment replaces file attachment').toHaveText( + 'File removed by prune', + ); + }); + }, + ); + + test( + 'should update main thread message attachment on pruning files only', + { + tag: '@channel', + annotation: { + type: 'issue', + description: 'https://rocketchat.atlassian.net/browse/CORE-1168', + }, + }, + async ({ api }) => { + const { content } = poHomeChannel; + + await content.sendFileMessage('any_file.txt'); + await content.descriptionInput.fill('a message with a file'); + await content.btnModalConfirm.click(); + await expect(content.lastMessageFileName).toHaveText('any_file.txt'); + + await content.lastUserMessage.hover(); + await content.lastUserMessage.getByTitle('Reply in thread').click(); + expect( + ( + await api.post('/rooms.cleanHistory', { + roomId: targetChannel._id, + filesOnly: true, + latest: new Date(Date.now() + 30 * 24 * 3600), // in future + oldest: new Date(Date.now() - 30 * 24 * 3600), // in past + }) + ).status(), + ).toBe(200); + + await test.step('check main thread message for prune message-attachment', async () => { + await expect(content.lastThreadMessageFileName).not.toBeVisible(); + await expect(content.lastThreadMessageTextAttachment, 'Prune message attachment replaces file attachment').toHaveText( + 'File removed by prune', + ); + }); + }, + ); }); diff --git a/packages/core-services/src/events/Events.ts b/packages/core-services/src/events/Events.ts index f149aec9f9ac3..faa8d230199cb 100644 --- a/packages/core-services/src/events/Events.ts +++ b/packages/core-services/src/events/Events.ts @@ -34,6 +34,7 @@ import type { ICustomUserStatus, IWebdavAccount, IOTRMessage, + MessageAttachment, } from '@rocket.chat/core-typings'; import type * as UiKit from '@rocket.chat/ui-kit'; @@ -104,7 +105,15 @@ export type EventSignatures = { users: string[]; ids?: string[]; // message ids have priority over ts showDeletedStatus?: boolean; - }, + } & ( + | { + filesOnly: true; + replaceFileAttachmentsWith?: MessageAttachment; + } + | { + filesOnly?: false; + } + ), ): void; 'notify.deleteCustomSound'(data: { soundData: ICustomSound }): void; 'notify.updateCustomSound'(data: { soundData: ICustomSound }): void; diff --git a/packages/ddp-client/src/types/streams.ts b/packages/ddp-client/src/types/streams.ts index 5b362ead1386b..7d8a2ac96784f 100644 --- a/packages/ddp-client/src/types/streams.ts +++ b/packages/ddp-client/src/types/streams.ts @@ -24,6 +24,7 @@ import type { LicenseLimitKind, ICustomUserStatus, IWebdavAccount, + MessageAttachment, } from '@rocket.chat/core-typings'; import type * as UiKit from '@rocket.chat/ui-kit'; @@ -56,7 +57,15 @@ export interface StreamerEvents { users: string[]; ids?: string[]; // message ids have priority over ts showDeletedStatus?: boolean; - }, + } & ( + | { + filesOnly: true; + replaceFileAttachmentsWith?: MessageAttachment; + } + | { + filesOnly?: false; + } + ), ]; }, { key: `${string}/deleteMessage`; args: [{ _id: IMessage['_id'] }] },