From 9bc780c1f9750dce0318604f021bdcdf07998ae2 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Wed, 26 Apr 2023 21:15:46 +0400 Subject: [PATCH] Round Video: Fix smooth progress indicator --- src/components/middle/message/RoundVideo.tsx | 32 ++++++++++++++------ src/hooks/useAsyncResolvers.ts | 30 ++++++++++++++++-- 2 files changed, 49 insertions(+), 13 deletions(-) diff --git a/src/components/middle/message/RoundVideo.tsx b/src/components/middle/message/RoundVideo.tsx index eafad9dfb..801f02d2b 100644 --- a/src/components/middle/message/RoundVideo.tsx +++ b/src/components/middle/message/RoundVideo.tsx @@ -1,6 +1,7 @@ import type { FC } from '../../../lib/teact/teact'; import React, { useCallback, + useEffect, useLayoutEffect, useRef, useState, @@ -25,6 +26,8 @@ import useMediaTransition from '../../../hooks/useMediaTransition'; import usePrevious from '../../../hooks/usePrevious'; import useFlag from '../../../hooks/useFlag'; import useBlurredMediaThumbRef from './hooks/useBlurredMediaThumbRef'; +import useSignal from '../../../hooks/useSignal'; +import { useThrottledSignal } from '../../../hooks/useAsyncResolvers'; import ProgressSpinner from '../../ui/ProgressSpinner'; import OptimizedVideo from '../../ui/OptimizedVideo'; @@ -39,6 +42,8 @@ type OwnProps = { isDownloading?: boolean; }; +const PROGRESS_THROTTLE = 16; // Min period needed for `playerEl.currentTime` to update + let stopPrevious: NoneToVoidFunction; const RoundVideo: FC = ({ @@ -89,8 +94,19 @@ const RoundVideo: FC = ({ transitionClassNames: spinnerClassNames, } = useShowTransition(isTransferring, undefined, wasLoadDisabled); - const [isActivated, setIsActivated] = useState(false); - const [progress, setProgress] = useState(0); + 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) { @@ -100,9 +116,7 @@ const RoundVideo: FC = ({ const svgCenter = ROUND_VIDEO_DIMENSIONS_PX / 2; const svgMargin = 6; const circumference = (svgCenter - svgMargin) * 2 * Math.PI; - const strokeDashOffset = circumference - progress * circumference; - - const playerEl = playerRef.current!; + const strokeDashOffset = circumference - getThrottledProgress() * circumference; const playingProgressEl = playingProgressRef.current!; const svgEl = playingProgressEl.firstElementChild; @@ -118,9 +132,7 @@ const RoundVideo: FC = ({ } else { (svgEl.firstElementChild as SVGElement).setAttribute('stroke-dashoffset', strokeDashOffset.toString()); } - - setProgress(playerEl.currentTime / playerEl.duration); - }, [isActivated, progress]); + }, [isActivated, getThrottledProgress]); const shouldPlay = Boolean(mediaData && isIntersecting); @@ -136,7 +148,7 @@ const RoundVideo: FC = ({ requestMutation(() => { playingProgressRef.current!.innerHTML = ''; }); - }, []); + }, [setProgress]); const capturePlaying = useCallback(() => { stopPrevious?.(); @@ -179,7 +191,7 @@ const RoundVideo: FC = ({ const playerEl = e.currentTarget; setProgress(playerEl.currentTime / playerEl.duration); - }, []); + }, [setProgress]); return (
(resolver: () => T, deps: any[], ms: number, noFirst = false) { +export function useThrottledResolver( + resolver: () => T, + deps: any[], + msOrSchedulerFn: number | Scheduler, + noFirst = false, +) { return useThrottledCallback((setValue: (newValue: T) => void) => { setValue(resolver()); // eslint-disable-next-line react-hooks-static-deps/exhaustive-deps - }, deps, ms, noFirst); + }, deps, msOrSchedulerFn, noFirst); } -export function useDebouncedResolver(resolver: () => T, deps: any[], ms: number, noFirst = false, noLast = false) { +export function useThrottledSignal( + getValue: Signal, + ms: number, + noFirst = false, +): Signal { + const throttledResolver = useThrottledResolver(() => getValue(), [getValue], ms, noFirst); + + return useDerivedSignal( + throttledResolver, [throttledResolver, getValue], true, + ); +} + +export function useDebouncedResolver( + resolver: () => T, + deps: any[], + ms: number, + noFirst = false, + noLast = false, +) { return useDebouncedCallback((setValue: (newValue: T) => void) => { setValue(resolver()); // eslint-disable-next-line react-hooks-static-deps/exhaustive-deps