import {useCallback, useEffect, useRef, useState} from 'react' import {Pressable, StyleSheet, View} from 'react-native' import {Image} from 'expo-image' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {FocusGuards, FocusScope} from 'radix-ui/internal' import {RemoveScrollBar} from 'react-remove-scroll-bar' import {useA11y} from '#/state/a11y' import {useLightbox, useLightboxControls} from '#/state/lightbox' import { atoms as a, flatten, ThemeProvider, useBreakpoints, useTheme, } from '#/alf' import {Button} from '#/components/Button' import {Backdrop} from '#/components/Dialog' import { ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeftIcon, ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon, } from '#/components/icons/Chevron' import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times' import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' import {type ImageSource} from './ImageViewing/@types' export function Lightbox() { const {activeLightbox} = useLightbox() const {closeLightbox} = useLightboxControls() const isActive = !!activeLightbox if (!isActive) { return null } const initialIndex = activeLightbox.index const imgs = activeLightbox.images return ( ) } function LightboxContainer({ children, handleBackgroundPress, }: { children: React.ReactNode handleBackgroundPress: () => void }) { const {_} = useLingui() FocusGuards.useFocusGuards() return (
{children}
) } function LightboxGallery({ imgs, initialIndex = 0, onClose, }: { imgs: ImageSource[] initialIndex: number onClose: () => void }) { const t = useTheme() const {_} = useLingui() const {reduceMotionEnabled} = useA11y() const [index, setIndex] = useState(initialIndex) const [hasAnyLoaded, setAnyHasLoaded] = useState(false) const [isAltExpanded, setAltExpanded] = useState(false) const {gtPhone} = useBreakpoints() const canGoLeft = index >= 1 const canGoRight = index < imgs.length - 1 const onPressLeft = useCallback(() => { if (canGoLeft) { setIndex(index - 1) } }, [index, canGoLeft]) const onPressRight = useCallback(() => { if (canGoRight) { setIndex(index + 1) } }, [index, canGoRight]) const onKeyDown = useCallback( (e: KeyboardEvent) => { if (e.key === 'Escape') { e.preventDefault() onClose() } else if (e.key === 'ArrowLeft') { onPressLeft() } else if (e.key === 'ArrowRight') { onPressRight() } }, [onClose, onPressLeft, onPressRight], ) useEffect(() => { window.addEventListener('keydown', onKeyDown) return () => window.removeEventListener('keydown', onKeyDown) }, [onKeyDown]) // Push a history entry so the browser back button closes the lightbox // instead of navigating away from the page. const closedByPopStateRef = useRef(false) useEffect(() => { history.pushState({lightbox: true}, '') const handlePopState = () => { closedByPopStateRef.current = true onClose() } window.addEventListener('popstate', handlePopState) return () => { window.removeEventListener('popstate', handlePopState) // Only pop our entry if it's still the current one. If navigation // already pushed a new entry on top, leave the orphaned entry — // it shares the same URL so traversing through it is harmless. if ( !closedByPopStateRef.current && (history.state as {lightbox?: boolean})?.lightbox ) { history.back() } } }, [onClose]) const delayedFadeInAnim = !reduceMotionEnabled && [ a.fade_in, {animationDelay: '0.2s', animationFillMode: 'both'}, ] const img = imgs[index] return ( setAnyHasLoaded(true)} /> {canGoLeft && ( )} {canGoRight && ( )} {img.alt ? ( { setAltExpanded(!isAltExpanded) }}> {img.alt} ) : null} {imgs.length > 1 && (
{_(msg`Image ${index + 1} of ${imgs.length}`)}
)}
) } function LightboxGalleryItem({ source, alt, type, onLoad, hasAnyLoaded, }: { source: string alt: string | undefined type: ImageSource['type'] onLoad: () => void hasAnyLoaded: boolean }) { const {reduceMotionEnabled} = useA11y() const [hasLoaded, setHasLoaded] = useState(false) const [isFirstToLoad] = useState(!hasAnyLoaded) /** * We want to show a zoom/fade in animation when the lightbox first opens. * To avoid showing it as we switch between images, we keep track in the parent * whether any image has loaded yet. We then save what the value of this is on first * render (as when it changes, we don't want to then *remove* then animation). when * the image loads, if this is the first image to load, we play the animation. * * We also use this `hasLoaded` state to show a loading indicator. This is on a 1s * delay and then a slow fade in to avoid flicker. -sfn */ const zoomInWhenReady = !reduceMotionEnabled && isFirstToLoad && (hasAnyLoaded ? [a.zoom_fade_in, {animationDuration: '0.5s'}] : {opacity: 0}) const handleLoad = () => { setHasLoaded(true) onLoad() } let image = null switch (type) { case 'circle-avi': case 'rect-avi': image = ( {alt} ) break case 'image': image = ( {alt} ) break } return ( <> {image} {!hasLoaded && ( )} ) } const styles = StyleSheet.create({ avi: { // @ts-ignore web-only maxWidth: `calc(min(400px, 100vw))`, // @ts-ignore web-only maxHeight: `calc(min(400px, 100vh))`, padding: 16, boxSizing: 'border-box', }, closeBtn: { top: 20, right: 20, }, leftBtn: { left: 20, right: 'auto', top: '50%', }, rightBtn: { right: 20, left: 'auto', top: '50%', }, blurredBackdrop: { backgroundColor: '#00000077', // @ts-expect-error web only -sfn backdropFilter: 'blur(10px)', }, blurredBackdropHover: { backgroundColor: '#00000088', }, })