import clsx from 'clsx'; import debounce from 'lodash/debounce'; import React, { useCallback, useEffect, useState } from 'react'; import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; import { useHistory } from 'react-router-dom'; import ReactSwipeableViews from 'react-swipeable-views'; import { fetchNext, fetchStatusWithContext } from 'soapbox/actions/statuses'; import ExtendedVideoPlayer from 'soapbox/components/extended-video-player'; import MissingIndicator from 'soapbox/components/missing-indicator'; import StatusActionBar from 'soapbox/components/status-action-bar'; import { Icon, IconButton, HStack, Stack } from 'soapbox/components/ui'; import Audio from 'soapbox/features/audio'; import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder-status'; import Thread from 'soapbox/features/status/components/thread'; import Video from 'soapbox/features/video'; import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; import { userTouching } from 'soapbox/is-mobile'; import { makeGetStatus } from 'soapbox/selectors'; import ImageLoader from '../image-loader'; import type { List as ImmutableList } from 'immutable'; import type { Attachment, Status } from 'soapbox/types/entities'; const messages = defineMessages({ close: { id: 'lightbox.close', defaultMessage: 'Close' }, expand: { id: 'lightbox.expand', defaultMessage: 'Expand' }, minimize: { id: 'lightbox.minimize', defaultMessage: 'Minimize' }, next: { id: 'lightbox.next', defaultMessage: 'Next' }, previous: { id: 'lightbox.previous', defaultMessage: 'Previous' }, }); // you can't use 100vh, because the viewport height is taller // than the visible part of the document in some mobile // browsers when it's address bar is visible. // https://developers.google.com/web/updates/2016/12/url-bar-resizing const swipeableViewsStyle: React.CSSProperties = { width: '100%', height: '100%', }; const containerStyle: React.CSSProperties = { alignItems: 'center', // center vertically }; interface IMediaModal { media: ImmutableList; status?: Status; index: number; time?: number; onClose(): void; } const MediaModal: React.FC = (props) => { const { media, status, onClose, time = 0, } = props; const dispatch = useAppDispatch(); const history = useHistory(); const intl = useIntl(); const getStatus = useCallback(makeGetStatus(), []); const actualStatus = useAppSelector((state) => getStatus(state, { id: status?.id as string })); const [isLoaded, setIsLoaded] = useState(!!status); const [next, setNext] = useState(); const [index, setIndex] = useState(null); const [navigationHidden, setNavigationHidden] = useState(false); const [isFullScreen, setIsFullScreen] = useState(!status); const hasMultipleImages = media.size > 1; const handleSwipe = (index: number) => setIndex(index % media.size); const handleNextClick = () => setIndex((getIndex() + 1) % media.size); const handlePrevClick = () => setIndex((media.size + getIndex() - 1) % media.size); const navigationHiddenClassName = navigationHidden ? 'pointer-events-none opacity-0' : ''; const handleKeyDown = (e: KeyboardEvent) => { switch (e.key) { case 'ArrowLeft': handlePrevClick(); e.preventDefault(); e.stopPropagation(); break; case 'ArrowRight': handleNextClick(); e.preventDefault(); e.stopPropagation(); break; } }; const handleDownload = () => { const mediaItem = hasMultipleImages ? media.get(index as number) : media.get(0); window.open(mediaItem?.url); }; const getIndex = () => index !== null ? index : props.index; const toggleNavigation = () => { setNavigationHidden(value => !value && userTouching.matches); }; const handleStatusClick: React.MouseEventHandler = e => { if (status && e.button === 0 && !(e.ctrlKey || e.metaKey)) { e.preventDefault(); history.push(`/@${status.account.acct}/posts/${status?.id}`); onClose(); } }; const content = media.map((attachment, i) => { const width = (attachment.meta.getIn(['original', 'width']) || undefined) as number | undefined; const height = (attachment.meta.getIn(['original', 'height']) || undefined) as number | undefined; const link = (status && ( )); if (attachment.type === 'image') { return ( ); } else if (attachment.type === 'video') { return (