"use client"; /** * Attachments under a message bubble. * * Media (images and videos) tile into a grid and open in a lightbox; links and * files keep their own row treatments, since a link card and a photo are read * very differently. * * Media is fetched through the BFF's stable /chat/attachments/{id} URL, which * re-checks membership and redirects to a short-lived signed URL. The object * itself lives in a private bucket and is never public. */ import { useState } from "react"; import { FileText } from "lucide-react"; import { LinkPreviewCard } from "@/components/chat/link-preview-card"; import { MediaGrid } from "@/components/chat/media-grid"; import { MediaLightbox } from "@/components/chat/media-lightbox"; import { attachmentUrl, type Attachment } from "@/lib/chat"; function humanSize(bytes?: number | null): string { if (!bytes) return ""; const units = ["B", "KB", "MB", "GB"]; let n = bytes; let i = 0; while (n >= 1024 && i < units.length - 1) { n /= 1024; i += 1; } return `${n < 10 && i > 0 ? n.toFixed(1) : Math.round(n)} ${units[i]}`; } export function MessageAttachments({ attachments, }: { attachments: Attachment[]; }) { const [lightboxAt, setLightboxAt] = useState(null); if (!attachments || attachments.length === 0) return null; // Only images and videos tile; everything else keeps its own row. const media = attachments.filter( (a) => a.kind === "image" || a.kind === "video", ); const rest = attachments.filter( (a) => a.kind !== "image" && a.kind !== "video", ); return ( <>
{media.length > 0 && ( setLightboxAt(i)} /> )} {rest.map((a) => { switch (a.kind) { case "link": return ( ); case "audio": return (
{lightboxAt !== null && ( setLightboxAt(null)} /> )} ); }