'use client'; /** * Chat image album — a Telegram/WhatsApp-style adaptive collage for 1–N * images. NOT a true masonry: messengers use a small set of deterministic * album layouts (no JS height measuring, no extra deps), which is the right * call for a chat where albums are a small, bounded case. * * 1 → single image 2 → two equal columns * 3 → one tall + two stacked 4 → 2×2 grid * 5+ → 2×2 grid, the last visible tile shows a "+N more" overlay * * Clicking any tile opens the shared zoom/pan `ImageViewer` lightbox in a * dialog, seeded at the clicked index, with prev/next across the whole set. */ import { useCallback, useState } from 'react'; import { Dialog, DialogContent, DialogTitle } from '@djangocfg/ui-core/components'; import { cn } from '@djangocfg/ui-core/lib'; import { LazyImageViewer } from '../../../../media/ImageViewer/lazy'; export interface ChatAlbumImage { id: string; src: string; thumbnail?: string; alt?: string; } export interface ChatImageAlbumProps { images: ChatAlbumImage[]; /** Max tiles shown before collapsing the rest into a "+N" overlay. */ maxVisible?: number; } /** Tile — a single clickable image cell. */ function Tile({ image, onClick, className, overlayCount, }: { image: ChatAlbumImage; onClick: () => void; className?: string; overlayCount?: number; }) { return ( ); } export function ChatImageAlbum({ images, maxVisible = 4 }: ChatImageAlbumProps) { const [open, setOpen] = useState(false); const [index, setIndex] = useState(0); const openAt = useCallback((i: number) => { setIndex(i); setOpen(true); }, []); const count = images.length; const visible = images.slice(0, Math.min(count, maxVisible)); const hiddenCount = count - visible.length; // Layout per count. `gap-0.5` mimics the tight messenger seam; the outer // BLOCK_SURFACE provides the rounded clip + border. let grid: React.ReactNode; if (count === 1) { const img = images[0]; grid = ( openAt(0)} className="max-h-96 w-full" /> ); } else if (count === 2) { grid = (
{visible.map((img, i) => ( openAt(i)} /> ))}
); } else if (count === 3) { grid = (
openAt(0)} className="row-span-2" /> openAt(1)} /> openAt(2)} />
); } else { // 4+ → 2×2, last visible tile carries the "+N" overlay when collapsed. grid = (
{visible.map((img, i) => { const isLast = i === visible.length - 1; return ( openAt(i)} overlayCount={isLast && hiddenCount > 0 ? hiddenCount : undefined} /> ); })}
); } return ( <> {grid} Image {index + 1} of {count} {open && ( ({ file: { name: img.alt ?? 'image', path: img.src }, src: img.src, }))} initialIndex={index} inDialog autoFocus /> )} ); }