import { useCallback, useEffect, useRef, useState } from 'react'; const useDocumentPreview = ({ routeDocumentId, documents, searchResults, selectedFolder, assetManager, api, resolveApiPath, notifyApiError, navigate, locationPathname, locationSearch, detailPanelControlRef, setActivePreviewId, }) => { const [previewEntries, setPreviewEntries] = useState(() => new Map()); const [previewDocuments, setPreviewDocuments] = useState(() => new Map()); const previewInflightRef = useRef(new Map()); const previewReturnPathRef = useRef(null); const resetPreviewState = useCallback(() => { setPreviewEntries(() => new Map()); previewInflightRef.current = new Map(); previewReturnPathRef.current = null; }, []); const removePreviewEntries = useCallback((ids) => { if (!Array.isArray(ids) || ids.length === 0) { return; } setPreviewEntries((prev) => { if (!prev.size) { return prev; } let changed = false; const next = new Map(prev); ids.forEach((id) => { if (next.delete(id)) { changed = true; } previewInflightRef.current.delete(id); }); return changed ? next : prev; }); }, []); const cachePreviewDocument = useCallback((doc) => { if (!doc?.id) { return; } setPreviewDocuments((prev) => { const existing = prev.get(doc.id); if (existing === doc) { return prev; } const next = new Map(prev); next.set(doc.id, doc); return next; }); }, []); const removeCachedPreviewDocument = useCallback((documentId) => { if (!documentId) { return; } setPreviewDocuments((prev) => { if (!prev.has(documentId)) { return prev; } const next = new Map(prev); next.delete(documentId); return next; }); }, []); const ensurePreviewUrl = useCallback( async (documentId, { force = false } = {}) => { if (!documentId) return null; const existing = previewEntries.get(documentId) || null; const now = Date.now(); const expiresAt = typeof existing?.expiresAt === 'number' ? existing.expiresAt : null; if (!force && existing && (!expiresAt || expiresAt > now)) { return existing; } if (!force && previewInflightRef.current.has(documentId)) { return previewInflightRef.current.get(documentId); } const request = (async () => { try { const docResponse = await api.get(`/documents/${documentId}`); const downloadPath = docResponse.data?.document?.current_version?.download_path; if (!downloadPath || !resolveApiPath) { throw new Error('Document missing download path'); } const href = resolveApiPath(downloadPath); const entry = { url: href, contentType: docResponse.data?.document?.current_version?.version?.content_type || null, filename: docResponse.data?.document?.filename, expiresAt: Date.now() + 5 * 60 * 1000, }; setPreviewEntries((prev) => { const next = new Map(prev); next.set(documentId, entry); return next; }); return entry; } catch (error) { notifyApiError(error, 'Unable to fetch document preview.'); throw error; } finally { previewInflightRef.current.delete(documentId); } })(); previewInflightRef.current.set(documentId, request); return request; }, [previewEntries, api, resolveApiPath, notifyApiError, setPreviewEntries], ); const ensurePreviewData = useCallback( async (documentId) => { if (!documentId) return null; const findInCache = () => { const pool = searchResults ?? documents; return pool.find((item) => item.id === documentId) || null; }; let doc = findInCache(); if (!doc) { const { data } = await api.get(`/documents/${documentId}`); const hydratedDetail = assetManager.hydrateDetail(data); const fetched = hydratedDetail?.document || data.document || data; doc = fetched ? assetManager.hydrateDocument(fetched) : null; if (!doc) { throw new Error('Document metadata unavailable.'); } const existsInDocuments = documents.some((item) => item.id === doc.id); const existsInSearch = Array.isArray(searchResults) ? searchResults.some((item) => item.id === doc.id) : false; if (existsInDocuments || existsInSearch) { removeCachedPreviewDocument(doc.id); } else { cachePreviewDocument(doc); } } if (!previewReturnPathRef.current) { const fallbackFolderId = doc?.folder_id || 'root'; previewReturnPathRef.current = fallbackFolderId === 'root' ? '/documents' : `/documents/folder/${fallbackFolderId}`; } await ensurePreviewUrl(documentId, { force: false }); setActivePreviewId(documentId); return doc; }, [ searchResults, documents, assetManager, ensurePreviewUrl, setActivePreviewId, api, cachePreviewDocument, removeCachedPreviewDocument, ], ); const openDocumentPreview = useCallback( (documentId, { replace = false } = {}) => { if (!documentId) return; detailPanelControlRef.current.close(); previewReturnPathRef.current = `${locationPathname}${locationSearch}`; navigate(`/documents/${documentId}`, { replace }); }, [navigate, locationPathname, locationSearch, detailPanelControlRef], ); const closeDocumentPreview = useCallback( (folderId = null) => { const fallbackPath = previewReturnPathRef.current; previewReturnPathRef.current = null; if (fallbackPath) { navigate(fallbackPath, { replace: false }); return; } const targetId = folderId || selectedFolder || 'root'; const path = targetId === 'root' ? '/documents' : `/documents/folder/${targetId}`; navigate(path, { replace: false }); }, [navigate, selectedFolder], ); useEffect(() => { if (!routeDocumentId) { return undefined; } const handleKeyDown = (event) => { if (event.key === 'Escape') { closeDocumentPreview(); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [routeDocumentId, closeDocumentPreview]); useEffect(() => { if (!routeDocumentId) { return undefined; } let cancelled = false; ensurePreviewData(routeDocumentId).catch((error) => { if (cancelled) { return; } notifyApiError(error, 'Failed to open document preview.'); closeDocumentPreview(); }); return () => { cancelled = true; }; }, [routeDocumentId, ensurePreviewData, notifyApiError, closeDocumentPreview]); useEffect(() => { setPreviewDocuments((prev) => { if (!prev.size) { return prev; } const next = new Map(prev); let changed = false; const prune = (list) => { if (!Array.isArray(list)) { return; } list.forEach((doc) => { if (doc?.id && next.has(doc.id)) { next.delete(doc.id); changed = true; } }); }; prune(documents); prune(searchResults); return changed ? next : prev; }); }, [documents, searchResults]); return { previewEntries, previewDocuments, ensurePreviewUrl, ensurePreviewData, openDocumentPreview, closeDocumentPreview, resetPreviewState, removePreviewEntries, }; }; export default useDocumentPreview;