'use client'; import { useCallback, useEffect, useRef, useState } from 'react'; import { supportApi } from './supportApi'; /** * Blob object URL for an attachment, or `undefined` while it loads / on failure. * * Chat attachments cannot be rendered with a direct ``. The API * guard takes the bearer token from the `Authorization` header and has no cookie * fallback, and an `` request cannot carry that header — a direct src is an * unavoidable 401. So the bytes are fetched through the authenticated client and * handed to the browser as an object URL. * * The URL is revoked on unmount and whenever the attachment changes, so a thread * scrolled through hundreds of images doesn't pin all of them in memory. */ export function useAttachmentObjectUrl(relativeUrl: string): { src?: string; failed: boolean; } { const [src, setSrc] = useState(); const [failed, setFailed] = useState(false); useEffect(() => { let cancelled = false; let created: string | undefined; setSrc(undefined); setFailed(false); supportApi .fetchAttachment(relativeUrl) .then((blob) => { // The component may have unmounted mid-flight; creating a URL then would // leak it, since the cleanup below has already run. if (cancelled) return; created = URL.createObjectURL(blob); setSrc(created); }) .catch(() => { if (!cancelled) setFailed(true); }); return () => { cancelled = true; if (created) URL.revokeObjectURL(created); }; }, [relativeUrl]); return { src, failed }; } /** * On-demand variant for files that aren't previewed inline (documents): fetch * only when the user actually opens one, rather than pulling every attachment in * the thread down just to render a filename row. * * Holds a single slot — opening another file revokes the previous URL, as does * unmounting. */ export function useLazyAttachmentObjectUrl(): (relativeUrl: string) => Promise { const current = useRef(); useEffect( () => () => { if (current.current) URL.revokeObjectURL(current.current); }, [], ); return useCallback(async (relativeUrl: string) => { const blob = await supportApi.fetchAttachment(relativeUrl); if (current.current) URL.revokeObjectURL(current.current); current.current = URL.createObjectURL(blob); return current.current; }, []); }