104 lines
3.0 KiB
TypeScript
104 lines
3.0 KiB
TypeScript
"use client";
|
||
|
||
/**
|
||
* The media tiles inside a bubble.
|
||
*
|
||
* One item fills the bubble; several tile into a 2×2. Beyond four, the fourth
|
||
* tile carries a "+N more" overlay rather than growing the bubble — a message
|
||
* with twenty photos should still be one glanceable block.
|
||
*
|
||
* Every tile is square so the grid stays even regardless of what was sent;
|
||
* portrait and landscape both crop to the same cell.
|
||
*/
|
||
|
||
import { Play } from "lucide-react";
|
||
import { attachmentUrl, type Attachment } from "@/lib/chat";
|
||
|
||
/** Tiles shown before the rest collapse into the "+N more" overlay. */
|
||
const MAX_TILES = 4;
|
||
|
||
export function MediaGrid({
|
||
items,
|
||
onOpen,
|
||
}: {
|
||
items: Attachment[];
|
||
/** Opens the lightbox at this index. */
|
||
onOpen: (index: number) => void;
|
||
}) {
|
||
if (items.length === 0) return null;
|
||
|
||
const visible = items.slice(0, MAX_TILES);
|
||
const hidden = items.length - visible.length;
|
||
|
||
if (items.length === 1) {
|
||
return (
|
||
<button
|
||
type="button"
|
||
onClick={() => onOpen(0)}
|
||
className="block overflow-hidden rounded-lg"
|
||
>
|
||
<Tile item={items[0]} className="max-h-72 w-auto" />
|
||
</button>
|
||
);
|
||
}
|
||
|
||
return (
|
||
// Two columns throughout: two items make a row, three or four make the
|
||
// 2×2. A three-item grid deliberately leaves the fourth cell empty rather
|
||
// than stretching one tile — an uneven crop reads worse than a gap.
|
||
<div className="grid w-56 grid-cols-2 gap-0.5 overflow-hidden rounded-lg sm:w-64">
|
||
{visible.map((item, i) => {
|
||
const isLast = i === visible.length - 1;
|
||
return (
|
||
<button
|
||
key={item.id}
|
||
type="button"
|
||
onClick={() => onOpen(i)}
|
||
className="relative aspect-square overflow-hidden"
|
||
>
|
||
<Tile item={item} className="size-full object-cover" />
|
||
{isLast && hidden > 0 && (
|
||
<span className="absolute inset-0 flex items-center justify-center bg-black/60 text-sm font-medium text-white">
|
||
+{hidden} more
|
||
</span>
|
||
)}
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function Tile({ item, className }: { item: Attachment; className?: string }) {
|
||
if (item.kind === "video") {
|
||
return (
|
||
<span className="relative block size-full">
|
||
<video
|
||
src={attachmentUrl(item.id)}
|
||
// Just the poster frame: the lightbox is where it plays.
|
||
preload="metadata"
|
||
muted
|
||
playsInline
|
||
className={className}
|
||
/>
|
||
<span className="absolute inset-0 flex items-center justify-center">
|
||
<span className="rounded-full bg-black/50 p-2">
|
||
<Play className="size-4 fill-white text-white" />
|
||
</span>
|
||
</span>
|
||
</span>
|
||
);
|
||
}
|
||
return (
|
||
// eslint-disable-next-line @next/next/no-img-element
|
||
<img
|
||
src={attachmentUrl(item.id)}
|
||
alt={item.file_name ?? "Shared media"}
|
||
loading="lazy"
|
||
width={item.width ?? undefined}
|
||
height={item.height ?? undefined}
|
||
className={className}
|
||
/>
|
||
);
|
||
}
|