attachment-file-view.tsx4.3 KBView on GitHub import { useQuery } from '@tanstack/react-query';
import { Download, FileText, Loader2, Paperclip } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { useTRPC } from '@/providers/query-provider';
import { formatFileSize } from '@/modules/files/utils/format-file-size';
/**
* Viewer for an attachment row whose bytes live behind `files.getDownloadUrl`.
*
* Deliberately keyed off nothing but the document id: the download result already
* carries mimeType and filename, so this renders correctly for a hand-uploaded file
* and for an email attachment that was only promoted to S3 by this very request.
*
* That laziness is the point — for an email attachment the first render is what
* pulls the bytes out of Gmail, so there is no metadata to inspect beforehand.
*/
export function AttachmentFileView({ documentId }: { documentId: string }) {
const trpc = useTRPC();
const downloadQuery = useQuery(
trpc.files.getDownloadUrl.queryOptions(
{ documentId, disposition: 'inline' },
// Presigned URLs live 5 minutes server-side; re-fetch just inside that.
{ staleTime: 4 * 60 * 1000, retry: false },
),
);
if (downloadQuery.isPending) {
return (
<div className="flex flex-1 items-center gap-2 py-5 text-sm text-muted-foreground">
<Loader2 className="size-4 animate-spin" />
Fetching file…
</div>
);
}
if (downloadQuery.isError || !downloadQuery.data) {
// The common failure is a mailbox that was disconnected or re-authed since the
// attachment was indexed, so name that rather than showing a bare error.
return (
<div className="flex flex-1 items-start py-5">
<div className="max-w-md space-y-2 text-sm text-muted-foreground">
<p className="font-medium text-foreground">Couldn't open this file</p>
<p>{downloadQuery.error?.message ?? 'The file could not be retrieved.'}</p>
</div>
</div>
);
}
const { url, mimeType, originalFilename, sizeBytes } = downloadQuery.data;
const mime = (mimeType ?? '').toLowerCase();
const isImage = mime.startsWith('image/');
const isPdf = mime === 'application/pdf';
const header = (
<div className="flex items-center justify-between gap-3 pb-3">
<div className="flex min-w-0 items-center gap-2">
<Paperclip className="size-4 shrink-0 text-muted-foreground" />
<span className="truncate text-sm font-medium text-foreground">{originalFilename}</span>
{sizeBytes > 0 && (
<span className="shrink-0 text-xs text-muted-foreground">{formatFileSize(sizeBytes)}</span>
)}
</div>
<Button size="sm" variant="outline" asChild>
<a href={url} target="_blank" rel="noreferrer" download={originalFilename}>
<Download className="size-3.5" />
Download
</a>
</Button>
</div>
);
if (isImage) {
return (
<div className="flex min-h-0 flex-1 flex-col py-5">
{header}
<div className="flex min-h-0 flex-1 items-center justify-center rounded-lg bg-muted/30 p-4">
<img
src={url}
alt={originalFilename}
className="max-h-full max-w-full rounded-md object-contain shadow-sm"
/>
</div>
</div>
);
}
if (isPdf) {
return (
<div className="flex min-h-0 flex-1 flex-col py-5">
{header}
<iframe src={url} title={originalFilename} className="min-h-[60vh] flex-1 rounded-lg border" />
</div>
);
}
// Everything else (docx, xlsx, zip…) has no browser-native preview worth showing.
return (
<div className="flex flex-1 items-start py-5">
<a
href={url}
target="_blank"
rel="noreferrer"
download={originalFilename}
className="group inline-flex items-center gap-3 rounded-lg border border-border bg-card px-4 py-3.5 text-sm shadow-sm transition-all duration-150 hover:border-border/80 hover:bg-accent/50"
>
<div className="flex size-8 shrink-0 items-center justify-center rounded-md bg-muted transition-colors group-hover:bg-muted/80">
<FileText className="size-4 text-muted-foreground" />
</div>
<span className="font-medium text-foreground">{originalFilename}</span>
{sizeBytes > 0 && <span className="text-xs text-muted-foreground">{formatFileSize(sizeBytes)}</span>}
</a>
</div>
);
}