FixThumbnailRegeneration (#6134)

This commit is contained in:
Anthony Stirling
2026-04-22 14:33:38 +01:00
committed by GitHub
parent 97e2dc2c68
commit d71a2c3d81
@@ -3,6 +3,7 @@ import { StirlingFileStub } from "@app/types/fileContext";
import { useIndexedDB } from "@app/contexts/IndexedDBContext"; import { useIndexedDB } from "@app/contexts/IndexedDBContext";
import { generateThumbnailForFile } from "@app/utils/thumbnailUtils"; import { generateThumbnailForFile } from "@app/utils/thumbnailUtils";
import { FileId } from "@app/types/fileContext"; import { FileId } from "@app/types/fileContext";
import { useFileManagement } from "@app/contexts/FileContext";
/** /**
* Hook for IndexedDB-aware thumbnail loading * Hook for IndexedDB-aware thumbnail loading
@@ -17,6 +18,7 @@ export function useIndexedDBThumbnail(
const [thumb, setThumb] = useState<string | null>(null); const [thumb, setThumb] = useState<string | null>(null);
const [generating, setGenerating] = useState(false); const [generating, setGenerating] = useState(false);
const indexedDB = useIndexedDB(); const indexedDB = useIndexedDB();
const { updateStirlingFileStub } = useFileManagement();
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
@@ -27,66 +29,71 @@ export function useIndexedDBThumbnail(
return; return;
} }
// First priority: use stored thumbnail // Tier 1: stored thumbnail on the stub.
if (file.thumbnailUrl) { if (file.thumbnailUrl) {
setThumb(file.thumbnailUrl); setThumb(file.thumbnailUrl);
return; return;
} }
// Second priority: generate thumbnail for files under 100MB // >=100MB files are skipped entirely — no thumbnail.
if (file.size < 100 * 1024 * 1024 && !generating) { 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); setGenerating(true);
try { try {
let fileObject: File; if (!file.id || !indexedDB) {
// 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( throw new Error(
"File ID not available or IndexedDB context not available", `missing prerequisite fileId=${file.id} indexedDB=${Boolean(indexedDB)}`,
); );
} }
// Use the universal thumbnail generator const loadedFile = await indexedDB.loadFile(file.id as FileId);
const thumbnail = await generateThumbnailForFile(fileObject); if (!loadedFile) {
if (!cancelled) { throw new Error("not in IndexedDB (likely remote-only stub)");
}
const thumbnail = await generateThumbnailForFile(loadedFile);
if (cancelled) return;
setThumb(thumbnail); setThumb(thumbnail);
// Save thumbnail to IndexedDB for persistence
if (file.id && indexedDB && thumbnail) { if (file.id && indexedDB && thumbnail) {
try { try {
await indexedDB.updateThumbnail(file.id as FileId, thumbnail); 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) { } catch (error) {
console.warn("Failed to save thumbnail to IndexedDB:", error); console.warn("Failed to persist thumbnail:", error);
}
} }
} }
} catch (error) { } catch (error) {
console.warn( console.warn("Failed to generate thumbnail for file", file.name, error);
"Failed to generate thumbnail for file",
file.name,
error,
);
if (!cancelled) setThumb(null); if (!cancelled) setThumb(null);
} finally { } finally {
if (!cancelled) setGenerating(false); if (!cancelled) setGenerating(false);
} }
} else {
// Large files - no thumbnail
setThumb(null);
}
} }
loadThumbnail(); loadThumbnail();
return () => { return () => {
cancelled = true; 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 }; return { thumbnail: thumb, isGenerating: generating };
} }