import { randomString } from '@/lib/random' import { cn } from '@/lib/utils' import logger from '@/lib/logger' import { useContentPolicy } from '@/providers/ContentPolicyProvider' import modalManager from '@/services/modal-manager.service' import { TImetaInfo } from '@/types' import { useCallback, useEffect, useMemo, useState } from 'react' import { createPortal } from 'react-dom' import { preferBlossomPrimalDisplayUrl } from '@/lib/url' import { useTranslation } from 'react-i18next' import Lightbox from 'yet-another-react-lightbox' import Captions from 'yet-another-react-lightbox/plugins/captions' import Zoom from 'yet-another-react-lightbox/plugins/zoom' import 'yet-another-react-lightbox/plugins/captions.css' import Image from '../Image' export default function ImageWithLightbox({ image, className, classNames = {} }: { image: TImetaInfo className?: string classNames?: { wrapper?: string } }) { const id = useMemo(() => `image-with-lightbox-${randomString()}`, []) const { t } = useTranslation() const { autoLoadMedia } = useContentPolicy() const [display, setDisplay] = useState(autoLoadMedia) const [index, setIndex] = useState(-1) useEffect(() => { setDisplay(autoLoadMedia) if (!autoLoadMedia) { setIndex(-1) } }, [autoLoadMedia]) const logLightboxEvent = useCallback((stage: string, details?: Record) => { logger.info('[LightboxTrace]', { stage, id, imageUrl: image.url, index, pathname: window.location.pathname, search: window.location.search, hash: window.location.hash, ...details }) }, [id, image.url, index]) useEffect(() => { if (index >= 0) { logLightboxEvent('modal-register') modalManager.register(id, () => { logLightboxEvent('modal-callback-close') setIndex(-1) }) } else { logLightboxEvent('modal-unregister') modalManager.unregister(id) } }, [id, index, logLightboxEvent]) useEffect(() => { if (index < 0) return const onCaptureKeydown = (event: KeyboardEvent) => { if (event.key === 'Escape') { logLightboxEvent('escape-keydown-capture', { defaultPrevented: event.defaultPrevented, eventPhase: event.eventPhase }) } } const onPopState = (event: PopStateEvent) => { logLightboxEvent('window-popstate-while-open', { hasState: !!event.state, state: event.state }) } window.addEventListener('keydown', onCaptureKeydown, true) window.addEventListener('popstate', onPopState) return () => { window.removeEventListener('keydown', onCaptureKeydown, true) window.removeEventListener('popstate', onPopState) } }, [index, logLightboxEvent]) const handlePhotoClick = (event: React.MouseEvent) => { logLightboxEvent('thumbnail-click', { defaultPreventedBefore: event.defaultPrevented }) event.stopPropagation() event.preventDefault() logLightboxEvent('set-open-index') setIndex(0) } // The portal is always mounted (not conditional on `index >= 0`) so that React // never removes it while yet-another-react-lightbox is mid-cleanup, which would // otherwise cause "Node.removeChild: The node to be removed is not a child of // this node". Visibility is controlled via the `open` prop instead. return (
{display ? ( handlePhotoClick(e)} /> ) : ( { e.stopPropagation() setDisplay(true) }} > [{t('Click to load image')}] )} {createPortal(
{ logLightboxEvent('overlay-click', { target: (e.target as HTMLElement)?.tagName }) e.stopPropagation() }} onPointerDown={(e) => { logLightboxEvent('overlay-pointerdown', { target: (e.target as HTMLElement)?.tagName }) e.stopPropagation() }} onMouseDown={(e) => { logLightboxEvent('overlay-mousedown', { target: (e.target as HTMLElement)?.tagName }) e.stopPropagation() }} onTouchStart={(e) => { logLightboxEvent('overlay-touchstart', { target: (e.target as HTMLElement)?.tagName }) e.stopPropagation() }} > = 0} close={() => { logLightboxEvent('lightbox-close-callback') setIndex(-1) }} controller={{ closeOnBackdropClick: false, closeOnPullUp: true, closeOnPullDown: true }} render={{ buttonPrev: () => null, buttonNext: () => null }} styles={{ toolbar: { paddingTop: '2.25rem' } }} />
, document.body )}
) }