import { randomString } from '@/lib/random' import { cn } from '@/lib/utils' import logger from '@/lib/logger' import { useShouldAutoLoadMedia } from '@/hooks/useShouldAutoLoadMedia' import modalManager from '@/services/modal-manager.service' import { TImetaInfo } from '@/types' import { ReactNode, useEffect, useLayoutEffect, useMemo, useState } from 'react' import { createPortal } from 'react-dom' import { lightboxSlideFromImeta } from '@/lib/lightbox-slides' import Lightbox from 'yet-another-react-lightbox' import Captions from 'yet-another-react-lightbox/plugins/captions' import Video from 'yet-another-react-lightbox/plugins/video' import Zoom from 'yet-another-react-lightbox/plugins/zoom' import 'yet-another-react-lightbox/plugins/captions.css' import Image from '../Image' import ImageWithLightbox from '../ImageWithLightbox' const galleryImageWrapper = (className?: string) => cn('w-full max-w-full min-w-0', className) export default function ImageGallery({ className, images, start = 0, end = images.length, mustLoad = false, authorPubkey }: { className?: string images: TImetaInfo[] start?: number end?: number mustLoad?: boolean authorPubkey?: string | null }) { const id = useMemo(() => `image-gallery-${randomString()}`, []) const autoLoadMedia = useShouldAutoLoadMedia(authorPubkey ?? images[start]?.pubkey) const [index, setIndex] = useState(-1) const [lightboxPortalActive, setLightboxPortalActive] = useState(false) useEffect(() => { if (index >= 0) { modalManager.register(id, () => { setIndex(-1) }) } else { modalManager.unregister(id) } }, [id, index]) const handlePhotoClick = (event: React.MouseEvent, current: number) => { event.stopPropagation() event.preventDefault() const newIndex = start + current logger.debug('[ImageGallery] Click:', { start, current, newIndex, totalImages: images.length, displayImages: displayImages.length }) setLightboxPortalActive(true) setIndex(newIndex) } const displayImages = images.slice(start, end) /** Tap-to-load: no shared grid lightbox — each image uses {@link ImageWithLightbox}. */ const tapToLoadGallery = !mustLoad && !autoLoadMedia useLayoutEffect(() => { if (tapToLoadGallery) { setIndex(-1) setLightboxPortalActive(false) } }, [tapToLoadGallery]) if (displayImages.length === 1) { return ( ) } let imageContent: ReactNode | null = null if (tapToLoadGallery) { imageContent = ( <> {displayImages.map((image, i) => ( ))} ) } else if (displayImages.length === 2 || displayImages.length === 4) { imageContent = (
{displayImages.map((image, i) => ( handlePhotoClick(e, i)} /> ))}
) } else { imageContent = (
{displayImages.map((image, i) => ( handlePhotoClick(e, i)} /> ))}
) } const portal = !tapToLoadGallery && lightboxPortalActive && typeof document !== 'undefined' ? createPortal(
e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()} onTouchStart={(e) => e.stopPropagation()} > { const slides = images.map((img) => lightboxSlideFromImeta(img)) logger.debug('[ImageGallery] Lightbox slides:', { index, slidesCount: slides.length, slides }) return slides })()} plugins={[Video, Zoom, Captions]} open={index >= 0} close={() => setIndex(-1)} on={{ exited: () => setLightboxPortalActive(false) }} controller={{ closeOnBackdropClick: false, closeOnPullUp: true, closeOnPullDown: true }} render={{ buttonPrev: images.length <= 1 ? () => null : undefined, buttonNext: images.length <= 1 ? () => null : undefined }} styles={{ toolbar: { paddingTop: '2.25rem' } }} carousel={{ finite: false }} />
, document.body ) : null return (
{imageContent} {portal}
) }