From 021f169aac4e8510b1a5f4f5967006cc355371ee Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Tue, 9 Jun 2026 10:00:34 -0300 Subject: [PATCH 1/8] refactor: replace ReactElement type annotations with ReactNode in functions (#40817) --- .../components/Sidebar/SidebarNavigationItem.tsx | 4 ++-- apps/meteor/client/hooks/useFileInput.ts | 2 +- apps/meteor/client/hooks/useRoomIcon.tsx | 2 +- apps/meteor/client/hooks/useSingleFileInput.ts | 2 +- apps/meteor/client/lib/appLayout.tsx | 14 ++++++-------- apps/meteor/client/lib/createSidebarItems.ts | 4 ++-- apps/meteor/client/lib/normalizeThreadMessage.tsx | 3 +-- 7 files changed, 14 insertions(+), 17 deletions(-) diff --git a/apps/meteor/client/components/Sidebar/SidebarNavigationItem.tsx b/apps/meteor/client/components/Sidebar/SidebarNavigationItem.tsx index 5ee687b9a374f..1a07cc1440dc4 100644 --- a/apps/meteor/client/components/Sidebar/SidebarNavigationItem.tsx +++ b/apps/meteor/client/components/Sidebar/SidebarNavigationItem.tsx @@ -1,6 +1,6 @@ import { Box, Icon, Tag } from '@rocket.chat/fuselage'; import type { Keys as IconName } from '@rocket.chat/icons'; -import type { ReactElement } from 'react'; +import type { ReactNode } from 'react'; import { memo } from 'react'; import SidebarGenericItem from './SidebarGenericItem'; @@ -13,7 +13,7 @@ type SidebarNavigationItemProps = { tag?: string; currentPath?: string; externalUrl?: boolean; - badge?: () => ReactElement; + badge?: () => ReactNode; }; const SidebarNavigationItem = ({ diff --git a/apps/meteor/client/hooks/useFileInput.ts b/apps/meteor/client/hooks/useFileInput.ts index 48b44645e786e..4283cf00f1431 100644 --- a/apps/meteor/client/hooks/useFileInput.ts +++ b/apps/meteor/client/hooks/useFileInput.ts @@ -5,7 +5,7 @@ import type { AllHTMLAttributes } from 'react'; import { useChat } from '../views/room/contexts/ChatContext'; export const useFileInput = (props: AllHTMLAttributes) => { - const fileInputRef = useRef(); + const fileInputRef = useRef(undefined); const chatContext = useChat(); const setupFileInput = useSafeRefCallback( diff --git a/apps/meteor/client/hooks/useRoomIcon.tsx b/apps/meteor/client/hooks/useRoomIcon.tsx index 6d090d7d8f66c..2956864853a49 100644 --- a/apps/meteor/client/hooks/useRoomIcon.tsx +++ b/apps/meteor/client/hooks/useRoomIcon.tsx @@ -7,7 +7,7 @@ import { ReactiveUserStatus } from '../components/UserStatus'; export const useRoomIcon = ( room: Pick, -): ComponentProps | ReactElement | null => { +): ComponentProps | ReactElement | null => { if (room.abacAttributes) { if (room.teamMain) { return { name: 'team-shield' }; diff --git a/apps/meteor/client/hooks/useSingleFileInput.ts b/apps/meteor/client/hooks/useSingleFileInput.ts index bda6c9a1f353d..75fef0e0f3df9 100644 --- a/apps/meteor/client/hooks/useSingleFileInput.ts +++ b/apps/meteor/client/hooks/useSingleFileInput.ts @@ -8,7 +8,7 @@ export const useSingleFileInput = ( maxSize?: number, onError?: () => void, ): [onClick: () => void, reset: () => void] => { - const ref = useRef(); + const ref = useRef(undefined); useEffect(() => { const fileInput = document.createElement('input'); diff --git a/apps/meteor/client/lib/appLayout.tsx b/apps/meteor/client/lib/appLayout.tsx index 845db4148b3f3..0f2fc6920b729 100644 --- a/apps/meteor/client/lib/appLayout.tsx +++ b/apps/meteor/client/lib/appLayout.tsx @@ -1,5 +1,5 @@ import { Emitter } from '@rocket.chat/emitter'; -import type { ReactElement } from 'react'; +import type { ReactNode } from 'react'; import { lazy } from 'react'; const ConnectionStatusBar = lazy(() => import('../components/connectionStatus/ConnectionStatusBar')); @@ -9,25 +9,23 @@ const ActionManagerBusyState = lazy(() => import('../components/ActionManagerBus const AppLayoutThemeWrapper = lazy(() => import('../components/AppLayoutThemeWrapper')); const CloudAnnouncementsRegion = lazy(() => import('../views/cloud/CloudAnnouncementsRegion')); -type AppLayoutDescriptor = ReactElement | null; - class AppLayoutSubscription extends Emitter<{ update: void }> { - private descriptor: AppLayoutDescriptor = null; + private descriptor: ReactNode = null; - getSnapshot = (): AppLayoutDescriptor => this.descriptor; + getSnapshot = (): ReactNode => this.descriptor; subscribe = (onStoreChange: () => void): (() => void) => this.on('update', onStoreChange); - setCurrentValue(descriptor: AppLayoutDescriptor): void { + setCurrentValue(descriptor: ReactNode): void { this.descriptor = descriptor; this.emit('update'); } - render(element: ReactElement): void { + render(element: ReactNode): void { this.setCurrentValue(element); } - wrap(element: ReactElement): ReactElement { + wrap(element: ReactNode): ReactNode { return ( diff --git a/apps/meteor/client/lib/createSidebarItems.ts b/apps/meteor/client/lib/createSidebarItems.ts index d8541620aa203..868a2ed6d6768 100644 --- a/apps/meteor/client/lib/createSidebarItems.ts +++ b/apps/meteor/client/lib/createSidebarItems.ts @@ -1,6 +1,6 @@ import type { Keys as IconName } from '@rocket.chat/icons'; import type { LocationPathname } from '@rocket.chat/ui-contexts'; -import type { ReactElement } from 'react'; +import type { ReactNode } from 'react'; import { GO_ROCKET_CHAT_PREFIX } from './links'; @@ -13,7 +13,7 @@ export type Item = { pathSection?: string; name?: string; externalUrl?: boolean; - badge?: () => ReactElement; + badge?: () => ReactNode; }; export type SidebarDivider = { divider: boolean; i18nLabel: string }; export type SidebarItem = Item | SidebarDivider; diff --git a/apps/meteor/client/lib/normalizeThreadMessage.tsx b/apps/meteor/client/lib/normalizeThreadMessage.tsx index 7f13803d2f80a..067a23f6aedfe 100644 --- a/apps/meteor/client/lib/normalizeThreadMessage.tsx +++ b/apps/meteor/client/lib/normalizeThreadMessage.tsx @@ -1,12 +1,11 @@ import type { IMessage } from '@rocket.chat/core-typings'; import { Markup } from '@rocket.chat/gazzodown'; import { parse } from '@rocket.chat/message-parser'; -import type { ReactElement } from 'react'; import { filterMarkdown } from '../../app/markdown/lib/markdown'; import GazzodownText from '../components/GazzodownText'; -export function normalizeThreadMessage({ ...message }: Readonly>): ReactElement | null { +export function normalizeThreadMessage({ ...message }: Readonly>) { if (message.msg) { message.msg = filterMarkdown(message.msg); delete message.mentions; From fbe2a624f203e091608e5e72e48b2048d2c6ef31 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli <84046180+nazabucciarelli@users.noreply.github.com> Date: Tue, 9 Jun 2026 10:42:37 -0300 Subject: [PATCH 2/8] chore: adapt code to use of new desktop setting for encrypted pdf preview size limit (#40517) --- .../file/GenericFileAttachment.tsx | 39 ++- .../file/hooks/useOpenEncryptedPdf.spec.tsx | 322 ++++++++++++++++++ .../file/hooks/useOpenEncryptedPdf.tsx | 77 +++++ packages/desktop-api/src/index.ts | 2 + packages/i18n/src/locales/en.i18n.json | 1 + 5 files changed, 428 insertions(+), 13 deletions(-) create mode 100644 apps/meteor/client/components/message/content/attachments/file/hooks/useOpenEncryptedPdf.spec.tsx 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 2e0fb7107d983..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'; @@ -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; @@ -31,26 +32,38 @@ const GenericFileAttachment = ({ const getURL = useMediaUrl(); const uid = useId(); const { t } = useTranslation(); + const openEncryptedPdf = useOpenEncryptedPdf(); + const dispatchToastMessage = useToastMessageDispatch(); - const handleTitleClick = (event: UIEvent): void => { + const handleTitleClick = async (event: UIEvent): Promise => { if (!link) { return; } - if (openDocumentViewer && format === 'PDF') { - event.preventDefault(); + const isEncrypted = link.includes('/file-decrypt/'); - const url = new URL(getURL(link), window.location.origin); - url.searchParams.set('contentDisposition', 'inline'); - openDocumentViewer(url.toString(), format, ''); - return; - } + try { + if (format === 'PDF' && openDocumentViewer) { + event.preventDefault(); + + if (isEncrypted) { + await openEncryptedPdf(link, title, size, format, openDocumentViewer); + return; + } - if (link.includes('/file-decrypt/')) { - event.preventDefault(); + const url = new URL(getURL(link), window.location.origin); + url.searchParams.set('contentDisposition', 'inline'); + openDocumentViewer(url.toString(), format, ''); + return; + } - 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.spec.tsx b/apps/meteor/client/components/message/content/attachments/file/hooks/useOpenEncryptedPdf.spec.tsx new file mode 100644 index 0000000000000..11de6f8ec0afa --- /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 + + 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 expect(async () => { + await result.current(link, title, allowedSize, format, mockOpenDocumentViewer); + }).rejects.toThrow('Failed to fetch encrypted PDF: 404'); + + 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); + }); + }); +}); 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..cae87c48cf865 --- /dev/null +++ b/apps/meteor/client/components/message/content/attachments/file/hooks/useOpenEncryptedPdf.tsx @@ -0,0 +1,77 @@ +import { useMediaUrl } 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 = window.RocketChatDesktop?.getE2ePdfPreviewSizeLimit?.() ?? 10; + const pdfPreviewSizeLimitInBytes = pdfPreviewSizeLimit * 1024 * 1024; + 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 === undefined || size > pdfPreviewSizeLimitInBytes) { + 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); + throw error; + } + } + }; + + return openEncryptedPdf; +}; diff --git a/packages/desktop-api/src/index.ts b/packages/desktop-api/src/index.ts index 9d10c1f89d1c5..2d7b18c5cabb9 100644 --- a/packages/desktop-api/src/index.ts +++ b/packages/desktop-api/src/index.ts @@ -63,4 +63,6 @@ export interface IRocketChatDesktop { setUserToken: (token: string, userId: string) => void; openDocumentViewer: (url: string, format: string, options: any) => void; reloadServer: () => void; + getE2ePdfPreviewSizeLimit: () => number; + openInBrowser: (url: string) => void; } diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 921b3805a316b..6d06dfd8b6ad8 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -2332,6 +2332,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 1d949333c45ea33e905daf8b26b765da55342227 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Tue, 9 Jun 2026 12:23:09 -0300 Subject: [PATCH 3/8] refactor: remove unnecessary ReactElement type annotations in components (#40818) --- apps/meteor/client/components/Emoji.tsx | 3 +-- .../client/components/FingerprintChangeModal.tsx | 3 +-- .../FingerprintChangeModalConfirmation.tsx | 8 +------- apps/meteor/client/components/GazzodownText.tsx | 4 ++-- .../GenericUpsellModal/GenericUpsellModal.tsx | 7 +++---- apps/meteor/client/components/ListSkeleton.tsx | 3 +-- apps/meteor/client/components/LocalTime.tsx | 3 +-- apps/meteor/client/components/NotFoundState.tsx | 3 +-- apps/meteor/client/components/PageSkeleton.tsx | 3 +-- apps/meteor/client/components/RawText.tsx | 5 +---- .../RoomAutoComplete/RoomAutoComplete.tsx | 4 ++-- .../RoomAutoCompleteMultiple.tsx | 8 ++++---- apps/meteor/client/components/RoomIcon/RoomIcon.tsx | 4 ++-- apps/meteor/client/components/Sidebar/ListItem.tsx | 4 ++-- .../components/Sidebar/SidebarGenericItem.tsx | 4 ++-- .../components/SidebarToggler/SidebarToggler.tsx | 3 +-- apps/meteor/client/components/Skeleton.tsx | 4 ++-- apps/meteor/client/components/TextCopy.tsx | 8 ++++---- .../TwoFactorModal/TwoFactorEmailModal.tsx | 3 +-- .../components/TwoFactorModal/TwoFactorModal.tsx | 4 +--- .../TwoFactorModal/TwoFactorPasswordModal.tsx | 3 +-- .../TwoFactorModal/TwoFactorTotpModal.tsx | 3 +-- apps/meteor/client/components/UTCClock.tsx | 3 +-- apps/meteor/client/components/UrlChangeModal.tsx | 3 +-- .../UserAndRoomAutoCompleteMultiple.tsx | 4 ++-- .../UserAutoCompleteMultipleOptions.tsx | 4 ++-- .../client/components/UserCard/UserCardAction.tsx | 6 ++---- .../client/components/UserCard/UserCardActions.tsx | 4 ++-- .../client/components/UserCard/UserCardInfo.tsx | 4 ++-- .../client/components/UserCard/UserCardRole.tsx | 4 ++-- .../client/components/UserCard/UserCardRoles.tsx | 4 ++-- .../client/components/UserCard/UserCardUsername.tsx | 4 ++-- apps/meteor/client/components/UserInfo/UserInfo.tsx | 8 ++++---- .../client/components/UserInfo/UserInfoAction.tsx | 4 ++-- .../client/components/UserInfo/UserInfoAvatar.tsx | 4 ++-- .../client/components/UserInfo/UserInfoUsername.tsx | 6 +++--- .../components/UserStatus/ReactiveUserStatus.tsx | 4 ++-- apps/meteor/client/components/UserStatusMenu.tsx | 8 ++++---- apps/meteor/client/components/WarningModal.tsx | 4 ++-- .../client/components/avatar/RoomAvatarEditor.tsx | 2 +- .../avatar/UserAvatarEditor/UserAvatarEditor.tsx | 2 +- .../connectionStatus/useReconnectCountdown.ts | 2 +- .../components/dashboards/DownloadDataButton.tsx | 4 ++-- .../client/components/dashboards/PeriodSelector.tsx | 3 +-- apps/meteor/client/components/dataView/Counter.tsx | 4 ++-- .../client/components/dataView/CounterSet.tsx | 4 ++-- apps/meteor/client/components/dataView/Growth.tsx | 4 ++-- .../components/dataView/NegativeGrowthSymbol.tsx | 4 ++-- .../components/dataView/PositiveGrowthSymbol.tsx | 4 ++-- .../components/deviceManagement/DeviceIcon.tsx | 3 +-- .../DeviceManagementTable/DeviceManagementTable.tsx | 8 ++++---- .../components/deviceManagement/LoggedOutBanner.tsx | 3 +-- .../client/components/message/IgnoredContent.tsx | 4 ++-- .../components/message/MessageCollapsible.tsx | 4 ++-- .../client/components/message/MessageHeader.tsx | 3 +-- .../components/message/MessageToolbarHolder.tsx | 3 +-- .../components/message/ReadReceiptIndicator.tsx | 3 +-- .../client/components/message/StatusIndicators.tsx | 3 +-- .../client/components/message/content/Action.tsx | 4 ++-- .../components/message/content/Attachments.tsx | 3 +-- .../components/message/content/BroadcastMetrics.tsx | 3 +-- .../message/content/DiscussionMetrics.tsx | 3 +-- .../client/components/message/content/Location.tsx | 3 +-- .../components/message/content/MessageActions.tsx | 3 +-- .../client/components/message/content/Reactions.tsx | 4 ++-- .../components/message/content/ThreadMetrics.tsx | 3 +-- .../message/content/ThreadMetricsFollow.tsx | 4 ++-- .../message/content/ThreadMetricsParticipants.tsx | 3 +-- .../components/message/content/UrlPreviews.tsx | 3 +-- .../message/content/actions/MessageAction.tsx | 3 +-- .../message/content/attachments/AttachmentsItem.tsx | 3 +-- .../content/attachments/DefaultAttachment.tsx | 4 ++-- .../message/content/attachments/QuoteAttachment.tsx | 3 +-- .../attachments/default/ActionAttachmentButton.tsx | 4 ++-- .../structure/AttachmentAuthorAvatar.tsx | 3 +-- .../content/collapsible/CollapsibleContent.tsx | 4 ++-- .../message/content/reactions/Reaction.tsx | 4 ++-- .../content/urlPreviews/OEmbedCollapsible.tsx | 4 ++-- .../content/urlPreviews/OEmbedHtmlPreview.tsx | 3 +-- .../content/urlPreviews/OEmbedImagePreview.tsx | 3 +-- .../content/urlPreviews/OEmbedLinkPreview.tsx | 3 +-- .../content/urlPreviews/OEmbedPreviewContent.tsx | 13 ++----------- .../message/content/urlPreviews/OEmbedResolver.tsx | 4 +--- .../message/content/urlPreviews/UrlAudioPreview.tsx | 3 +-- .../message/content/urlPreviews/UrlImagePreview.tsx | 3 +-- .../message/content/urlPreviews/UrlPreview.tsx | 3 +-- .../content/urlPreviews/UrlPreviewResolver.tsx | 4 +--- .../message/content/urlPreviews/UrlVideoPreview.tsx | 6 ++---- .../components/message/header/MessageRoles.tsx | 3 +-- .../message/notification/AllMentionNotification.tsx | 4 +--- .../message/notification/MessageNotification.tsx | 3 +-- .../notification/UnreadMessagesNotification.tsx | 4 +--- .../components/message/toolbar/MessageToolbar.tsx | 11 ++--------- .../Timestamp/TimestampPicker/DatePicker.tsx | 4 ++-- .../Timestamp/TimestampPicker/FormatSelector.tsx | 4 ++-- .../actions/Timestamp/TimestampPicker/Preview.tsx | 3 +-- .../Timestamp/TimestampPicker/TimePicker.tsx | 4 ++-- .../Timestamp/TimestampPicker/TimezoneSelector.tsx | 4 ++-- .../components/message/variants/RoomMessage.tsx | 4 ++-- .../components/message/variants/SystemMessage.tsx | 4 ++-- .../components/message/variants/ThreadMessage.tsx | 3 +-- .../message/variants/ThreadMessagePreview.tsx | 4 ++-- .../message/variants/room/RoomMessageContent.tsx | 3 +-- .../variants/thread/ThreadMessageContent.tsx | 3 +-- .../threadPreview/ThreadMessagePreviewBody.tsx | 3 +-- 105 files changed, 168 insertions(+), 252 deletions(-) diff --git a/apps/meteor/client/components/Emoji.tsx b/apps/meteor/client/components/Emoji.tsx index 46e36459537c6..accd2517b20e1 100644 --- a/apps/meteor/client/components/Emoji.tsx +++ b/apps/meteor/client/components/Emoji.tsx @@ -1,5 +1,4 @@ import styled from '@rocket.chat/styled'; -import type { ReactElement } from 'react'; import { getEmojiClassNameAndDataTitle } from '../lib/utils/renderEmoji'; @@ -20,7 +19,7 @@ const EmojiComponent = styled('span', ({ fillContainer: _fillContainer, ...props : ''} `; -function Emoji({ emojiHandle, className = undefined, fillContainer }: EmojiProps): ReactElement { +function Emoji({ emojiHandle, className = undefined, fillContainer }: EmojiProps) { const { className: emojiClassName, image, ...props } = getEmojiClassNameAndDataTitle(emojiHandle); return ( diff --git a/apps/meteor/client/components/FingerprintChangeModal.tsx b/apps/meteor/client/components/FingerprintChangeModal.tsx index 066d77334cf44..170451eba0233 100644 --- a/apps/meteor/client/components/FingerprintChangeModal.tsx +++ b/apps/meteor/client/components/FingerprintChangeModal.tsx @@ -1,6 +1,5 @@ import { Box } from '@rocket.chat/fuselage'; import { ExternalLink, GenericModal } from '@rocket.chat/ui-client'; -import type { ReactElement } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { links } from '../lib/links'; @@ -11,7 +10,7 @@ type FingerprintChangeModalProps = { onClose: () => void; }; -const FingerprintChangeModal = ({ onConfirm, onCancel, onClose }: FingerprintChangeModalProps): ReactElement => { +const FingerprintChangeModal = ({ onConfirm, onCancel, onClose }: FingerprintChangeModalProps) => { const { t } = useTranslation(); return ( { +const FingerprintChangeModalConfirmation = ({ onConfirm, onCancel, onClose, newWorkspace }: FingerprintChangeModalConfirmationProps) => { const { t } = useTranslation(); return ( , 'variant' | 'children' | 'onClose' | 'onDismiss'> & { - subtitle?: string | ReactElement; - description?: string | ReactElement; + subtitle?: ReactNode; + description?: ReactNode; img: ComponentProps['src']; - imgWidth?: ComponentProps['width']; imgHeight?: ComponentProps['height']; imgAlt?: string; diff --git a/apps/meteor/client/components/ListSkeleton.tsx b/apps/meteor/client/components/ListSkeleton.tsx index 034f167bca0e8..f507ccdd42b0b 100644 --- a/apps/meteor/client/components/ListSkeleton.tsx +++ b/apps/meteor/client/components/ListSkeleton.tsx @@ -1,5 +1,4 @@ import { Box, Skeleton } from '@rocket.chat/fuselage'; -import type { ReactElement } from 'react'; import { memo, useMemo } from 'react'; const availablePercentualWidths = [47, 68, 75, 82]; @@ -8,7 +7,7 @@ type ListSkeletonProps = { listCount?: number; }; -const ListSkeleton = ({ listCount = 2 }: ListSkeletonProps): ReactElement => { +const ListSkeleton = ({ listCount = 2 }: ListSkeletonProps) => { const widths = useMemo( () => Array.from({ length: listCount }, (_, index) => `${availablePercentualWidths[index % availablePercentualWidths.length]}%`), [listCount], diff --git a/apps/meteor/client/components/LocalTime.tsx b/apps/meteor/client/components/LocalTime.tsx index be5133050e22b..ded8e707ed4cd 100644 --- a/apps/meteor/client/components/LocalTime.tsx +++ b/apps/meteor/client/components/LocalTime.tsx @@ -1,4 +1,3 @@ -import type { ReactElement } from 'react'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -8,7 +7,7 @@ type LocalTimeProps = { utcOffset: number; }; -const LocalTime = ({ utcOffset }: LocalTimeProps): ReactElement => { +const LocalTime = ({ utcOffset }: LocalTimeProps) => { const time = useUTCClock(utcOffset); const { t } = useTranslation(); diff --git a/apps/meteor/client/components/NotFoundState.tsx b/apps/meteor/client/components/NotFoundState.tsx index 47911f496b7ec..6fd3fe363807b 100644 --- a/apps/meteor/client/components/NotFoundState.tsx +++ b/apps/meteor/client/components/NotFoundState.tsx @@ -1,6 +1,5 @@ import { Box, States, StatesAction, StatesActions, StatesIcon, StatesSubtitle, StatesTitle } from '@rocket.chat/fuselage'; import { useRouter } from '@rocket.chat/ui-contexts'; -import type { ReactElement } from 'react'; import { useTranslation } from 'react-i18next'; type NotFoundProps = { @@ -8,7 +7,7 @@ type NotFoundProps = { subtitle: string; }; -const NotFoundState = ({ title, subtitle }: NotFoundProps): ReactElement => { +const NotFoundState = ({ title, subtitle }: NotFoundProps) => { const { t } = useTranslation(); const router = useRouter(); diff --git a/apps/meteor/client/components/PageSkeleton.tsx b/apps/meteor/client/components/PageSkeleton.tsx index ca6b78d3c0257..19f7b682fad55 100644 --- a/apps/meteor/client/components/PageSkeleton.tsx +++ b/apps/meteor/client/components/PageSkeleton.tsx @@ -1,8 +1,7 @@ import { Box, Button, ButtonGroup, Skeleton } from '@rocket.chat/fuselage'; import { Page, PageHeader, PageContent } from '@rocket.chat/ui-client'; -import type { ReactElement } from 'react'; -const PageSkeleton = (): ReactElement => ( +const PageSkeleton = () => ( }> diff --git a/apps/meteor/client/components/RawText.tsx b/apps/meteor/client/components/RawText.tsx index 58ae89e4d041c..975c2100761df 100644 --- a/apps/meteor/client/components/RawText.tsx +++ b/apps/meteor/client/components/RawText.tsx @@ -1,9 +1,6 @@ import DOMPurify from 'dompurify'; -import type { ReactElement } from 'react'; /** @deprecated */ -const RawText = ({ children }: { children: string }): ReactElement => ( - -); +const RawText = ({ children }: { children: string }) => ; export default RawText; diff --git a/apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.tsx b/apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.tsx index 5ebc5393b0847..e4e9472464670 100644 --- a/apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.tsx +++ b/apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.tsx @@ -6,7 +6,7 @@ import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; import { RoomAvatar } from '@rocket.chat/ui-avatar'; import { useEndpoint } from '@rocket.chat/ui-contexts'; import { keepPreviousData, useQuery } from '@tanstack/react-query'; -import type { ReactElement } from 'react'; +import type { ReactNode } from 'react'; import { forwardRef, memo, useMemo, useState } from 'react'; type LabelType = { name: string; avatarETag?: string; type: IRoom['t']; encrypted?: IRoom['encrypted'] }; @@ -19,7 +19,7 @@ const generateQuery = ( type RoomAutoCompleteProps = Omit, 'filter'> & { scope?: 'admin' | 'regular'; - renderRoomIcon?: (props: { encrypted: IRoom['encrypted']; type: IRoom['t'] }) => ReactElement | null; + renderRoomIcon?: (props: { encrypted: IRoom['encrypted']; type: IRoom['t'] }) => ReactNode; setSelectedRoom?: (room: IRoom | undefined) => void; }; diff --git a/apps/meteor/client/components/RoomAutoCompleteMultiple/RoomAutoCompleteMultiple.tsx b/apps/meteor/client/components/RoomAutoCompleteMultiple/RoomAutoCompleteMultiple.tsx index 401a8879e6093..99d558b72f9d5 100644 --- a/apps/meteor/client/components/RoomAutoCompleteMultiple/RoomAutoCompleteMultiple.tsx +++ b/apps/meteor/client/components/RoomAutoCompleteMultiple/RoomAutoCompleteMultiple.tsx @@ -3,7 +3,7 @@ import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; import { RoomAvatar } from '@rocket.chat/ui-avatar'; import { useEndpoint } from '@rocket.chat/ui-contexts'; import { keepPreviousData, useQuery } from '@tanstack/react-query'; -import type { ReactElement, ComponentProps } from 'react'; +import type { ComponentProps } from 'react'; import { memo, useMemo, useState } from 'react'; const generateQuery = ( @@ -16,7 +16,7 @@ type RoomAutoCompleteProps = Omit, 'filter'> readOnly?: boolean; }; -const RoomAutoCompleteMultiple = ({ value, onChange, ...props }: RoomAutoCompleteProps): ReactElement => { +const RoomAutoCompleteMultiple = ({ value, onChange, ...props }: RoomAutoCompleteProps) => { const [filter, setFilter] = useState(''); const filterDebounced = useDebouncedValue(filter, 300); const autocomplete = useEndpoint('GET', '/v1/rooms.autocomplete.channelAndPrivate'); @@ -50,7 +50,7 @@ const RoomAutoCompleteMultiple = ({ value, onChange, ...props }: RoomAutoComplet filter={filter} setFilter={setFilter} multiple - renderSelected={({ selected: { value, label }, onRemove, ...props }): ReactElement => ( + renderSelected={({ selected: { value, label }, onRemove, ...props }) => ( @@ -58,7 +58,7 @@ const RoomAutoCompleteMultiple = ({ value, onChange, ...props }: RoomAutoComplet )} - renderItem={({ value, label, ...props }): ReactElement => ( + renderItem={({ value, label, ...props }) => (