import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react'; import type { RefCallback, FC } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import classNames from 'classnames'; import type { List as ImmutableList } from 'immutable'; import { animated, useSpring } from '@react-spring/web'; import { useDrag } from '@use-gesture/react'; import type { MediaAttachment } from '@/flavours/glitch/models/status'; import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react'; import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react'; import CloseIcon from '@/material-icons/400-24px/close.svg?react'; import FitScreenIcon from '@/material-icons/400-24px/fit_screen.svg?react'; import ActualSizeIcon from '@/svg-icons/actual_size.svg?react'; import type { RGB } from 'flavours/glitch/blurhash'; import { getAverageFromBlurhash } from 'flavours/glitch/blurhash'; import { GIFV } from 'flavours/glitch/components/gifv'; import { Icon } from 'flavours/glitch/components/icon'; import { IconButton } from 'flavours/glitch/components/icon_button'; import { Footer } from 'flavours/glitch/features/picture_in_picture/components/footer'; import { Video } from 'flavours/glitch/features/video'; import { ZoomableImage } from './zoomable_image'; const messages = defineMessages({ close: { id: 'lightbox.close', defaultMessage: 'Close' }, previous: { id: 'lightbox.previous', defaultMessage: 'Previous' }, next: { id: 'lightbox.next', defaultMessage: 'Next' }, zoomIn: { id: 'lightbox.zoom_in', defaultMessage: 'Zoom to actual size' }, zoomOut: { id: 'lightbox.zoom_out', defaultMessage: 'Zoom to fit' }, }); interface MediaModalProps { media: ImmutableList; statusId?: string; lang?: string; index: number; onClose: () => void; onChangeBackgroundColor: (color: RGB | null) => void; currentTime?: number; autoPlay?: boolean; volume?: number; } export const MediaModal: FC = forwardRef< HTMLDivElement, MediaModalProps >( ( { media, onClose, index: startIndex, lang, currentTime, autoPlay, volume, statusId, onChangeBackgroundColor, }, // eslint-disable-next-line @typescript-eslint/no-unused-vars -- _ref is required to keep the ref forwarding working _ref, ) => { const [index, setIndex] = useState(startIndex); const [zoomedIn, setZoomedIn] = useState(false); const currentMedia = media.get(index); const handleChangeIndex = useCallback( (newIndex: number) => { if (newIndex < 0) { newIndex = media.size + newIndex; } setIndex(newIndex % media.size); setZoomedIn(false); }, [media.size], ); const handlePrevClick = useCallback(() => { handleChangeIndex(index - 1); }, [handleChangeIndex, index]); const handleNextClick = useCallback(() => { handleChangeIndex(index + 1); }, [handleChangeIndex, index]); const handleKeyDown = useCallback( (event: KeyboardEvent) => { if (event.key === 'ArrowLeft') { handlePrevClick(); event.preventDefault(); event.stopPropagation(); } else if (event.key === 'ArrowRight') { handleNextClick(); event.preventDefault(); event.stopPropagation(); } }, [handleNextClick, handlePrevClick], ); useEffect(() => { window.addEventListener('keydown', handleKeyDown, false); return () => { window.removeEventListener('keydown', handleKeyDown); }; }, [handleKeyDown]); useEffect(() => { const blurhash = currentMedia?.get('blurhash') as string | undefined; if (blurhash) { const backgroundColor = getAverageFromBlurhash(blurhash); if (backgroundColor) { onChangeBackgroundColor(backgroundColor); } } return () => { onChangeBackgroundColor(null); }; }, [currentMedia, onChangeBackgroundColor]); const [viewportDimensions, setViewportDimensions] = useState<{ width: number; height: number; }>({ width: 0, height: 0 }); const handleRef: RefCallback = useCallback((ele) => { if (ele?.clientWidth && ele.clientHeight) { setViewportDimensions({ width: ele.clientWidth, height: ele.clientHeight, }); } }, []); const zoomable = currentMedia?.get('type') === 'image' && ((currentMedia.getIn(['meta', 'original', 'width']) as number) > viewportDimensions.width || (currentMedia.getIn(['meta', 'original', 'height']) as number) > viewportDimensions.height); const handleZoomClick = useCallback(() => { setZoomedIn((prev) => !prev); }, []); const wrapperStyles = useSpring({ x: `-${index * 100}%`, }); const bind = useDrag( ({ swipe: [swipeX] }) => { if (swipeX === 0) return; handleChangeIndex(index + swipeX * -1); // Invert swipe as swiping left loads the next slide. }, { pointer: { capture: false } }, ); const [navigationHidden, setNavigationHidden] = useState(false); const handleToggleNavigation = useCallback(() => { setNavigationHidden((prev) => !prev); }, []); const content = useMemo( () => media.map((item, idx) => { const url = item.get('url') as string; const blurhash = item.get('blurhash') as string; const width = item.getIn(['meta', 'original', 'width'], 0) as number; const height = item.getIn( ['meta', 'original', 'height'], 0, ) as number; const description = item.getIn( ['translation', 'description'], item.get('description'), ) as string; if (item.get('type') === 'image') { return ( ); } else if (item.get('type') === 'video') { return (