From 77b5b4656c25ab5c403c464d291e1da099149977 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Wed, 13 May 2026 15:29:08 -0300 Subject: [PATCH 01/20] add encrypted pdf file blob codification --- .../file/GenericFileAttachment.tsx | 40 ++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) 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 2e0fb7107d983..2920a460dd11c 100644 --- a/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx +++ b/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx @@ -7,12 +7,13 @@ import { MessageGenericPreviewDescription, } from '@rocket.chat/fuselage'; import { useMediaUrl } from '@rocket.chat/ui-contexts'; -import { useId } from 'react'; +import { useId, useEffect, useRef } from 'react'; import type { UIEvent } from 'react'; import { useTranslation } from 'react-i18next'; import { getFileExtension } from '../../../../../../lib/utils/getFileExtension'; import { forAttachmentDownload, registerDownloadForUid } from '../../../../../hooks/useDownloadFromServiceWorker'; +import { MAX_FILE_SIZE_PREVIEW } from '../../../../../lib/constants'; import MessageCollapsible from '../../../MessageCollapsible'; import AttachmentSize from '../structure/AttachmentSize'; @@ -32,23 +33,52 @@ const GenericFileAttachment = ({ const uid = useId(); const { t } = useTranslation(); - const handleTitleClick = (event: UIEvent): void => { + const blobUrlRef = useRef(undefined); + + useEffect(() => { + return () => { + if (blobUrlRef.current) { + URL.revokeObjectURL(blobUrlRef.current); + blobUrlRef.current = undefined; + } + }; + }, []); + + const handleTitleClick = async (event: UIEvent): Promise => { if (!link) { return; } - if (openDocumentViewer && format === 'PDF') { + const isEncrypted = link.includes('/file-decrypt/'); + + if (format === 'PDF' && openDocumentViewer) { event.preventDefault(); + if (isEncrypted) { + if (size && size > MAX_FILE_SIZE_PREVIEW) { + registerDownloadForUid(uid, t, title); + forAttachmentDownload(uid, link); + return; + } + if (blobUrlRef.current) { + URL.revokeObjectURL(blobUrlRef.current); + } + const response = await fetch(getURL(link)); + const blob = await response.blob(); + const blobUrl = URL.createObjectURL(blob); + blobUrlRef.current = blobUrl; + openDocumentViewer(blobUrl, format, title ?? ''); + return; + } + const url = new URL(getURL(link), window.location.origin); url.searchParams.set('contentDisposition', 'inline'); openDocumentViewer(url.toString(), format, ''); return; } - if (link.includes('/file-decrypt/')) { + if (isEncrypted) { event.preventDefault(); - registerDownloadForUid(uid, t, title); forAttachmentDownload(uid, link); } From 339c708dd3dc48f8ff377df42ff3792a895ada7a Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Thu, 14 May 2026 19:13:36 -0300 Subject: [PATCH 02/20] add changeset --- .changeset/fine-jokes-trade.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fine-jokes-trade.md diff --git a/.changeset/fine-jokes-trade.md b/.changeset/fine-jokes-trade.md new file mode 100644 index 0000000000000..42662692a39c5 --- /dev/null +++ b/.changeset/fine-jokes-trade.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes inability to download encrypted PDFs on desktop From ab04cdf68d25765a514f7eaac1d808c2b590d1ed Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Fri, 15 May 2026 11:19:09 -0300 Subject: [PATCH 03/20] add use of abort controller --- .../file/GenericFileAttachment.tsx | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) 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 2920a460dd11c..dd9ca13613266 100644 --- a/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx +++ b/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx @@ -34,9 +34,13 @@ const GenericFileAttachment = ({ const { t } = useTranslation(); const blobUrlRef = useRef(undefined); + const abortControllerRef = useRef(null); useEffect(() => { return () => { + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } if (blobUrlRef.current) { URL.revokeObjectURL(blobUrlRef.current); blobUrlRef.current = undefined; @@ -60,14 +64,33 @@ const GenericFileAttachment = ({ forAttachmentDownload(uid, link); return; } + if (blobUrlRef.current) { URL.revokeObjectURL(blobUrlRef.current); + blobUrlRef.current = undefined; + } + + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + abortControllerRef.current = new AbortController(); + + try { + const response = await fetch(getURL(link), { + signal: abortControllerRef.current.signal, + }); + const blob = await response.blob(); + if (abortControllerRef.current.signal.aborted) { + return; + } + const blobUrl = URL.createObjectURL(blob); + blobUrlRef.current = blobUrl; + openDocumentViewer(blobUrl, format, title ?? ''); + } catch (error: any) { + if (error.name !== 'AbortError') { + console.error('Error fetching encrypted PDF', error); + } } - const response = await fetch(getURL(link)); - const blob = await response.blob(); - const blobUrl = URL.createObjectURL(blob); - blobUrlRef.current = blobUrl; - openDocumentViewer(blobUrl, format, title ?? ''); return; } From 78225b7f1463044f554765a0a3b06cec30b2b94f Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Fri, 15 May 2026 13:00:40 -0300 Subject: [PATCH 04/20] add fetch failure handling --- .../content/attachments/file/GenericFileAttachment.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 dd9ca13613266..a447112c502b2 100644 --- a/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx +++ b/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx @@ -79,6 +79,9 @@ const GenericFileAttachment = ({ const response = await fetch(getURL(link), { signal: abortControllerRef.current.signal, }); + if (!response.ok) { + throw new Error(`Failed to fetch encrypted PDF: ${response.status}`); + } const blob = await response.blob(); if (abortControllerRef.current.signal.aborted) { return; @@ -88,7 +91,7 @@ const GenericFileAttachment = ({ openDocumentViewer(blobUrl, format, title ?? ''); } catch (error: any) { if (error.name !== 'AbortError') { - console.error('Error fetching encrypted PDF', error); + console.error('Error opening preview of encrypted PDF', error); } } return; From 98015cb011096d419568a9850a4e1e14d00af0c5 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Mon, 18 May 2026 19:01:11 -0300 Subject: [PATCH 05/20] add new setting --- .../content/attachments/file/GenericFileAttachment.tsx | 7 ++++--- apps/meteor/server/settings/e2e.ts | 7 +++++++ packages/i18n/src/locales/en.i18n.json | 2 ++ 3 files changed, 13 insertions(+), 3 deletions(-) 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 a447112c502b2..a0aa6fde03a16 100644 --- a/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx +++ b/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx @@ -6,14 +6,13 @@ import { MessageGenericPreviewTitle, MessageGenericPreviewDescription, } from '@rocket.chat/fuselage'; -import { useMediaUrl } from '@rocket.chat/ui-contexts'; +import { useMediaUrl, useSetting } from '@rocket.chat/ui-contexts'; import { useId, useEffect, useRef } from 'react'; import type { UIEvent } from 'react'; import { useTranslation } from 'react-i18next'; import { getFileExtension } from '../../../../../../lib/utils/getFileExtension'; import { forAttachmentDownload, registerDownloadForUid } from '../../../../../hooks/useDownloadFromServiceWorker'; -import { MAX_FILE_SIZE_PREVIEW } from '../../../../../lib/constants'; import MessageCollapsible from '../../../MessageCollapsible'; import AttachmentSize from '../structure/AttachmentSize'; @@ -30,6 +29,8 @@ const GenericFileAttachment = ({ collapsed, }: GenericFileAttachmentProps) => { const getURL = useMediaUrl(); + const pdfPreviewSizeLimitMb = useSetting('E2E_PDF_Preview_Size_Limit', 10); + const pdfPreviewSizeLimit = Number(pdfPreviewSizeLimitMb) * 1024 * 1024; const uid = useId(); const { t } = useTranslation(); @@ -59,7 +60,7 @@ const GenericFileAttachment = ({ event.preventDefault(); if (isEncrypted) { - if (size && size > MAX_FILE_SIZE_PREVIEW) { + if (size && size > pdfPreviewSizeLimit) { registerDownloadForUid(uid, t, title); forAttachmentDownload(uid, link); return; diff --git a/apps/meteor/server/settings/e2e.ts b/apps/meteor/server/settings/e2e.ts index b94bfc38d8c66..dd5da7a87a236 100644 --- a/apps/meteor/server/settings/e2e.ts +++ b/apps/meteor/server/settings/e2e.ts @@ -34,6 +34,13 @@ export const createE2ESettings = () => enableQuery: { _id: 'E2E_Enable', value: true }, }); + await this.add('E2E_PDF_Preview_Size_Limit', 10, { + type: 'int', + public: true, + i18nLabel: 'E2E_PDF_Preview_Size_Limit', + i18nDescription: 'E2E_PDF_Preview_Size_Limit_Description', + }); + await this.add('E2E_Enabled_Mentions', true, { type: 'boolean', public: true, diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 4bf33527ac06a..04a18ec94f0cc 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -1921,6 +1921,8 @@ "E2E_Encryption_enabled_for_room": "End-to-end encryption enabled for #{{roomName}}", "E2E_Invalid_Key": "No E2E encryption key found for this room", "E2E_Key_Error": "This message is end-to-end encrypted and cannot be decrypted due to incorrect encryption key", + "E2E_PDF_Preview_Size_Limit": "PDF size limit for previews (MB)", + "E2E_PDF_Preview_Size_Limit_Description": "Encrypted files are loaded to memory for previews. Files larger than this limit will be downloaded directly.", "E2E_Reset_Email_Content": "You've been automatically logged out. When you log in again, a new key will be generated and access will be restored to any encrypted room with at least one member online. If no members are online, access will be restored as soon as a member comes online.", "E2E_Reset_Other_Key_Warning": "Resetting the E2EE key will log out the user. When the user logs in again, a new key will be generated and access will be restored to any encrypted room with at least one member online. If no members are online, access will be restored as soon as a member comes online.", "E2E_disable_encryption": "Disable encryption", From 4b6851cd409ca26650f54b32da162e03ef0d941e Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Tue, 19 May 2026 10:52:29 -0300 Subject: [PATCH 06/20] use bytes rather than megabytes for setting --- .../message/content/attachments/file/GenericFileAttachment.tsx | 3 +-- apps/meteor/server/settings/e2e.ts | 2 +- packages/i18n/src/locales/en.i18n.json | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) 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 a0aa6fde03a16..bd96f7a2d2e3c 100644 --- a/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx +++ b/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx @@ -29,8 +29,7 @@ const GenericFileAttachment = ({ collapsed, }: GenericFileAttachmentProps) => { const getURL = useMediaUrl(); - const pdfPreviewSizeLimitMb = useSetting('E2E_PDF_Preview_Size_Limit', 10); - const pdfPreviewSizeLimit = Number(pdfPreviewSizeLimitMb) * 1024 * 1024; + const pdfPreviewSizeLimit = useSetting('E2E_PDF_Preview_Size_Limit', 10485760); const uid = useId(); const { t } = useTranslation(); diff --git a/apps/meteor/server/settings/e2e.ts b/apps/meteor/server/settings/e2e.ts index dd5da7a87a236..a58ed36093b50 100644 --- a/apps/meteor/server/settings/e2e.ts +++ b/apps/meteor/server/settings/e2e.ts @@ -34,7 +34,7 @@ export const createE2ESettings = () => enableQuery: { _id: 'E2E_Enable', value: true }, }); - await this.add('E2E_PDF_Preview_Size_Limit', 10, { + await this.add('E2E_PDF_Preview_Size_Limit', 10485760, { type: 'int', public: true, i18nLabel: 'E2E_PDF_Preview_Size_Limit', diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 04a18ec94f0cc..6f66f3182a801 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -1921,7 +1921,7 @@ "E2E_Encryption_enabled_for_room": "End-to-end encryption enabled for #{{roomName}}", "E2E_Invalid_Key": "No E2E encryption key found for this room", "E2E_Key_Error": "This message is end-to-end encrypted and cannot be decrypted due to incorrect encryption key", - "E2E_PDF_Preview_Size_Limit": "PDF size limit for previews (MB)", + "E2E_PDF_Preview_Size_Limit": "PDF size limit for previews (in bytes)", "E2E_PDF_Preview_Size_Limit_Description": "Encrypted files are loaded to memory for previews. Files larger than this limit will be downloaded directly.", "E2E_Reset_Email_Content": "You've been automatically logged out. When you log in again, a new key will be generated and access will be restored to any encrypted room with at least one member online. If no members are online, access will be restored as soon as a member comes online.", "E2E_Reset_Other_Key_Warning": "Resetting the E2EE key will log out the user. When the user logs in again, a new key will be generated and access will be restored to any encrypted room with at least one member online. If no members are online, access will be restored as soon as a member comes online.", From a082845f4f864705a1ad8b60752a118a5e124533 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Tue, 19 May 2026 11:18:50 -0300 Subject: [PATCH 07/20] address abortController race condition --- .../content/attachments/file/GenericFileAttachment.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 bd96f7a2d2e3c..44ed7a836e848 100644 --- a/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx +++ b/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx @@ -73,17 +73,18 @@ const GenericFileAttachment = ({ if (abortControllerRef.current) { abortControllerRef.current.abort(); } - abortControllerRef.current = new AbortController(); + const abortController = new AbortController(); + abortControllerRef.current = abortController; try { const response = await fetch(getURL(link), { - signal: abortControllerRef.current.signal, + signal: abortController.signal, }); if (!response.ok) { throw new Error(`Failed to fetch encrypted PDF: ${response.status}`); } const blob = await response.blob(); - if (abortControllerRef.current.signal.aborted) { + if (abortController.signal.aborted || abortControllerRef.current !== abortController) { return; } const blobUrl = URL.createObjectURL(blob); From 45fb0fec4ca6beea5d89c0265b696f7e11e6f405 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Tue, 19 May 2026 11:20:31 -0300 Subject: [PATCH 08/20] update changeset --- .changeset/fine-jokes-trade.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/fine-jokes-trade.md b/.changeset/fine-jokes-trade.md index 42662692a39c5..4cba21cf6c18b 100644 --- a/.changeset/fine-jokes-trade.md +++ b/.changeset/fine-jokes-trade.md @@ -1,5 +1,5 @@ --- -'@rocket.chat/meteor': patch +'@rocket.chat/meteor': minor --- -Fixes inability to download encrypted PDFs on desktop +Adds new `E2E_PDF_Preview_Size_Limit` setting to set the maximum file size for PDF preview in E2E encrypted rooms. The default value is 10MB. If a PDF file exceeds this limit, it will not be previewed and will be downloaded instead. From 6375d390c68c0148ab7859119837f633182f5cd9 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Tue, 19 May 2026 11:28:24 -0300 Subject: [PATCH 09/20] fix grammar in translation --- packages/i18n/src/locales/en.i18n.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 6f66f3182a801..66db34b39cf38 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -1922,7 +1922,7 @@ "E2E_Invalid_Key": "No E2E encryption key found for this room", "E2E_Key_Error": "This message is end-to-end encrypted and cannot be decrypted due to incorrect encryption key", "E2E_PDF_Preview_Size_Limit": "PDF size limit for previews (in bytes)", - "E2E_PDF_Preview_Size_Limit_Description": "Encrypted files are loaded to memory for previews. Files larger than this limit will be downloaded directly.", + "E2E_PDF_Preview_Size_Limit_Description": "Encrypted files are loaded into memory for previews. Files larger than this limit will be downloaded directly.", "E2E_Reset_Email_Content": "You've been automatically logged out. When you log in again, a new key will be generated and access will be restored to any encrypted room with at least one member online. If no members are online, access will be restored as soon as a member comes online.", "E2E_Reset_Other_Key_Warning": "Resetting the E2EE key will log out the user. When the user logs in again, a new key will be generated and access will be restored to any encrypted room with at least one member online. If no members are online, access will be restored as soon as a member comes online.", "E2E_disable_encryption": "Disable encryption", From 7a51828f4cb3e2873ef910149cee1383f81bd1f3 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Tue, 19 May 2026 12:07:18 -0300 Subject: [PATCH 10/20] extract encrypted pdf opening logic to hook --- .../file/GenericFileAttachment.tsx | 59 ++------------- .../file/hooks/useOpenEncryptedPdf.tsx | 75 +++++++++++++++++++ 2 files changed, 80 insertions(+), 54 deletions(-) create mode 100644 apps/meteor/client/components/message/content/attachments/file/hooks/useOpenEncryptedPdf.tsx 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 44ed7a836e848..ee6d7978d3eff 100644 --- a/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx +++ b/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx @@ -6,8 +6,8 @@ import { MessageGenericPreviewTitle, MessageGenericPreviewDescription, } from '@rocket.chat/fuselage'; -import { useMediaUrl, useSetting } from '@rocket.chat/ui-contexts'; -import { useId, useEffect, useRef } from 'react'; +import { useMediaUrl } from '@rocket.chat/ui-contexts'; +import { useId } from 'react'; import type { UIEvent } from 'react'; import { useTranslation } from 'react-i18next'; @@ -15,6 +15,7 @@ import { getFileExtension } from '../../../../../../lib/utils/getFileExtension'; import { forAttachmentDownload, registerDownloadForUid } from '../../../../../hooks/useDownloadFromServiceWorker'; import MessageCollapsible from '../../../MessageCollapsible'; import AttachmentSize from '../structure/AttachmentSize'; +import { useOpenEncryptedPdf } from './hooks/useOpenEncryptedPdf'; const openDocumentViewer = window.RocketChatDesktop?.openDocumentViewer; @@ -29,24 +30,9 @@ const GenericFileAttachment = ({ collapsed, }: GenericFileAttachmentProps) => { const getURL = useMediaUrl(); - const pdfPreviewSizeLimit = useSetting('E2E_PDF_Preview_Size_Limit', 10485760); const uid = useId(); const { t } = useTranslation(); - - const blobUrlRef = useRef(undefined); - const abortControllerRef = useRef(null); - - useEffect(() => { - return () => { - if (abortControllerRef.current) { - abortControllerRef.current.abort(); - } - if (blobUrlRef.current) { - URL.revokeObjectURL(blobUrlRef.current); - blobUrlRef.current = undefined; - } - }; - }, []); + const openEncryptedPdf = useOpenEncryptedPdf(); const handleTitleClick = async (event: UIEvent): Promise => { if (!link) { @@ -59,42 +45,7 @@ const GenericFileAttachment = ({ event.preventDefault(); if (isEncrypted) { - if (size && size > pdfPreviewSizeLimit) { - registerDownloadForUid(uid, t, title); - forAttachmentDownload(uid, link); - return; - } - - if (blobUrlRef.current) { - URL.revokeObjectURL(blobUrlRef.current); - blobUrlRef.current = undefined; - } - - if (abortControllerRef.current) { - abortControllerRef.current.abort(); - } - const abortController = new AbortController(); - abortControllerRef.current = abortController; - - try { - const response = await fetch(getURL(link), { - signal: abortController.signal, - }); - if (!response.ok) { - throw new Error(`Failed to fetch encrypted PDF: ${response.status}`); - } - const blob = await response.blob(); - if (abortController.signal.aborted || abortControllerRef.current !== abortController) { - return; - } - const blobUrl = URL.createObjectURL(blob); - blobUrlRef.current = blobUrl; - openDocumentViewer(blobUrl, format, title ?? ''); - } catch (error: any) { - if (error.name !== 'AbortError') { - console.error('Error opening preview of encrypted PDF', error); - } - } + openEncryptedPdf(link, title, size, format, openDocumentViewer); return; } diff --git a/apps/meteor/client/components/message/content/attachments/file/hooks/useOpenEncryptedPdf.tsx b/apps/meteor/client/components/message/content/attachments/file/hooks/useOpenEncryptedPdf.tsx new file mode 100644 index 0000000000000..7f32f110e1076 --- /dev/null +++ b/apps/meteor/client/components/message/content/attachments/file/hooks/useOpenEncryptedPdf.tsx @@ -0,0 +1,75 @@ +import { useMediaUrl, useSetting } from '@rocket.chat/ui-contexts'; +import { useId, useEffect, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { forAttachmentDownload, registerDownloadForUid } from '../../../../../../hooks/useDownloadFromServiceWorker'; + +export const useOpenEncryptedPdf = () => { + const getURL = useMediaUrl(); + const pdfPreviewSizeLimit = useSetting('E2E_PDF_Preview_Size_Limit', 10485760); + const uid = useId(); + const { t } = useTranslation(); + + const blobUrlRef = useRef(undefined); + const abortControllerRef = useRef(null); + + useEffect(() => { + return () => { + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + if (blobUrlRef.current) { + URL.revokeObjectURL(blobUrlRef.current); + blobUrlRef.current = undefined; + } + }; + }, []); + + const openEncryptedPdf = async ( + link: string, + title: string | undefined, + size: number | undefined, + format: string, + openDocumentViewer: (url: string, format: string, options: any) => void, + ) => { + if (size && size > pdfPreviewSizeLimit) { + registerDownloadForUid(uid, t, title); + forAttachmentDownload(uid, link); + return; + } + + if (blobUrlRef.current) { + URL.revokeObjectURL(blobUrlRef.current); + blobUrlRef.current = undefined; + } + + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + + const abortController = new AbortController(); + abortControllerRef.current = abortController; + + try { + const response = await fetch(getURL(link), { + signal: abortController.signal, + }); + if (!response.ok) { + throw new Error(`Failed to fetch encrypted PDF: ${response.status}`); + } + const blob = await response.blob(); + if (abortController.signal.aborted || abortControllerRef.current !== abortController) { + return; + } + const blobUrl = URL.createObjectURL(blob); + blobUrlRef.current = blobUrl; + openDocumentViewer(blobUrl, format, title ?? ''); + } catch (error: any) { + if (error.name !== 'AbortError') { + console.error('Error opening preview of encrypted PDF', error); + } + } + }; + + return openEncryptedPdf; +}; From 68f5ae03a384fcc776f65b0bc71bccda1436022d Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Tue, 19 May 2026 16:47:09 -0300 Subject: [PATCH 11/20] remove workspace setting to use desktop setting --- .changeset/fine-jokes-trade.md | 5 ----- .../content/attachments/file/hooks/useOpenEncryptedPdf.tsx | 4 ++-- apps/meteor/server/settings/e2e.ts | 7 ------- packages/i18n/src/locales/en.i18n.json | 2 -- 4 files changed, 2 insertions(+), 16 deletions(-) delete mode 100644 .changeset/fine-jokes-trade.md diff --git a/.changeset/fine-jokes-trade.md b/.changeset/fine-jokes-trade.md deleted file mode 100644 index 4cba21cf6c18b..0000000000000 --- a/.changeset/fine-jokes-trade.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': minor ---- - -Adds new `E2E_PDF_Preview_Size_Limit` setting to set the maximum file size for PDF preview in E2E encrypted rooms. The default value is 10MB. If a PDF file exceeds this limit, it will not be previewed and will be downloaded instead. diff --git a/apps/meteor/client/components/message/content/attachments/file/hooks/useOpenEncryptedPdf.tsx b/apps/meteor/client/components/message/content/attachments/file/hooks/useOpenEncryptedPdf.tsx index 7f32f110e1076..1f2df06fd0119 100644 --- a/apps/meteor/client/components/message/content/attachments/file/hooks/useOpenEncryptedPdf.tsx +++ b/apps/meteor/client/components/message/content/attachments/file/hooks/useOpenEncryptedPdf.tsx @@ -1,4 +1,4 @@ -import { useMediaUrl, useSetting } from '@rocket.chat/ui-contexts'; +import { useMediaUrl } from '@rocket.chat/ui-contexts'; import { useId, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; @@ -6,7 +6,7 @@ import { forAttachmentDownload, registerDownloadForUid } from '../../../../../.. export const useOpenEncryptedPdf = () => { const getURL = useMediaUrl(); - const pdfPreviewSizeLimit = useSetting('E2E_PDF_Preview_Size_Limit', 10485760); + const pdfPreviewSizeLimit = window.RocketChatDesktop?.getE2ePdfPreviewSizeLimit?.() ?? 10; const uid = useId(); const { t } = useTranslation(); diff --git a/apps/meteor/server/settings/e2e.ts b/apps/meteor/server/settings/e2e.ts index a58ed36093b50..b94bfc38d8c66 100644 --- a/apps/meteor/server/settings/e2e.ts +++ b/apps/meteor/server/settings/e2e.ts @@ -34,13 +34,6 @@ export const createE2ESettings = () => enableQuery: { _id: 'E2E_Enable', value: true }, }); - await this.add('E2E_PDF_Preview_Size_Limit', 10485760, { - type: 'int', - public: true, - i18nLabel: 'E2E_PDF_Preview_Size_Limit', - i18nDescription: 'E2E_PDF_Preview_Size_Limit_Description', - }); - await this.add('E2E_Enabled_Mentions', true, { type: 'boolean', public: true, diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 66db34b39cf38..4bf33527ac06a 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -1921,8 +1921,6 @@ "E2E_Encryption_enabled_for_room": "End-to-end encryption enabled for #{{roomName}}", "E2E_Invalid_Key": "No E2E encryption key found for this room", "E2E_Key_Error": "This message is end-to-end encrypted and cannot be decrypted due to incorrect encryption key", - "E2E_PDF_Preview_Size_Limit": "PDF size limit for previews (in bytes)", - "E2E_PDF_Preview_Size_Limit_Description": "Encrypted files are loaded into memory for previews. Files larger than this limit will be downloaded directly.", "E2E_Reset_Email_Content": "You've been automatically logged out. When you log in again, a new key will be generated and access will be restored to any encrypted room with at least one member online. If no members are online, access will be restored as soon as a member comes online.", "E2E_Reset_Other_Key_Warning": "Resetting the E2EE key will log out the user. When the user logs in again, a new key will be generated and access will be restored to any encrypted room with at least one member online. If no members are online, access will be restored as soon as a member comes online.", "E2E_disable_encryption": "Disable encryption", From a7323abac67eb2d1ae80ee3fcb2b8cd8eedde52a Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Tue, 19 May 2026 16:55:50 -0300 Subject: [PATCH 12/20] convert setting value in bytes --- .../content/attachments/file/hooks/useOpenEncryptedPdf.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/meteor/client/components/message/content/attachments/file/hooks/useOpenEncryptedPdf.tsx b/apps/meteor/client/components/message/content/attachments/file/hooks/useOpenEncryptedPdf.tsx index 1f2df06fd0119..45b5d567fc2c7 100644 --- a/apps/meteor/client/components/message/content/attachments/file/hooks/useOpenEncryptedPdf.tsx +++ b/apps/meteor/client/components/message/content/attachments/file/hooks/useOpenEncryptedPdf.tsx @@ -7,6 +7,7 @@ import { forAttachmentDownload, registerDownloadForUid } from '../../../../../.. export const useOpenEncryptedPdf = () => { const getURL = useMediaUrl(); const pdfPreviewSizeLimit = window.RocketChatDesktop?.getE2ePdfPreviewSizeLimit?.() ?? 10; + const pdfPreviewSizeLimitInBytes = pdfPreviewSizeLimit * 1024 * 1024; const uid = useId(); const { t } = useTranslation(); @@ -32,7 +33,7 @@ export const useOpenEncryptedPdf = () => { format: string, openDocumentViewer: (url: string, format: string, options: any) => void, ) => { - if (size && size > pdfPreviewSizeLimit) { + if (size && size > pdfPreviewSizeLimitInBytes) { registerDownloadForUid(uid, t, title); forAttachmentDownload(uid, link); return; From ca906d3312eab55e5b483480541a50ea8047a588 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Wed, 20 May 2026 11:28:08 -0300 Subject: [PATCH 13/20] add missing property signatura to IRocketChatDesktop interface --- packages/desktop-api/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/desktop-api/src/index.ts b/packages/desktop-api/src/index.ts index 9d10c1f89d1c5..a8cbadd2aff47 100644 --- a/packages/desktop-api/src/index.ts +++ b/packages/desktop-api/src/index.ts @@ -63,4 +63,5 @@ export interface IRocketChatDesktop { setUserToken: (token: string, userId: string) => void; openDocumentViewer: (url: string, format: string, options: any) => void; reloadServer: () => void; + getE2ePdfPreviewSizeLimit: () => number; } From 63f6063bde05d5d0296a00c6b2a9eb238e8aaad6 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Fri, 5 Jun 2026 20:02:35 -0300 Subject: [PATCH 14/20] handle undefined size case --- .../content/attachments/file/hooks/useOpenEncryptedPdf.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/client/components/message/content/attachments/file/hooks/useOpenEncryptedPdf.tsx b/apps/meteor/client/components/message/content/attachments/file/hooks/useOpenEncryptedPdf.tsx index 45b5d567fc2c7..c623a293767fc 100644 --- a/apps/meteor/client/components/message/content/attachments/file/hooks/useOpenEncryptedPdf.tsx +++ b/apps/meteor/client/components/message/content/attachments/file/hooks/useOpenEncryptedPdf.tsx @@ -33,7 +33,7 @@ export const useOpenEncryptedPdf = () => { format: string, openDocumentViewer: (url: string, format: string, options: any) => void, ) => { - if (size && size > pdfPreviewSizeLimitInBytes) { + if (size === undefined || size > pdfPreviewSizeLimitInBytes) { registerDownloadForUid(uid, t, title); forAttachmentDownload(uid, link); return; From 8c7c4c22a0f9194fd1346565376890a603a4b7b9 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Fri, 5 Jun 2026 20:17:01 -0300 Subject: [PATCH 15/20] add useOpenEncryptedPdf test suite --- .../file/hooks/useOpenEncryptedPdf.spec.tsx | 322 ++++++++++++++++++ 1 file changed, 322 insertions(+) create mode 100644 apps/meteor/client/components/message/content/attachments/file/hooks/useOpenEncryptedPdf.spec.tsx diff --git a/apps/meteor/client/components/message/content/attachments/file/hooks/useOpenEncryptedPdf.spec.tsx b/apps/meteor/client/components/message/content/attachments/file/hooks/useOpenEncryptedPdf.spec.tsx new file mode 100644 index 0000000000000..26c14fafa75cb --- /dev/null +++ b/apps/meteor/client/components/message/content/attachments/file/hooks/useOpenEncryptedPdf.spec.tsx @@ -0,0 +1,322 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { renderHook, act, waitFor } from '@testing-library/react'; + +import { useOpenEncryptedPdf } from './useOpenEncryptedPdf'; +import { forAttachmentDownload, registerDownloadForUid } from '../../../../../../hooks/useDownloadFromServiceWorker'; + +jest.mock('../../../../../../hooks/useDownloadFromServiceWorker', () => ({ + forAttachmentDownload: jest.fn(), + registerDownloadForUid: jest.fn(), +})); + +jest.mock('@rocket.chat/ui-contexts', () => ({ + ...jest.requireActual('@rocket.chat/ui-contexts'), + useMediaUrl: () => (url: string) => url, +})); + +const mockForAttachmentDownload = forAttachmentDownload as jest.MockedFunction; +const mockRegisterDownloadForUid = registerDownloadForUid as jest.MockedFunction; + +const mockAbort = jest.fn(); +const mockAbortController = jest.fn(() => { + const signal = { aborted: false }; + return { + abort: jest.fn(() => { + mockAbort(); + signal.aborted = true; + }), + signal, + }; +}); + +describe('useOpenEncryptedPdf', () => { + const testBlob = new Blob(['content'], { type: 'application/pdf' }); + const title = 'My PDF'; + const link = '/file-decrypt/encrypted-pdf.pdf'; + const format = 'PDF'; + const allowedSize = 5 * 1024 * 1024; + + let mockOpenDocumentViewer: jest.Mock; + let mockFetch: jest.Mock; + let mockRevokeObjectURL: jest.Mock; + let mockCreateObjectURL: jest.Mock; + + const originalFetch = global.fetch; + const originalCreateObjectURL = global.URL.createObjectURL; + const originalRevokeObjectURL = global.URL.revokeObjectURL; + const originalAbortController = global.AbortController; + + beforeEach(() => { + jest.clearAllMocks(); + + window.RocketChatDesktop = { + getE2ePdfPreviewSizeLimit: jest.fn(() => 15), + } as any; + + mockOpenDocumentViewer = jest.fn(); + + // Mock fetch + mockFetch = jest.fn(); + global.fetch = mockFetch; + + // Mock URL methods + mockCreateObjectURL = jest.fn(() => `blob:mock-url-${Math.random()}`); + mockRevokeObjectURL = jest.fn(); + global.URL.createObjectURL = mockCreateObjectURL; + global.URL.revokeObjectURL = mockRevokeObjectURL; + + // Mock AbortController + global.AbortController = mockAbortController as any; + }); + + afterEach(() => { + jest.restoreAllMocks(); + global.fetch = originalFetch; + global.URL.createObjectURL = originalCreateObjectURL; + global.URL.revokeObjectURL = originalRevokeObjectURL; + global.AbortController = originalAbortController; + delete (window as any).RocketChatDesktop; + }); + + describe('file size is not within the limit', () => { + it('should download file if it exceeds the preview size limit', async () => { + const { result } = renderHook(() => useOpenEncryptedPdf(), { + wrapper: mockAppRoot().build(), + }); + + const exceededSize = 20 * 1024 * 1024; // 20 MB (exceeds 15 MB limit) + + await act(async () => { + await result.current(link, title, exceededSize, format, mockOpenDocumentViewer); + }); + + expect(mockRegisterDownloadForUid).toHaveBeenCalled(); + expect(mockForAttachmentDownload).toHaveBeenCalledWith(expect.any(String), link); + expect(mockOpenDocumentViewer).not.toHaveBeenCalled(); + }); + + it('should download file if size is undefined', async () => { + const { result } = renderHook(() => useOpenEncryptedPdf(), { + wrapper: mockAppRoot().build(), + }); + + await act(async () => { + await result.current(link, title, undefined, format, mockOpenDocumentViewer); + }); + + expect(mockRegisterDownloadForUid).toHaveBeenCalled(); + expect(mockForAttachmentDownload).toHaveBeenCalledWith(expect.any(String), link); + expect(mockOpenDocumentViewer).not.toHaveBeenCalled(); + }); + }); + + describe('file size is within the limit', () => { + it('should fetch and open PDF', async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + blob: jest.fn().mockResolvedValueOnce(testBlob), + }); + + const { result } = renderHook(() => useOpenEncryptedPdf(), { + wrapper: mockAppRoot().build(), + }); + + await act(async () => { + await result.current(link, title, allowedSize, format, mockOpenDocumentViewer); + }); + + await waitFor(() => { + expect(mockFetch).toHaveBeenCalledWith(link, expect.objectContaining({ signal: expect.any(Object) })); + expect(mockCreateObjectURL).toHaveBeenCalledWith(testBlob); + expect(mockOpenDocumentViewer).toHaveBeenCalledWith(expect.stringContaining('blob:'), format, title); + }); + }); + + it('should open the PDF viewer if title is undefined, falling back to empty string', async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + blob: jest.fn().mockResolvedValueOnce(testBlob), + }); + + const { result } = renderHook(() => useOpenEncryptedPdf(), { + wrapper: mockAppRoot().build(), + }); + + await act(async () => { + await result.current(link, undefined, allowedSize, format, mockOpenDocumentViewer); + }); + + await waitFor(() => { + expect(mockFetch).toHaveBeenCalledWith(link, expect.objectContaining({ signal: expect.any(Object) })); + expect(mockCreateObjectURL).toHaveBeenCalledWith(testBlob); + expect(mockOpenDocumentViewer).toHaveBeenCalledWith(expect.stringContaining('blob:'), format, ''); + }); + }); + }); + + describe('when RocketChatDesktop is undefined', () => { + beforeEach(() => { + delete (window as any).RocketChatDesktop; + }); + + it('should fall back to 10MB limit and download if file exceeds it', async () => { + const { result } = renderHook(() => useOpenEncryptedPdf(), { + wrapper: mockAppRoot().build(), + }); + + const exceededSize = 11 * 1024 * 1024; // 11 MB (supera los 10MB del fallback) + + await act(async () => { + await result.current(link, title, exceededSize, format, mockOpenDocumentViewer); + }); + + expect(mockRegisterDownloadForUid).toHaveBeenCalled(); + expect(mockForAttachmentDownload).toHaveBeenCalledWith(expect.any(String), link); + expect(mockOpenDocumentViewer).not.toHaveBeenCalled(); + }); + + it('should fall back to 10MB limit and open PDF if size is within limit', async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + blob: jest.fn().mockResolvedValueOnce(testBlob), + }); + + const { result } = renderHook(() => useOpenEncryptedPdf(), { + wrapper: mockAppRoot().build(), + }); + + await act(async () => { + await result.current(link, title, allowedSize, format, mockOpenDocumentViewer); + }); + + await waitFor(() => { + expect(mockFetch).toHaveBeenCalledWith(link, expect.objectContaining({ signal: expect.any(Object) })); + expect(mockCreateObjectURL).toHaveBeenCalledWith(testBlob); + expect(mockOpenDocumentViewer).toHaveBeenCalledWith(expect.stringContaining('blob:'), format, title); + }); + }); + }); + + describe('blob URL management', () => { + it('should revoke previous blob URL before creating a new one', async () => { + mockFetch + .mockResolvedValueOnce({ + ok: true, + blob: jest.fn().mockResolvedValueOnce(testBlob), + }) + .mockResolvedValueOnce({ + ok: true, + blob: jest.fn().mockResolvedValueOnce(testBlob), + }); + + const { result } = renderHook(() => useOpenEncryptedPdf(), { + wrapper: mockAppRoot().build(), + }); + + // First call + await act(async () => { + await result.current(link, title, allowedSize, format, mockOpenDocumentViewer); + }); + + await waitFor(() => { + expect(mockCreateObjectURL).toHaveBeenCalledTimes(1); + }); + + // Second call + await act(async () => { + await result.current(link, title, allowedSize, format, mockOpenDocumentViewer); + }); + + await waitFor(() => { + expect(mockRevokeObjectURL).toHaveBeenCalled(); + expect(mockCreateObjectURL).toHaveBeenCalledTimes(2); + }); + }); + + it('should revoke blob URL on component unmount', async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + blob: jest.fn().mockResolvedValueOnce(testBlob), + }); + + const { result, unmount } = renderHook(() => useOpenEncryptedPdf(), { + wrapper: mockAppRoot().build(), + }); + + await act(async () => { + await result.current(link, title, allowedSize, format, mockOpenDocumentViewer); + }); + + await waitFor(() => { + expect(mockCreateObjectURL).toHaveBeenCalled(); + }); + + unmount(); + + expect(mockRevokeObjectURL).toHaveBeenCalled(); + }); + }); + + describe('fetch failure handling', () => { + let consoleErrorSpy: jest.SpyInstance; + + beforeEach(() => { + consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined); + }); + + afterEach(() => { + consoleErrorSpy.mockRestore(); + }); + + it('should throw error if fetch response is not ok', async () => { + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 404, + }); + + const { result } = renderHook(() => useOpenEncryptedPdf(), { + wrapper: mockAppRoot().build(), + }); + + await act(async () => { + await result.current(link, title, allowedSize, format, mockOpenDocumentViewer); + }); + + await waitFor(() => { + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'Error opening preview of encrypted PDF', + expect.objectContaining({ message: expect.stringContaining('Failed to fetch encrypted PDF: 404') }), + ); + }); + }); + }); + + describe('concurrent requests', () => { + it('should ignore blob from cancelled request', async () => { + let resolveFirstFetch: any; + + mockFetch + .mockImplementationOnce(() => { + return new Promise((resolve) => { + resolveFirstFetch = resolve; + }); + }) + .mockImplementationOnce(() => { + return Promise.resolve({ ok: true, blob: jest.fn().mockResolvedValueOnce(testBlob) }); + }); + + const { result } = renderHook(() => useOpenEncryptedPdf(), { wrapper: mockAppRoot().build() }); + + const promise1 = result.current(link, title, allowedSize, format, mockOpenDocumentViewer); + const promise2 = result.current(link, title, allowedSize, format, mockOpenDocumentViewer); + + await act(async () => { + resolveFirstFetch({ ok: true, blob: jest.fn().mockResolvedValue(testBlob) }); + await Promise.all([promise1, promise2]); + }); + + expect(mockOpenDocumentViewer).toHaveBeenCalledTimes(1); + expect(mockAbort).toHaveBeenCalledTimes(1); + }); + }); +}); From 6c4fdd7d616b04540453a08dce7edbfd1d6b213a Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Mon, 8 Jun 2026 18:15:38 -0300 Subject: [PATCH 16/20] remove comment --- .../content/attachments/file/hooks/useOpenEncryptedPdf.spec.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/client/components/message/content/attachments/file/hooks/useOpenEncryptedPdf.spec.tsx b/apps/meteor/client/components/message/content/attachments/file/hooks/useOpenEncryptedPdf.spec.tsx index 26c14fafa75cb..192841019dba3 100644 --- a/apps/meteor/client/components/message/content/attachments/file/hooks/useOpenEncryptedPdf.spec.tsx +++ b/apps/meteor/client/components/message/content/attachments/file/hooks/useOpenEncryptedPdf.spec.tsx @@ -164,7 +164,7 @@ describe('useOpenEncryptedPdf', () => { wrapper: mockAppRoot().build(), }); - const exceededSize = 11 * 1024 * 1024; // 11 MB (supera los 10MB del fallback) + const exceededSize = 11 * 1024 * 1024; // 11 MB await act(async () => { await result.current(link, title, exceededSize, format, mockOpenDocumentViewer); From 45007202dc2ecb005773530cfe55d6f29d1b420f Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Mon, 8 Jun 2026 20:17:31 -0300 Subject: [PATCH 17/20] add error handling in GenericFileAttachment --- .../file/GenericFileAttachment.tsx | 35 +++++++++++-------- .../file/hooks/useOpenEncryptedPdf.tsx | 1 + 2 files changed, 21 insertions(+), 15 deletions(-) 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 ee6d7978d3eff..d57325334098e 100644 --- a/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx +++ b/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx @@ -6,7 +6,7 @@ import { MessageGenericPreviewTitle, MessageGenericPreviewDescription, } from '@rocket.chat/fuselage'; -import { useMediaUrl } from '@rocket.chat/ui-contexts'; +import { useMediaUrl, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import { useId } from 'react'; import type { UIEvent } from 'react'; import { useTranslation } from 'react-i18next'; @@ -33,6 +33,7 @@ const GenericFileAttachment = ({ const uid = useId(); const { t } = useTranslation(); const openEncryptedPdf = useOpenEncryptedPdf(); + const dispatchToastMessage = useToastMessageDispatch(); const handleTitleClick = async (event: UIEvent): Promise => { if (!link) { @@ -41,24 +42,28 @@ const GenericFileAttachment = ({ const isEncrypted = link.includes('/file-decrypt/'); - if (format === 'PDF' && openDocumentViewer) { - event.preventDefault(); + try { + if (format === 'PDF' && openDocumentViewer) { + event.preventDefault(); - if (isEncrypted) { - openEncryptedPdf(link, title, size, format, openDocumentViewer); + if (isEncrypted) { + await openEncryptedPdf(link, title, size, format, openDocumentViewer); + return; + } + + const url = new URL(getURL(link), window.location.origin); + url.searchParams.set('contentDisposition', 'inline'); + openDocumentViewer(url.toString(), format, ''); return; } - const url = new URL(getURL(link), window.location.origin); - url.searchParams.set('contentDisposition', 'inline'); - openDocumentViewer(url.toString(), format, ''); - return; - } - - if (isEncrypted) { - event.preventDefault(); - registerDownloadForUid(uid, t, title); - forAttachmentDownload(uid, link); + if (isEncrypted) { + event.preventDefault(); + registerDownloadForUid(uid, t, title); + forAttachmentDownload(uid, link); + } + } catch (error) { + dispatchToastMessage({ type: 'error', message: t('FileUpload_Error_Trying_To_Open_File') }); } }; diff --git a/apps/meteor/client/components/message/content/attachments/file/hooks/useOpenEncryptedPdf.tsx b/apps/meteor/client/components/message/content/attachments/file/hooks/useOpenEncryptedPdf.tsx index c623a293767fc..1a3d39927c464 100644 --- a/apps/meteor/client/components/message/content/attachments/file/hooks/useOpenEncryptedPdf.tsx +++ b/apps/meteor/client/components/message/content/attachments/file/hooks/useOpenEncryptedPdf.tsx @@ -68,6 +68,7 @@ export const useOpenEncryptedPdf = () => { } catch (error: any) { if (error.name !== 'AbortError') { console.error('Error opening preview of encrypted PDF', error); + throw error; } } }; From 4f63e668233cdc625896840081fa74c16ffb3cb0 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Mon, 8 Jun 2026 20:19:20 -0300 Subject: [PATCH 18/20] add i18n for error --- packages/i18n/src/locales/en.i18n.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 3d361dc9ac675..a876ba8468531 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -2316,6 +2316,7 @@ "FileUpload_Enabled": "File Uploads Enabled", "FileUpload_Enabled_Direct": "File Uploads Enabled in Direct Messages ", "FileUpload_Error": "File Upload Error", + "FileUpload_Error_Trying_To_Open_File": "Error trying to open file", "FileUpload_FileSystemPath": "System Path", "FileUpload_File_Empty": "File empty", "FileUpload_Canceled": "Upload canceled", From b3504b48db3a21657e902199a29a01201a670e89 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Mon, 8 Jun 2026 20:28:18 -0300 Subject: [PATCH 19/20] fix test --- .../attachments/file/hooks/useOpenEncryptedPdf.spec.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/meteor/client/components/message/content/attachments/file/hooks/useOpenEncryptedPdf.spec.tsx b/apps/meteor/client/components/message/content/attachments/file/hooks/useOpenEncryptedPdf.spec.tsx index 192841019dba3..11de6f8ec0afa 100644 --- a/apps/meteor/client/components/message/content/attachments/file/hooks/useOpenEncryptedPdf.spec.tsx +++ b/apps/meteor/client/components/message/content/attachments/file/hooks/useOpenEncryptedPdf.spec.tsx @@ -278,9 +278,9 @@ describe('useOpenEncryptedPdf', () => { wrapper: mockAppRoot().build(), }); - await act(async () => { + await expect(async () => { await result.current(link, title, allowedSize, format, mockOpenDocumentViewer); - }); + }).rejects.toThrow('Failed to fetch encrypted PDF: 404'); await waitFor(() => { expect(consoleErrorSpy).toHaveBeenCalledWith( From aadda1344749bfc0d7b6c5bb54443dadaf36604a Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Mon, 8 Jun 2026 21:37:13 -0300 Subject: [PATCH 20/20] lint --- .../content/attachments/file/hooks/useOpenEncryptedPdf.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/client/components/message/content/attachments/file/hooks/useOpenEncryptedPdf.tsx b/apps/meteor/client/components/message/content/attachments/file/hooks/useOpenEncryptedPdf.tsx index 1a3d39927c464..cae87c48cf865 100644 --- a/apps/meteor/client/components/message/content/attachments/file/hooks/useOpenEncryptedPdf.tsx +++ b/apps/meteor/client/components/message/content/attachments/file/hooks/useOpenEncryptedPdf.tsx @@ -68,7 +68,7 @@ export const useOpenEncryptedPdf = () => { } catch (error: any) { if (error.name !== 'AbortError') { console.error('Error opening preview of encrypted PDF', error); - throw error; + throw error; } } };