diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx index d2efbefe..700a76b0 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx @@ -40,6 +40,7 @@ import { DocsBlockNoteEditor } from '../types'; import { randomColor } from '../utils'; import { BlockNoteSuggestionMenu } from './BlockNoteSuggestionMenu'; +import { EncryptedDocBanner } from './EncryptedDocBanner'; import { EncryptionProvider } from './EncryptionProvider'; import { BlockNoteToolbar } from './BlockNoteToolBar/BlockNoteToolbar'; import { cssComments, useComments } from './comments/'; @@ -249,6 +250,7 @@ export const BlockNoteEditor = ({ return ( + { + const { t } = useTranslation(); + const { isEncrypted, pendingPlaceholders, requestRevealAll } = + useEncryption(); + + if (!isEncrypted || pendingPlaceholders === 0) { + return null; + } + + return ( + + + + ); +}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/EncryptionProvider.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/EncryptionProvider.tsx index 2d08563f..11df0c4f 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/EncryptionProvider.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/EncryptionProvider.tsx @@ -6,6 +6,7 @@ import { useEffect, useMemo, useRef, + useState, } from 'react'; import { decryptContent } from '@/docs/doc-collaboration/encryption'; @@ -37,11 +38,23 @@ const MIME_MAP: Record = { interface EncryptionContextValue { isEncrypted: boolean; decryptFileUrl: (url: string) => Promise; + revealAllCounter: number; + requestRevealAll: () => void; + pendingPlaceholders: number; + registerPlaceholder: () => void; + unregisterPlaceholder: () => void; } +const noop = () => {}; + const DEFAULT_VALUE: EncryptionContextValue = { isEncrypted: false, decryptFileUrl: async (url: string) => url, + revealAllCounter: 0, + requestRevealAll: noop, + pendingPlaceholders: 0, + registerPlaceholder: noop, + unregisterPlaceholder: noop, }; const EncryptionContext = createContext(DEFAULT_VALUE); @@ -56,6 +69,20 @@ export const EncryptionProvider = ({ children, }: EncryptionProviderProps) => { const blobUrlCacheRef = useRef>(new Map()); + const [revealAllCounter, setRevealAllCounter] = useState(0); + const [pendingPlaceholders, setPendingPlaceholders] = useState(0); + + const requestRevealAll = useCallback(() => { + setRevealAllCounter((c) => c + 1); + }, []); + + const registerPlaceholder = useCallback(() => { + setPendingPlaceholders((c) => c + 1); + }, []); + + const unregisterPlaceholder = useCallback(() => { + setPendingPlaceholders((c) => Math.max(0, c - 1)); + }, []); const decryptFileUrl = useCallback( async (url: string): Promise => { @@ -103,8 +130,21 @@ export const EncryptionProvider = ({ () => ({ isEncrypted: !!symmetricKey, decryptFileUrl, + revealAllCounter, + requestRevealAll, + pendingPlaceholders, + registerPlaceholder, + unregisterPlaceholder, }), - [symmetricKey, decryptFileUrl], + [ + symmetricKey, + decryptFileUrl, + revealAllCounter, + requestRevealAll, + pendingPlaceholders, + registerPlaceholder, + unregisterPlaceholder, + ], ); return ( diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useDecryptMedia.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useDecryptMedia.tsx index c7952787..dcf96927 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useDecryptMedia.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useDecryptMedia.tsx @@ -1,10 +1,16 @@ -import { useCallback, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { useEncryption } from '../components/EncryptionProvider'; import { ANALYZE_URL } from '../conf'; export const useDecryptMedia = (url: string | undefined) => { - const { isEncrypted, decryptFileUrl } = useEncryption(); + const { + isEncrypted, + decryptFileUrl, + revealAllCounter, + registerPlaceholder, + unregisterPlaceholder, + } = useEncryption(); const [resolvedUrl, setResolvedUrl] = useState(null); const [isLoading, setIsLoading] = useState(false); const [hasError, setHasError] = useState(false); @@ -28,11 +34,41 @@ export const useDecryptMedia = (url: string | undefined) => { } }, [url, resolvedUrl, isLoading, decryptFileUrl]); + // Auto-decrypt when "Reveal all" is requested + const decryptRef = useRef(decrypt); + decryptRef.current = decrypt; + + useEffect(() => { + if (revealAllCounter > 0 && isEncrypted && url && !isAnalyzing) { + void decryptRef.current(); + } + }, [revealAllCounter, isEncrypted, url, isAnalyzing]); + const showPlaceholder = isEncrypted && !resolvedUrl && !hasError && !!url && !isAnalyzing; const showMedia = !!url && !isAnalyzing && (!isEncrypted || !!resolvedUrl); + // Track pending placeholders in the provider + const wasShowingPlaceholder = useRef(false); + useEffect(() => { + if (showPlaceholder && !wasShowingPlaceholder.current) { + registerPlaceholder(); + wasShowingPlaceholder.current = true; + } else if (!showPlaceholder && wasShowingPlaceholder.current) { + unregisterPlaceholder(); + wasShowingPlaceholder.current = false; + } + }, [showPlaceholder, registerPlaceholder, unregisterPlaceholder]); + + useEffect(() => { + return () => { + if (wasShowingPlaceholder.current) { + unregisterPlaceholder(); + } + }; + }, [unregisterPlaceholder]); + return { isEncrypted, resolvedUrl,