wip allow revealing all medias at once

This commit is contained in:
Thomas Ramé
2026-03-05 00:46:54 +01:00
parent 1eba8b77c0
commit 4baef38cae
4 changed files with 115 additions and 3 deletions
@@ -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 (
<EncryptionProvider symmetricKey={symmetricKey}>
<EncryptedDocBanner />
<Box
ref={refEditorContainer}
$css={css`
@@ -0,0 +1,34 @@
import { Button } from '@gouvfr-lasuite/cunningham-react';
import { useTranslation } from 'react-i18next';
import { Box, Icon } from '@/components';
import { useEncryption } from './EncryptionProvider';
export const EncryptedDocBanner = () => {
const { t } = useTranslation();
const { isEncrypted, pendingPlaceholders, requestRevealAll } =
useEncryption();
if (!isEncrypted || pendingPlaceholders === 0) {
return null;
}
return (
<Box
$direction="row"
$align="center"
$justify="flex-end"
$padding={{ horizontal: '54px', vertical: '3xs' }}
>
<Button
size="small"
variant="tertiary"
onClick={requestRevealAll}
icon={<Icon iconName="visibility" $size="sm" $color="inherit" />}
>
{t('Reveal all media')}
</Button>
</Box>
);
};
@@ -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<string, string> = {
interface EncryptionContextValue {
isEncrypted: boolean;
decryptFileUrl: (url: string) => Promise<string>;
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<EncryptionContextValue>(DEFAULT_VALUE);
@@ -56,6 +69,20 @@ export const EncryptionProvider = ({
children,
}: EncryptionProviderProps) => {
const blobUrlCacheRef = useRef<Map<string, string>>(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<string> => {
@@ -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 (
@@ -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<string | null>(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,