import { useState, useCallback, useEffect } from 'react'; import { createPortal } from 'react-dom'; import { ChevronLeftIcon, ChevronRightIcon, DocumentIcon, XIcon } from '@/components/icons'; import { ticketsApi } from '../../api/tickets'; export interface MediaItem { type: string; file_id: string; caption?: string | null; } interface MessageLike { has_media?: boolean; media_type?: string | null; media_file_id?: string | null; media_caption?: string | null; media_items?: MediaItem[] | null; } /** * Normalize message media into a unified list. * If media_items is present, use it. Otherwise fall back to legacy single-media fields. */ function getItems(message: MessageLike): MediaItem[] { if (message.media_items && message.media_items.length > 0) { return message.media_items; } if (message.media_file_id && message.media_type) { return [ { type: message.media_type, file_id: message.media_file_id, caption: message.media_caption, }, ]; } return []; } export function MessageMediaGrid({ message, translateError = 'Failed to load image', }: { message: MessageLike; translateError?: string; }) { const items = getItems(message); const photoItems = items.filter((i) => i.type === 'photo'); const otherItems = items.filter((i) => i.type !== 'photo'); const [fullscreenIndex, setFullscreenIndex] = useState(null); const openFullscreen = useCallback((idx: number) => setFullscreenIndex(idx), []); const closeFullscreen = useCallback(() => setFullscreenIndex(null), []); // Escape + arrow keys for fullscreen nav useEffect(() => { if (fullscreenIndex === null) return; const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') setFullscreenIndex(null); else if (e.key === 'ArrowLeft' && fullscreenIndex > 0) setFullscreenIndex(fullscreenIndex - 1); else if (e.key === 'ArrowRight' && fullscreenIndex < photoItems.length - 1) setFullscreenIndex(fullscreenIndex + 1); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [fullscreenIndex, photoItems.length]); // Lock body scroll while fullscreen overlay is open (mobile mainly). useEffect(() => { if (fullscreenIndex === null) return undefined; const prev = document.body.style.overflow; document.body.style.overflow = 'hidden'; return () => { document.body.style.overflow = prev; }; }, [fullscreenIndex]); // All hooks have been called — safe to early-return now. if (items.length === 0) return null; // Grid layout based on photo count let gridClass = ''; if (photoItems.length === 1) { gridClass = 'grid-cols-1'; } else if (photoItems.length === 2) { gridClass = 'grid-cols-2'; } else if (photoItems.length === 3) { gridClass = 'grid-cols-3'; } else { gridClass = 'grid-cols-2'; // 4+ → 2x2 } const visiblePhotos = photoItems.slice(0, 4); const hiddenCount = photoItems.length - visiblePhotos.length; return (
{photoItems.length > 0 && (
{visiblePhotos.map((item, visIdx) => { // visIdx is always the correct index into photoItems (visible prefix) const originalIdx = visIdx; const isLastVisible = visIdx === visiblePhotos.length - 1 && hiddenCount > 0; return ( ); })}
)} {/* Non-photo media rendered inline */} {otherItems.map((item) => { const mediaUrl = ticketsApi.getMediaUrl(item.file_id); if (item.type === 'video') { return (
); } return ( {item.caption || `Download ${item.type}`} ); })} {fullscreenIndex !== null && photoItems[fullscreenIndex] && createPortal(
{photoItems.length > 1 && ( <>
{fullscreenIndex + 1} / {photoItems.length}
)}
{photoItems[fullscreenIndex].caption e.stopPropagation()} />
, document.body, )} {/* Fallback: error state */} {photoItems.length === 0 && otherItems.length === 0 && (
{translateError}
)}
); }