diff --git a/apps/meteor/app/autotranslate/client/lib/autotranslate.ts b/apps/meteor/app/autotranslate/client/lib/autotranslate.ts index bab1ed08213e1..d3207b9205930 100644 --- a/apps/meteor/app/autotranslate/client/lib/autotranslate.ts +++ b/apps/meteor/app/autotranslate/client/lib/autotranslate.ts @@ -69,6 +69,16 @@ export const AutoTranslate = { } } + if (attachment.description && attachment.translations && attachment.translations[language]) { + attachment.translations.original = attachment.description; + + if (autoTranslateShowInverse) { + attachment.description = attachment.translations.original; + } else { + attachment.description = attachment.translations[language]; + } + } + if (attachment.attachments && attachment.attachments.length > 0) { // @ts-expect-error - not sure what to do with this attachment.attachments = this.translateAttachments(attachment.attachments, language); diff --git a/apps/meteor/app/autotranslate/server/autotranslate.ts b/apps/meteor/app/autotranslate/server/autotranslate.ts index 3e04f6d39eb30..2f91e02463d58 100644 --- a/apps/meteor/app/autotranslate/server/autotranslate.ts +++ b/apps/meteor/app/autotranslate/server/autotranslate.ts @@ -320,7 +320,7 @@ export abstract class AutoTranslate { if (message.attachments && message.attachments.length > 0) { setImmediate(async () => { for (const [index, attachment] of message.attachments?.entries() ?? []) { - if (attachment.text) { + if (attachment.description || attachment.text) { // Removes the initial link `[ ](quoterl)` from quote message before translation const translatedText = attachment?.text?.replace(/\[(.*?)\]\(.*?\)/g, '$1') || attachment?.text; const attachmentMessage = { ...attachment, text: translatedText }; diff --git a/apps/meteor/app/autotranslate/server/deeplTranslate.ts b/apps/meteor/app/autotranslate/server/deeplTranslate.ts index 35f73e1755da6..d76a7ea2e4901 100644 --- a/apps/meteor/app/autotranslate/server/deeplTranslate.ts +++ b/apps/meteor/app/autotranslate/server/deeplTranslate.ts @@ -196,7 +196,7 @@ class DeeplAutoTranslate extends AutoTranslate { params: { auth_key: this.apiKey, target_lang: language, - text: attachment.text || '', + text: attachment.description || attachment.text || '', }, }); if (!result.ok) { diff --git a/apps/meteor/app/autotranslate/server/googleTranslate.ts b/apps/meteor/app/autotranslate/server/googleTranslate.ts index 53b9bb7c1d5ea..9667ae53c967a 100644 --- a/apps/meteor/app/autotranslate/server/googleTranslate.ts +++ b/apps/meteor/app/autotranslate/server/googleTranslate.ts @@ -195,7 +195,7 @@ class GoogleAutoTranslate extends AutoTranslate { key: this.apiKey, target: language, format: 'text', - q: attachment.text || '', + q: attachment.description || attachment.text || '', }, }); if (!result.ok) { diff --git a/apps/meteor/app/autotranslate/server/msTranslate.ts b/apps/meteor/app/autotranslate/server/msTranslate.ts index 6508734a1c0da..ddb345d3c895a 100644 --- a/apps/meteor/app/autotranslate/server/msTranslate.ts +++ b/apps/meteor/app/autotranslate/server/msTranslate.ts @@ -192,7 +192,7 @@ class MsAutoTranslate extends AutoTranslate { return this._translate( [ { - Text: attachment.text || '', + Text: attachment.description || attachment.text || '', }, ], targetLanguages, diff --git a/apps/meteor/app/lib/server/functions/notifications/email.js b/apps/meteor/app/lib/server/functions/notifications/email.js index 4de543abb7dd6..c41445fcf55b2 100644 --- a/apps/meteor/app/lib/server/functions/notifications/email.js +++ b/apps/meteor/app/lib/server/functions/notifications/email.js @@ -77,8 +77,13 @@ export async function getEmailContent({ message, user, room }) { } if (hasFiles) { - const fileParts = files.map((file) => { - return escapeHTML(file.name); + const attachments = message.attachments || []; + const fileParts = files.map((file, index) => { + let part = escapeHTML(file.name); + if (attachments[index]?.description) { + part += `

${escapeHTML(attachments[index].description)}`; + } + return part; }); contentParts.push(fileParts.join('

')); } diff --git a/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.ts b/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.ts index 498cef1624624..7a089abba0815 100644 --- a/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.ts +++ b/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.ts @@ -195,6 +195,8 @@ export const sendNotification = async ({ const firstAttachment = message.attachments?.length && message.attachments.shift(); if (firstAttachment) { + firstAttachment.description = + typeof firstAttachment.description === 'string' ? emojione.shortnameToUnicode(firstAttachment.description) : undefined; firstAttachment.text = typeof firstAttachment.text === 'string' ? emojione.shortnameToUnicode(firstAttachment.text) : undefined; } diff --git a/apps/meteor/app/lib/server/methods/updateMessage.ts b/apps/meteor/app/lib/server/methods/updateMessage.ts index 833b4403c0eca..45ba42f25f000 100644 --- a/apps/meteor/app/lib/server/methods/updateMessage.ts +++ b/apps/meteor/app/lib/server/methods/updateMessage.ts @@ -34,7 +34,7 @@ export async function executeUpdateMessage( // IF the message has custom fields, always update // Ideally, we'll compare the custom fields to check for change, but since we don't know the shape of // custom fields, as it's user defined, we're gonna update - const msgText = originalMessage.msg; + const msgText = originalMessage?.attachments?.[0]?.description ?? originalMessage.msg; if (msgText === message.msg && !previewUrls && !message.customFields) { return; } @@ -86,6 +86,13 @@ export async function executeUpdateMessage( } await canSendMessageAsync(message.rid, { uid: user._id, username: user.username ?? undefined, ...user }); + // It is possible to have an empty array as the attachments property, so ensure both things exist + if (originalMessage.attachments && originalMessage.attachments.length > 0 && originalMessage.attachments[0].description !== undefined) { + originalMessage.attachments[0].description = message.msg; + message.attachments = originalMessage.attachments; + message.msg = originalMessage.msg; + } + message.u = originalMessage.u; return updateMessage(message, user, originalMessage, previewUrls); diff --git a/apps/meteor/app/livechat/server/lib/sendTranscript.ts b/apps/meteor/app/livechat/server/lib/sendTranscript.ts index f52ac3f516710..199275f6a516b 100644 --- a/apps/meteor/app/livechat/server/lib/sendTranscript.ts +++ b/apps/meteor/app/livechat/server/lib/sendTranscript.ts @@ -108,7 +108,7 @@ export async function sendTranscript({ const messageType = MessageTypes.getType(message); - const messageContent = messageType?.system + let messageContent = messageType?.system ? DOMPurify.sanitize(` ${messageType.text(i18n.cloneInstance({ interpolation: { escapeValue: false } }).t, message)}}`) : escapeHtml(message.msg); @@ -116,6 +116,9 @@ export async function sendTranscript({ let filesHTML = ''; if (message.attachments && message.attachments?.length > 0) { + messageContent = message.attachments[0].description || ''; + escapeHtml(messageContent); + for await (const attachment of message.attachments) { if (!isFileAttachment(attachment)) { continue; diff --git a/apps/meteor/app/slackbridge/server/RocketAdapter.ts b/apps/meteor/app/slackbridge/server/RocketAdapter.ts index 5c46d75368dda..100e6991c3b08 100644 --- a/apps/meteor/app/slackbridge/server/RocketAdapter.ts +++ b/apps/meteor/app/slackbridge/server/RocketAdapter.ts @@ -203,11 +203,14 @@ export default class RocketAdapter { if (rocketMessage.file.name) { let fileName = rocketMessage.file.name; - const text = rocketMessage.msg; + let text = rocketMessage.msg; const attachment = this.getMessageAttachment(rocketMessage); if (attachment) { fileName = Meteor.absoluteUrl(attachment.title_link); + if (!text) { + text = attachment.description; + } } await slack.postMessage(slack.getSlackChannel(rocketMessage.rid), { ...rocketMessage, msg: `${text} ${fileName}` }); diff --git a/apps/meteor/app/ui/client/lib/ChatMessages.ts b/apps/meteor/app/ui/client/lib/ChatMessages.ts index 70b64201979ba..a6febf3fdfef9 100644 --- a/apps/meteor/app/ui/client/lib/ChatMessages.ts +++ b/apps/meteor/app/ui/client/lib/ChatMessages.ts @@ -120,7 +120,7 @@ export class ChatMessages implements ChatAPI { }, editMessage: async (message: IMessage, { cursorAtStart = false }: { cursorAtStart?: boolean } = {}) => { this.composer?.uploads.clear(); - const text = (await this.data.getDraft(message._id)) || message.msg; + const text = (await this.data.getDraft(message._id)) || message.attachments?.[0]?.description || message.msg; await this.currentEditingMessage.stop(); diff --git a/apps/meteor/client/components/message/content/attachments/file/AudioAttachment.tsx b/apps/meteor/client/components/message/content/attachments/file/AudioAttachment.tsx index 9fa94126127e8..227874cfb8eaf 100644 --- a/apps/meteor/client/components/message/content/attachments/file/AudioAttachment.tsx +++ b/apps/meteor/client/components/message/content/attachments/file/AudioAttachment.tsx @@ -4,13 +4,17 @@ import { useMediaUrl } from '@rocket.chat/ui-contexts'; import { useMemo } from 'react'; import { useReloadOnError } from './hooks/useReloadOnError'; +import MarkdownText from '../../../../MarkdownText'; import MessageCollapsible from '../../../MessageCollapsible'; +import MessageContentBody from '../../../MessageContentBody'; const AudioAttachment = ({ title, audio_url: url, audio_type: type, audio_size: size, + description, + descriptionMd, title_link: link, title_link_download: hasDownload, collapsed, @@ -21,6 +25,7 @@ const AudioAttachment = ({ return ( <> + {descriptionMd ? : } diff --git a/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx b/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx index d57325334098e..f46b24d8d5634 100644 --- a/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx +++ b/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx @@ -13,7 +13,9 @@ import { useTranslation } from 'react-i18next'; import { getFileExtension } from '../../../../../../lib/utils/getFileExtension'; import { forAttachmentDownload, registerDownloadForUid } from '../../../../../hooks/useDownloadFromServiceWorker'; +import MarkdownText from '../../../../MarkdownText'; import MessageCollapsible from '../../../MessageCollapsible'; +import MessageContentBody from '../../../MessageContentBody'; import AttachmentSize from '../structure/AttachmentSize'; import { useOpenEncryptedPdf } from './hooks/useOpenEncryptedPdf'; @@ -23,6 +25,8 @@ type GenericFileAttachmentProps = MessageAttachmentBase; const GenericFileAttachment = ({ title, + description, + descriptionMd, title_link: link, title_link_download: hasDownload, size, @@ -81,6 +85,7 @@ const GenericFileAttachment = ({ return ( <> + {descriptionMd ? : } + {descriptionMd ? : } + {descriptionMd ? : } diff --git a/apps/meteor/client/components/message/toolbar/useCopyAction.ts b/apps/meteor/client/components/message/toolbar/useCopyAction.ts index b8275144abca1..1a03dac99936d 100644 --- a/apps/meteor/client/components/message/toolbar/useCopyAction.ts +++ b/apps/meteor/client/components/message/toolbar/useCopyAction.ts @@ -6,8 +6,8 @@ import type { MessageActionConfig } from '../../../../app/ui-utils/client/lib/Me const getMainMessageText = (message: IMessage): IMessage => { const newMessage = { ...message }; - newMessage.msg = newMessage.msg || newMessage.attachments?.[0]?.title || ''; - newMessage.md = newMessage.md || undefined; + newMessage.msg = newMessage.msg || newMessage.attachments?.[0]?.description || newMessage.attachments?.[0]?.title || ''; + newMessage.md = newMessage.md || newMessage.attachments?.[0]?.descriptionMd || undefined; return { ...newMessage }; }; diff --git a/apps/meteor/client/components/message/toolbar/useReportMessageAction.tsx b/apps/meteor/client/components/message/toolbar/useReportMessageAction.tsx index dbbb962032907..ba281c5a1f5a2 100644 --- a/apps/meteor/client/components/message/toolbar/useReportMessageAction.tsx +++ b/apps/meteor/client/components/message/toolbar/useReportMessageAction.tsx @@ -7,8 +7,8 @@ import ReportMessageModal from '../../../views/room/modals/ReportMessageModal'; const getMainMessageText = (message: IMessage): IMessage => { const newMessage = { ...message }; - newMessage.msg = newMessage.msg || newMessage.attachments?.[0]?.title || ''; - newMessage.md = newMessage.md || undefined; + newMessage.msg = newMessage.msg || newMessage.attachments?.[0]?.description || newMessage.attachments?.[0]?.title || ''; + newMessage.md = newMessage.md || newMessage.attachments?.[0]?.descriptionMd || undefined; return { ...newMessage }; }; diff --git a/apps/meteor/client/hooks/useDecryptedMessage.spec.ts b/apps/meteor/client/hooks/useDecryptedMessage.spec.ts index 3103e708910f6..5b35e8d6e3352 100644 --- a/apps/meteor/client/hooks/useDecryptedMessage.spec.ts +++ b/apps/meteor/client/hooks/useDecryptedMessage.spec.ts @@ -53,7 +53,7 @@ describe('useDecryptedMessage', () => { it('should handle E2EE messages with attachments', async () => { (isE2EEMessage as jest.MockedFunction).mockReturnValue(true); (e2e.decryptMessage as jest.Mock).mockResolvedValue({ - attachments: [{ title: 'Attachment title' }], + attachments: [{ description: 'Attachment description' }], }); const message = { msg: 'Encrypted message with attachment' }; @@ -63,6 +63,7 @@ describe('useDecryptedMessage', () => { expect(result.current).toBe('E2E_message_encrypted_placeholder'); }); + expect(result.current).toBe('Attachment description'); expect(e2e.decryptMessage).toHaveBeenCalledWith(message); }); diff --git a/apps/meteor/client/hooks/useDecryptedMessage.ts b/apps/meteor/client/hooks/useDecryptedMessage.ts index 771665dc0b631..e560aacc5b111 100644 --- a/apps/meteor/client/hooks/useDecryptedMessage.ts +++ b/apps/meteor/client/hooks/useDecryptedMessage.ts @@ -18,11 +18,14 @@ export const useDecryptedMessage = (message: IMessage): string => { e2e.decryptMessage(message).then((decryptedMsg) => { if (decryptedMsg.msg) { setDecryptedMessage(decryptedMsg.msg); - return; } - if (decryptedMsg.attachments && decryptedMsg.attachments.length > 0) { - setDecryptedMessage(t('Message_with_attachment')); + if (decryptedMsg.attachments && decryptedMsg.attachments?.length > 0) { + if (decryptedMsg.attachments[0].description) { + setDecryptedMessage(decryptedMsg.attachments[0].description); + } else { + setDecryptedMessage(t('Message_with_attachment')); + } } }); }, [message, t, setDecryptedMessage]); diff --git a/apps/meteor/client/lib/normalizeThreadMessage.tsx b/apps/meteor/client/lib/normalizeThreadMessage.tsx index 067a23f6aedfe..f2ee47688a59e 100644 --- a/apps/meteor/client/lib/normalizeThreadMessage.tsx +++ b/apps/meteor/client/lib/normalizeThreadMessage.tsx @@ -24,7 +24,11 @@ export function normalizeThreadMessage({ ...message }: Readonly attachment.title); + const attachment = message.attachments.find((attachment) => attachment.title || attachment.description); + + if (attachment?.description) { + return <>{attachment.description}; + } if (attachment?.title) { return <>{attachment.title}; diff --git a/apps/meteor/client/lib/parseMessageTextToAstMarkdown.spec.ts b/apps/meteor/client/lib/parseMessageTextToAstMarkdown.spec.ts index 0105608949b7f..e48eb15f885cf 100644 --- a/apps/meteor/client/lib/parseMessageTextToAstMarkdown.spec.ts +++ b/apps/meteor/client/lib/parseMessageTextToAstMarkdown.spec.ts @@ -178,6 +178,47 @@ describe('parseMessageTextToAstMarkdown', () => { }); it('should return correct attachment translated parsed md when translate is active', () => { + const attachmentTranslatedMessage = { + ...translatedMessage, + attachments: [ + { + description: 'description', + translations: { + en: 'description translated', + }, + }, + ], + }; + const attachmentTranslatedMessageParsed = { + ...translatedMessage, + md: translatedMessageParsed, + attachments: [ + { + description: 'description', + translations: { + en: 'description translated', + }, + md: [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'description translated', + }, + ], + }, + ], + }, + ], + }; + + expect(parseMessageTextToAstMarkdown(attachmentTranslatedMessage, parseOptions, enabledAutoTranslatedOptions)).toStrictEqual( + attachmentTranslatedMessageParsed, + ); + }); + + it('should return correct attachment quote translated parsed md when translate is active', () => { const attachmentTranslatedMessage = { ...translatedMessage, attachments: [ @@ -337,7 +378,7 @@ describe('parseMessageAttachments', () => { const attachmentMessage = [ { - text: 'message **bold** _italic_ and ~strike~', + description: 'message **bold** _italic_ and ~strike~', md: messageParserTokenMessage, }, ]; @@ -359,18 +400,46 @@ describe('parseMessageAttachments', () => { autoTranslateLanguage: 'en', }; - it('should return correct attachment text parsed md when translate is active and auto translate language is undefined', () => { - const textAttachment = [ + it('should return correct attachment description translated parsed md when translate is active', () => { + const descriptionAttachment = [ { ...attachmentMessage[0], - text: 'attachment not translated', + description: 'attachment not translated', translationProvider: 'provider', translations: { en: 'attachment translated', }, }, ]; - const textAttachmentParsed: Root = [ + const descriptionAttachmentParsed: Root = [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'attachment translated', + }, + ], + }, + ]; + + expect(parseMessageAttachments(descriptionAttachment, parseOptions, enabledAutoTranslatedOptions)[0].md).toStrictEqual( + descriptionAttachmentParsed, + ); + }); + + it('should return correct attachment description parsed md when translate is active and auto translate language is undefined', () => { + const descriptionAttachment = [ + { + ...attachmentMessage[0], + description: 'attachment not translated', + translationProvider: 'provider', + translations: { + en: 'attachment translated', + }, + }, + ]; + const descriptionAttachmentParsed: Root = [ { type: 'PARAGRAPH', value: [ @@ -383,11 +452,39 @@ describe('parseMessageAttachments', () => { ]; expect( - parseMessageAttachments(textAttachment, parseOptions, { + parseMessageAttachments(descriptionAttachment, parseOptions, { ...enabledAutoTranslatedOptions, autoTranslateLanguage: undefined, })[0].md, - ).toStrictEqual(textAttachmentParsed); + ).toStrictEqual(descriptionAttachmentParsed); + }); + + it('should return correct attachment text translated parsed md when translate is active', () => { + const textAttachment = [ + { + ...attachmentMessage[0], + text: 'attachment not translated', + translationProvider: 'provider', + translations: { + en: 'attachment translated', + }, + }, + ]; + const textAttachmentParsed: Root = [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'attachment translated', + }, + ], + }, + ]; + + expect(parseMessageAttachments(textAttachment, parseOptions, enabledAutoTranslatedOptions)[0].md).toStrictEqual( + textAttachmentParsed, + ); }); it('should return correct attachment text translated parsed md when translate is active and has multiple texts', () => { diff --git a/apps/meteor/client/lib/parseMessageTextToAstMarkdown.ts b/apps/meteor/client/lib/parseMessageTextToAstMarkdown.ts index 55d393cee38ed..df84785e26351 100644 --- a/apps/meteor/client/lib/parseMessageTextToAstMarkdown.ts +++ b/apps/meteor/client/lib/parseMessageTextToAstMarkdown.ts @@ -1,5 +1,12 @@ import type { IMessage, ITranslatedMessage, MessageAttachment } from '@rocket.chat/core-typings'; -import { isE2EEMessage, isQuoteAttachment, isTranslatedAttachment, isTranslatedMessage } from '@rocket.chat/core-typings'; +import { + isFileAttachment, + isE2EEMessage, + isQuoteAttachment, + isTranslatedAttachment, + isTranslatedMessage, + isEncryptedMessageAttachment, +} from '@rocket.chat/core-typings'; import type { Options, Root } from '@rocket.chat/message-parser'; import { parse } from '@rocket.chat/message-parser'; @@ -51,7 +58,7 @@ export const parseMessageAttachment = ( autoTranslateOptions: { autoTranslateLanguage?: string; translated: boolean }, ): T => { const { translated, autoTranslateLanguage } = autoTranslateOptions; - if (!attachment.text) { + if (!attachment.text && !attachment.description) { return attachment; } @@ -62,8 +69,16 @@ export const parseMessageAttachment = ( const text = (isTranslatedAttachment(attachment) && autoTranslateLanguage && attachment?.translations?.[autoTranslateLanguage]) || attachment.text || + attachment.description || ''; + if (isFileAttachment(attachment) && attachment.description) { + attachment.descriptionMd = + translated || isEncryptedMessageAttachment(attachment) + ? textToMessageToken(text, parseOptions) + : (attachment.descriptionMd ?? textToMessageToken(text, parseOptions)); + } + return { ...attachment, md: translated ? textToMessageToken(text, parseOptions) : (attachment.md ?? textToMessageToken(text, parseOptions)), diff --git a/apps/meteor/client/lib/utils/normalizeMessagePreview/normalizeMessagePreview.spec.ts b/apps/meteor/client/lib/utils/normalizeMessagePreview/normalizeMessagePreview.spec.ts index 9584c13531439..184145a6c4506 100644 --- a/apps/meteor/client/lib/utils/normalizeMessagePreview/normalizeMessagePreview.spec.ts +++ b/apps/meteor/client/lib/utils/normalizeMessagePreview/normalizeMessagePreview.spec.ts @@ -48,7 +48,7 @@ describe('normalizeMessagePreview', () => { }); describe('when message has attachments', () => { - it('should return attachment title when description is available', () => { + it('should return attachment description when available', () => { const message = createFakeMessageWithAttachment({ msg: '', attachments: [ @@ -60,10 +60,10 @@ describe('normalizeMessagePreview', () => { }); const result = normalizeMessagePreview(message, mockT); - expect(result).toBe('Attachment title'); + expect(result).toBe('Attachment description'); }); - it('should return attachment title when message is not provided', () => { + it('should return attachment title when description is not available', () => { const message = createFakeMessageWithAttachment({ msg: '', attachments: [ @@ -112,7 +112,7 @@ describe('normalizeMessagePreview', () => { expect(result).toBe('Second attachment title'); }); - it('should find first attachment title', () => { + it('should find first attachment description', () => { const message = createFakeMessageWithAttachment({ msg: '', attachments: [ @@ -129,7 +129,21 @@ describe('normalizeMessagePreview', () => { }); const result = normalizeMessagePreview(message, mockT); - expect(result).toBe('Third attachment title'); + expect(result).toBe('Second attachment description'); + }); + + it('should escape HTML in attachment description', () => { + const message = createFakeMessageWithAttachment({ + msg: '', + attachments: [ + { + description: '', + }, + ], + }); + const result = normalizeMessagePreview(message, mockT); + + expect(result).toBe('<script>alert("xss")</script>'); }); it('should escape HTML in attachment title', () => { diff --git a/apps/meteor/client/lib/utils/normalizeMessagePreview/normalizeMessagePreview.ts b/apps/meteor/client/lib/utils/normalizeMessagePreview/normalizeMessagePreview.ts index 0fbefea5fa48d..53bf5bf2d4056 100644 --- a/apps/meteor/client/lib/utils/normalizeMessagePreview/normalizeMessagePreview.ts +++ b/apps/meteor/client/lib/utils/normalizeMessagePreview/normalizeMessagePreview.ts @@ -11,7 +11,11 @@ export const normalizeMessagePreview = (message: IMessage, t: TFunction): string } if (message.attachments) { - const attachment = message.attachments.find((attachment) => attachment.title); + const attachment = message.attachments.find((attachment) => attachment.title || attachment.description); + + if (attachment?.description) { + return escapeHTML(attachment.description); + } if (attachment?.title) { return escapeHTML(attachment.title); diff --git a/apps/meteor/client/views/room/MessageList/hooks/useMessageBody.tsx b/apps/meteor/client/views/room/MessageList/hooks/useMessageBody.tsx index 94313e6925432..0bcbbccece740 100644 --- a/apps/meteor/client/views/room/MessageList/hooks/useMessageBody.tsx +++ b/apps/meteor/client/views/room/MessageList/hooks/useMessageBody.tsx @@ -31,7 +31,11 @@ export const useMessageBody = (message: IMessage | undefined): string | Root => } if (message.attachments) { - const attachment = message.attachments.find((attachment) => attachment.title); + const attachment = message.attachments.find((attachment) => attachment.title || attachment.description); + + if (attachment?.description) { + return attachment.description; + } if (attachment?.title) { return attachment.title; diff --git a/apps/meteor/client/views/room/contextualBar/ExportMessages/useDownloadExportMutation.ts b/apps/meteor/client/views/room/contextualBar/ExportMessages/useDownloadExportMutation.ts index 291475075f4e4..f7b53352df5a1 100644 --- a/apps/meteor/client/views/room/contextualBar/ExportMessages/useDownloadExportMutation.ts +++ b/apps/meteor/client/views/room/contextualBar/ExportMessages/useDownloadExportMutation.ts @@ -39,6 +39,7 @@ export const useDownloadExportMutation = () => { ...('image_type' in attachment && { image_type: attachment.image_type }), ...('image_size' in attachment && { image_size: attachment.image_size }), ...('type' in attachment && { type: attachment.type }), + description: attachment.description, })) ?? [], }), ); diff --git a/apps/meteor/client/views/room/contextualBar/ExportMessages/useExportMessagesAsPDFMutation.tsx b/apps/meteor/client/views/room/contextualBar/ExportMessages/useExportMessagesAsPDFMutation.tsx index 383388f0c316f..716f7f3c8bd19 100644 --- a/apps/meteor/client/views/room/contextualBar/ExportMessages/useExportMessagesAsPDFMutation.tsx +++ b/apps/meteor/client/views/room/contextualBar/ExportMessages/useExportMessagesAsPDFMutation.tsx @@ -116,6 +116,7 @@ export const useExportMessagesAsPDFMutation = () => { {parseMessage(message)} {message.attachments?.map((attachment: MessageAttachmentDefault, index) => ( + {attachment.description && {attachment.description}} {attachment.image_url && } {attachment.title} diff --git a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useNormalizedThreadTitleHtml.ts b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useNormalizedThreadTitleHtml.ts index c052942a9567d..4ac708211cf47 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useNormalizedThreadTitleHtml.ts +++ b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useNormalizedThreadTitleHtml.ts @@ -33,7 +33,11 @@ export const useNormalizedThreadTitleHtml = (mainMessage: IThreadMainMessage) => } if (message.attachments) { - const attachment = message.attachments.find((attachment) => attachment.title); + const attachment = message.attachments.find((attachment) => attachment.title || attachment.description); + + if (attachment?.description) { + return escapeHTML(attachment.description); + } if (attachment?.title) { return escapeHTML(attachment.title); diff --git a/apps/meteor/server/features/EmailInbox/EmailInbox_Outgoing.ts b/apps/meteor/server/features/EmailInbox/EmailInbox_Outgoing.ts index d15ac6dbbc909..b2480acff247d 100644 --- a/apps/meteor/server/features/EmailInbox/EmailInbox_Outgoing.ts +++ b/apps/meteor/server/features/EmailInbox/EmailInbox_Outgoing.ts @@ -142,7 +142,11 @@ slashCommands.add({ return; } - const emailText = message?.msg || ''; + const emailText = + message?.attachments + ?.map((a) => a.description) + .filter(Boolean) + .join('\n\n') || ''; void sendEmail( inbox, diff --git a/apps/meteor/server/services/messages/hooks/BeforeSaveMarkdownParser.ts b/apps/meteor/server/services/messages/hooks/BeforeSaveMarkdownParser.ts index b2b47ad0b9228..088aff0c8b052 100644 --- a/apps/meteor/server/services/messages/hooks/BeforeSaveMarkdownParser.ts +++ b/apps/meteor/server/services/messages/hooks/BeforeSaveMarkdownParser.ts @@ -38,6 +38,10 @@ export class BeforeSaveMarkdownParser { if (message.msg) { message.md = parse(message.msg, config); } + + if (message.attachments?.[0]?.description) { + message.attachments[0].descriptionMd = parse(message.attachments[0].description, config); + } } catch (e) { console.error(e); // errors logged while the parser is at experimental stage } diff --git a/apps/meteor/tests/unit/server/services/messages/hooks/BeforeSaveJumpToMessage.tests.ts b/apps/meteor/tests/unit/server/services/messages/hooks/BeforeSaveJumpToMessage.tests.ts index 791404e4f1d9f..0ce7279e89b39 100644 --- a/apps/meteor/tests/unit/server/services/messages/hooks/BeforeSaveJumpToMessage.tests.ts +++ b/apps/meteor/tests/unit/server/services/messages/hooks/BeforeSaveJumpToMessage.tests.ts @@ -500,6 +500,17 @@ describe('Create attachments for message URLs', () => { image_size: 68016, type: 'file', description: 'chained 3 - file', + descriptionMd: [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'chained 3 - file', + }, + ], + }, + ], }, ], }, diff --git a/apps/meteor/tests/unit/server/services/messages/hooks/BeforeSaveMarkdownParser.tests.ts b/apps/meteor/tests/unit/server/services/messages/hooks/BeforeSaveMarkdownParser.tests.ts index c744883ce9e3c..c511b071ac1a0 100644 --- a/apps/meteor/tests/unit/server/services/messages/hooks/BeforeSaveMarkdownParser.tests.ts +++ b/apps/meteor/tests/unit/server/services/messages/hooks/BeforeSaveMarkdownParser.tests.ts @@ -82,4 +82,29 @@ describe('Markdown parser', () => { expect(message).to.have.property('md'); }); + + it('should parse markdown on the first attachment only', async () => { + const markdownParser = new BeforeSaveMarkdownParser(true); + + const message = await markdownParser.parseMarkdown({ + message: createMessage('hey', { + attachments: [ + { + description: 'hey ho', + }, + { + description: 'lets go', + }, + ], + }), + config: {}, + }); + + expect(message).to.have.property('md'); + + const [attachment1, attachment2] = message.attachments || []; + + expect(attachment1).to.have.property('descriptionMd'); + expect(attachment2).to.not.have.property('descriptionMd'); + }); }); diff --git a/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts b/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts index 67b9aa5ab16d9..4755688c9f006 100644 --- a/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts +++ b/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts @@ -312,7 +312,9 @@ export class OmnichannelTranscript extends ServiceClass implements IOmnichannelT } } - const msg = message.msg || ''; + // When you send a file message, the things you type in the modal are not "msg", they're in "description" of the attachment + // So, we'll fetch the the msg, if empty, go for the first description on an attachment, if empty, empty string + const msg = message.msg || message.attachments.find((attachment) => attachment.description)?.description || ''; // Remove nulls from final array messagesData.push({ msg, diff --git a/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentBase.ts b/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentBase.ts index 7d236bcafa2be..82ce0db3325ef 100644 --- a/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentBase.ts +++ b/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentBase.ts @@ -7,6 +7,7 @@ export type MessageAttachmentBase = { collapsed?: boolean; /** description isn't being used on client for non-image attachments, we're keeping it for backward compatibility */ description?: string; + descriptionMd?: Root; text?: string; md?: Root; size?: number;