From d71a2c3d8185b2fab25a651817e8adbf8677e04f Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Wed, 22 Apr 2026 14:33:38 +0100 Subject: [PATCH] FixThumbnailRegeneration (#6134) --- .../src/core/hooks/useIndexedDBThumbnail.ts | 101 ++++++++++-------- 1 file changed, 54 insertions(+), 47 deletions(-) diff --git a/frontend/src/core/hooks/useIndexedDBThumbnail.ts b/frontend/src/core/hooks/useIndexedDBThumbnail.ts index 5ee9e45e26..3b454e6849 100644 --- a/frontend/src/core/hooks/useIndexedDBThumbnail.ts +++ b/frontend/src/core/hooks/useIndexedDBThumbnail.ts @@ -3,6 +3,7 @@ import { StirlingFileStub } from "@app/types/fileContext"; import { useIndexedDB } from "@app/contexts/IndexedDBContext"; import { generateThumbnailForFile } from "@app/utils/thumbnailUtils"; import { FileId } from "@app/types/fileContext"; +import { useFileManagement } from "@app/contexts/FileContext"; /** * Hook for IndexedDB-aware thumbnail loading @@ -17,6 +18,7 @@ export function useIndexedDBThumbnail( const [thumb, setThumb] = useState(null); const [generating, setGenerating] = useState(false); const indexedDB = useIndexedDB(); + const { updateStirlingFileStub } = useFileManagement(); useEffect(() => { let cancelled = false; @@ -27,58 +29,59 @@ export function useIndexedDBThumbnail( return; } - // First priority: use stored thumbnail + // Tier 1: stored thumbnail on the stub. if (file.thumbnailUrl) { setThumb(file.thumbnailUrl); return; } - // Second priority: generate thumbnail for files under 100MB - if (file.size < 100 * 1024 * 1024 && !generating) { - setGenerating(true); - try { - let fileObject: File; - - // Try to load file from IndexedDB using new context - if (file.id && indexedDB) { - const loadedFile = await indexedDB.loadFile(file.id as FileId); - if (!loadedFile) { - throw new Error("File not found in IndexedDB"); - } - fileObject = loadedFile; - } else { - throw new Error( - "File ID not available or IndexedDB context not available", - ); - } - - // Use the universal thumbnail generator - const thumbnail = await generateThumbnailForFile(fileObject); - if (!cancelled) { - setThumb(thumbnail); - - // Save thumbnail to IndexedDB for persistence - if (file.id && indexedDB && thumbnail) { - try { - await indexedDB.updateThumbnail(file.id as FileId, thumbnail); - } catch (error) { - console.warn("Failed to save thumbnail to IndexedDB:", error); - } - } - } - } catch (error) { - console.warn( - "Failed to generate thumbnail for file", - file.name, - error, - ); - if (!cancelled) setThumb(null); - } finally { - if (!cancelled) setGenerating(false); - } - } else { - // Large files - no thumbnail + // >=100MB files are skipped entirely — no thumbnail. + if (file.size >= 100 * 1024 * 1024) { setThumb(null); + return; + } + + // Tier 2: generate on demand from the File bytes in IndexedDB. + // Re-entry guard is handled by the effect's cleanup/cancelled pattern — + // `generating` is NOT in the deps, so setGenerating() does not trigger + // the effect to re-run and cancel itself mid-flight. + setGenerating(true); + try { + if (!file.id || !indexedDB) { + throw new Error( + `missing prerequisite fileId=${file.id} indexedDB=${Boolean(indexedDB)}`, + ); + } + + const loadedFile = await indexedDB.loadFile(file.id as FileId); + if (!loadedFile) { + throw new Error("not in IndexedDB (likely remote-only stub)"); + } + + const thumbnail = await generateThumbnailForFile(loadedFile); + if (cancelled) return; + + setThumb(thumbnail); + + if (file.id && indexedDB && thumbnail) { + try { + await indexedDB.updateThumbnail(file.id as FileId, thumbnail); + // Also sync the in-memory stub so subsequent re-mounts hit tier 1 + // instead of regenerating. IndexedDB persistence alone only helps + // the next page load; the current session reads file.thumbnailUrl + // from the FileContext stub. + updateStirlingFileStub(file.id as FileId, { + thumbnailUrl: thumbnail, + }); + } catch (error) { + console.warn("Failed to persist thumbnail:", error); + } + } + } catch (error) { + console.warn("Failed to generate thumbnail for file", file.name, error); + if (!cancelled) setThumb(null); + } finally { + if (!cancelled) setGenerating(false); } } @@ -86,7 +89,11 @@ export function useIndexedDBThumbnail( return () => { cancelled = true; }; - }, [file, file?.thumbnailUrl, file?.id, indexedDB, generating]); + // `generating` is intentionally NOT in the deps — it's an internal flag + // set by this effect, and including it caused the effect to cancel + // itself mid-flight (orphaning the render and leaving generating=true + // stuck forever). + }, [file, file?.thumbnailUrl, file?.id, indexedDB, updateStirlingFileStub]); return { thumbnail: thumb, isGenerating: generating }; }