import type { FC } from '../../../lib/teact/teact'; import React, { useEffect, useLayoutEffect, useRef, useState, } from '../../../lib/teact/teact'; import { getActions } from '../../../global'; import type { ApiMessage } from '../../../api/types'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import { ApiMediaFormat } from '../../../api/types'; import { getMessageMediaFormat, getMessageMediaHash, getMessageMediaThumbDataUri, hasMessageTtl, } from '../../../global/helpers'; import { stopCurrentAudio } from '../../../util/audioPlayer'; import buildClassName from '../../../util/buildClassName'; import { formatMediaDuration } from '../../../util/date/dateFormat'; import safePlay from '../../../util/safePlay'; import { ROUND_VIDEO_DIMENSIONS_PX } from '../../common/helpers/mediaDimensions'; import { useThrottledSignal } from '../../../hooks/useAsyncResolvers'; import useFlag from '../../../hooks/useFlag'; import { useIsIntersecting } from '../../../hooks/useIntersectionObserver'; import useLastCallback from '../../../hooks/useLastCallback'; import useMediaTransition from '../../../hooks/useMediaTransition'; import useMediaWithLoadProgress from '../../../hooks/useMediaWithLoadProgress'; import usePrevious from '../../../hooks/usePrevious'; import useShowTransition from '../../../hooks/useShowTransition'; import useSignal from '../../../hooks/useSignal'; import useBlurredMediaThumbRef from './hooks/useBlurredMediaThumbRef'; import Icon from '../../common/Icon'; import MediaSpoiler from '../../common/MediaSpoiler'; import Button from '../../ui/Button'; import OptimizedVideo from '../../ui/OptimizedVideo'; import ProgressSpinner from '../../ui/ProgressSpinner'; import './RoundVideo.scss'; type OwnProps = { message: ApiMessage; className?: string; canAutoLoad?: boolean; isDownloading?: boolean; origin?: 'oneTimeModal'; observeIntersection?: ObserveFn; onStop?: NoneToVoidFunction; onReadMedia?: NoneToVoidFunction; }; const PROGRESS_CENTER = ROUND_VIDEO_DIMENSIONS_PX / 2; const PROGRESS_MARGIN = 6; const PROGRESS_CIRCUMFERENCE = (PROGRESS_CENTER - PROGRESS_MARGIN) * 2 * Math.PI; const PROGRESS_THROTTLE = 16; // Min period needed for `playerEl.currentTime` to update let stopPrevious: NoneToVoidFunction; const RoundVideo: FC = ({ message, className, canAutoLoad, isDownloading, origin, observeIntersection, onStop, onReadMedia, }) => { // eslint-disable-next-line no-null/no-null const ref = useRef(null); // eslint-disable-next-line no-null/no-null const playerRef = useRef(null); // eslint-disable-next-line no-null/no-null const circleRef = useRef(null); const video = message.content.video!; const { cancelMessageMediaDownload, openOneTimeMediaModal } = getActions(); const isIntersecting = useIsIntersecting(ref, observeIntersection); const [isLoadAllowed, setIsLoadAllowed] = useState(canAutoLoad); const shouldLoad = Boolean(isLoadAllowed && isIntersecting); const { mediaData, loadProgress } = useMediaWithLoadProgress( getMessageMediaHash(message, 'inline'), !shouldLoad, getMessageMediaFormat(message, 'inline'), ); const { loadProgress: downloadProgress } = useMediaWithLoadProgress( getMessageMediaHash(message, 'download'), !isDownloading, ApiMediaFormat.BlobUrl, ); const [isPlayerReady, markPlayerReady] = useFlag(); const hasTtl = hasMessageTtl(message); const isInOneTimeModal = origin === 'oneTimeModal'; const shouldRenderSpoiler = hasTtl && !isInOneTimeModal; const hasThumb = Boolean(getMessageMediaThumbDataUri(message)); const noThumb = !hasThumb || isPlayerReady || shouldRenderSpoiler; const thumbRef = useBlurredMediaThumbRef(message, noThumb); const thumbClassNames = useMediaTransition(!noThumb); const thumbDataUri = getMessageMediaThumbDataUri(message); const isTransferring = (isLoadAllowed && !isPlayerReady) || isDownloading; const wasLoadDisabled = usePrevious(isLoadAllowed) === false; const { shouldRender: shouldSpinnerRender, transitionClassNames: spinnerClassNames, } = useShowTransition(isTransferring, undefined, wasLoadDisabled); const [isActivated, setIsActivated] = useState(false); const [getProgress, setProgress] = useSignal(0); const getThrottledProgress = useThrottledSignal(getProgress, PROGRESS_THROTTLE); useEffect(() => { if (!isActivated) { return; } const playerEl = playerRef.current!; setProgress(playerEl.currentTime / playerEl.duration); }, [setProgress, isActivated, getThrottledProgress]); useLayoutEffect(() => { if (!isActivated || !circleRef.current) { return; } const strokeDashOffset = PROGRESS_CIRCUMFERENCE - getThrottledProgress() * PROGRESS_CIRCUMFERENCE; circleRef.current.setAttribute('stroke-dashoffset', strokeDashOffset.toString()); }, [isActivated, getThrottledProgress]); const shouldPlay = Boolean(mediaData && isIntersecting); const stopPlaying = useLastCallback(() => { if (!playerRef.current) { return; } setIsActivated(false); setProgress(0); safePlay(playerRef.current); }); const capturePlaying = useLastCallback(() => { stopPrevious?.(); stopPrevious = stopPlaying; }); const tooglePlaying = useLastCallback(() => { const playerEl = playerRef.current!; if (isActivated) { if (playerEl.paused) { safePlay(playerEl); stopCurrentAudio(); } else { playerEl.pause(); } } else { capturePlaying(); // Pause is a workaround for iOS Safari – otherwise it stops video after several frames playerEl.pause(); playerEl.currentTime = 0; safePlay(playerEl); stopCurrentAudio(); setIsActivated(true); } }); useEffect(() => { if (!isInOneTimeModal) { return; } tooglePlaying(); }, [isInOneTimeModal]); const handleClick = useLastCallback(() => { if (!mediaData) { setIsLoadAllowed((isAllowed) => !isAllowed); return; } if (isDownloading) { cancelMessageMediaDownload({ message }); return; } if (hasTtl && !isInOneTimeModal) { openOneTimeMediaModal({ message }); onReadMedia?.(); return; } tooglePlaying(); }); const handleTimeUpdate = useLastCallback((e: React.UIEvent) => { const playerEl = e.currentTarget; setProgress(playerEl.currentTime / playerEl.duration); }); function renderPlayWrapper() { return (
); } return (
{mediaData && (
{shouldRenderSpoiler && ( )}
)} {!shouldRenderSpoiler && ( )}
{isActivated && ( )}
{shouldSpinnerRender && (
)} {shouldRenderSpoiler && !shouldSpinnerRender && renderPlayWrapper()} {!mediaData && !isLoadAllowed && ( )} {!isInOneTimeModal && (
{isActivated ? formatMediaDuration(playerRef.current!.currentTime) : formatMediaDuration(video.duration)} {(!isActivated || playerRef.current!.paused) && }
)}
); }; export default RoundVideo;