From 9aeedd1d03354cb43bc595142317cb00a0fca76d Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Fri, 28 Nov 2025 09:19:08 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=EF=B8=8F(frontend)=20improve=20Upload?= =?UTF-8?q?File=20process?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We notices that `context.getChanges` was very greedy, on a large document with multiple users collaborating, it caused performance issues. We change the way that we track a upload by listening onUploadEnd event instead of tracking all changes in the document. When a doc opens, we check if there are any ongoing uploads and resume them. We fix as well a race condition that could happen when multiple collaborators were on a document during an upload. --- CHANGELOG.md | 1 + .../custom-blocks/UploadLoaderBlock.tsx | 46 ++++--- .../docs/doc-editor/hook/useUploadFile.tsx | 127 ++++++++++-------- 3 files changed, 100 insertions(+), 74 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45ed237a..b1905aa8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to - ⚡️(sw) stop to cache external resources likes videos #1655 - 💥(frontend) upgrade to ui-kit v2 +- ⚡️(frontend) improve perf on upload and table of contents #1662 ### Fixed diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/UploadLoaderBlock.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/UploadLoaderBlock.tsx index 26c5c74e..39406239 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/UploadLoaderBlock.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/UploadLoaderBlock.tsx @@ -75,27 +75,31 @@ const UploadLoaderBlockComponent = ({ loopCheckDocMediaStatus(url) .then((response) => { - // Replace the loading block with the resource block (image, audio, video, pdf ...) - try { - editor.replaceBlocks( - [block.id], - [ - { - type: block.props.blockUploadType, - props: { - url: `${mediaUrl}${response.file}`, - showPreview: block.props.blockUploadShowPreview, - name: block.props.blockUploadName, - caption: '', - backgroundColor: 'default', - textAlignment: 'left', - }, - } as never, - ], - ); - } catch { - /* During collaboration, another user might have updated the block */ - } + // Add random delay to reduce collision probability during collaboration + const randomDelay = Math.random() * 800; + setTimeout(() => { + // Replace the loading block with the resource block (image, audio, video, pdf ...) + try { + editor.replaceBlocks( + [block.id], + [ + { + type: block.props.blockUploadType, + props: { + url: `${mediaUrl}${response.file}`, + showPreview: block.props.blockUploadShowPreview, + name: block.props.blockUploadName, + caption: '', + backgroundColor: 'default', + textAlignment: 'left', + }, + } as never, + ], + ); + } catch { + /* During collaboration, another user might have updated the block */ + } + }, randomDelay); }) .catch((error) => { console.error('Error analyzing file:', error); diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useUploadFile.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useUploadFile.tsx index d3451ed0..5bea3435 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useUploadFile.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useUploadFile.tsx @@ -1,3 +1,4 @@ +import { Block } from '@blocknote/core'; import { captureException } from '@sentry/nextjs'; import { useCallback, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; @@ -36,73 +37,93 @@ export const useUploadFile = (docId: string) => { }; }; +/** + * When we upload a file it can takes some time to analyze it (e.g. virus scan). + * This hook listen to upload end and replace the uploaded block by a uploadLoader + * block to show analyzing status. + * The uploadLoader block will then handle the status display until the analysis is done + * then replaced by the final block (e.g. image, pdf, etc.). + * @param editor + */ export const useUploadStatus = (editor: DocsBlockNoteEditor) => { const ANALYZE_URL = 'media-check'; const { t } = useTranslation(); - useEffect(() => { - const unsubscribe = editor.onChange((_, context) => { - const blocksChanges = context.getChanges(); - - if (!blocksChanges.length) { - return; - } - - const blockChanges = blocksChanges[0]; - + /** + * Replace the resource block by a uploadLoader block to show analyzing status + */ + const replaceBlockWithUploadLoader = useCallback( + (block: Block) => { if ( - blockChanges.source.type !== 'local' || - blockChanges.type !== 'update' || - !('url' in blockChanges.block.props) || - ('url' in blockChanges.block.props && - !blockChanges.block.props.url.includes(ANALYZE_URL)) + !block || + !('url' in block.props) || + ('url' in block.props && !block.props.url.includes(ANALYZE_URL)) ) { return; } - const blockUploadUrl = blockChanges.block.props.url; - const blockUploadType = blockChanges.block.type; - const blockUploadName = blockChanges.block.props.name; + const blockUploadUrl = block.props.url; + const blockUploadType = block.type; + const blockUploadName = block.props.name; const blockUploadShowPreview = - ('showPreview' in blockChanges.block.props && - blockChanges.block.props.showPreview) || - false; + ('showPreview' in block.props && block.props.showPreview) || false; - const timeoutId = setTimeout(() => { - // Replace the resource block by a uploadLoader block - // to show analyzing status - try { - editor.replaceBlocks( - [blockChanges.block.id], - [ - { - type: 'uploadLoader', - props: { - information: t('Analyzing file...'), - type: 'loading', - blockUploadName, - blockUploadType, - blockUploadUrl, - blockUploadShowPreview, - }, + try { + editor.replaceBlocks( + [block.id], + [ + { + type: 'uploadLoader', + props: { + information: t('Analyzing file...'), + type: 'loading', + blockUploadName, + blockUploadType, + blockUploadUrl, + blockUploadShowPreview, }, - ], - ); - } catch (error) { - captureException(error, { - extra: { info: 'Error replacing block for upload loader' }, - }); - } - }, 250); + }, + ], + ); + } catch (error) { + captureException(error, { + extra: { info: 'Error replacing block for upload loader' }, + }); + } + }, + [editor, t], + ); + + useEffect(() => { + const imagesBlocks = editor?.document.filter( + (block) => + block.type === 'image' && block.props.url.includes(ANALYZE_URL), + ); + + imagesBlocks.forEach((block) => { + replaceBlockWithUploadLoader(block as Block); + }); + }, [editor, replaceBlockWithUploadLoader]); + + /** + * Handle upload end to replace the upload block by a uploadLoader + * block to show analyzing status + */ + useEffect(() => { + editor.onUploadEnd((blockId) => { + if (!blockId) { + return; + } + + const innerTimeoutId = setTimeout(() => { + const block = editor.getBlock({ id: blockId }); + + replaceBlockWithUploadLoader(block as Block); + }, 300); return () => { - clearTimeout(timeoutId); - unsubscribe(); + clearTimeout(innerTimeoutId); }; }); - - return () => { - unsubscribe(); - }; - }, [editor, t]); + }, [editor, replaceBlockWithUploadLoader]); };