Files
edr-platform/apps/edr-passenger-web/backoffice/src/features/support/useAttachmentObjectUrl.ts

80 lines
2.4 KiB
TypeScript

'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 `<img src={a.url}>`. The API
* guard takes the bearer token from the `Authorization` header and has no cookie
* fallback, and an `<img>` 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<string>();
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<string> {
const current = useRef<string>();
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;
}, []);
}