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&apos;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>
  );
}