diff --git a/src/components/common/AnimatedSticker.tsx b/src/components/common/AnimatedSticker.tsx index c40becdc5..d9b23ba3e 100644 --- a/src/components/common/AnimatedSticker.tsx +++ b/src/components/common/AnimatedSticker.tsx @@ -10,8 +10,9 @@ import buildClassName from '../../util/buildClassName'; import buildStyle from '../../util/buildStyle'; import generateIdFor from '../../util/generateIdFor'; -import useHeavyAnimationCheck from '../../hooks/useHeavyAnimationCheck'; -import useBackgroundMode from '../../hooks/useBackgroundMode'; +import useHeavyAnimationCheck, { isHeavyAnimating } from '../../hooks/useHeavyAnimationCheck'; +import usePriorityPlaybackCheck, { isPriorityPlaybackActive } from '../../hooks/usePriorityPlaybackCheck'; +import useBackgroundMode, { isBackgroundModeActive } from '../../hooks/useBackgroundMode'; import useSyncEffect from '../../hooks/useSyncEffect'; import { useStateRef } from '../../hooks/useStateRef'; @@ -91,8 +92,6 @@ const AnimatedSticker: FC = ({ const [animation, setAnimation] = useState(); const animationRef = useRef(); - const wasPlaying = useRef(false); - const isFrozen = useRef(false); const isFirstRender = useRef(true); const canPlay = play || playSegment; @@ -182,50 +181,31 @@ const AnimatedSticker: FC = ({ }, [viewId]); const playAnimation = useCallback((shouldRestart = false) => { - if (animation && (playRef.current || playSegmentRef.current)) { - if (playSegmentRef.current) { - animation.playSegment(playSegmentRef.current); - } else { - animation.play(shouldRestart, viewId); - } + if ( + !animation + || !(playRef.current || playSegmentRef.current) + || isFrozen() + ) { + return; + } + + if (playSegmentRef.current) { + animation.playSegment(playSegmentRef.current); + } else { + animation.play(shouldRestart, viewId); } }, [animation, playRef, playSegmentRef, viewId]); + const playAnimationOnRaf = useCallback(() => { + fastRaf(playAnimation); + }, [playAnimation]); + const pauseAnimation = useCallback(() => { - if (!animation) { - return; + if (animation?.isPlaying()) { + animation.pause(viewId); } - - animation.pause(viewId); }, [animation, viewId]); - const freezeAnimation = useCallback(() => { - isFrozen.current = true; - - if (!animation) { - return; - } - - if (!wasPlaying.current) { - wasPlaying.current = animation.isPlaying(); - } - - pauseAnimation(); - }, [animation, pauseAnimation]); - - const unfreezeAnimation = useCallback(() => { - if (wasPlaying.current) { - playAnimation(noLoop); - } - - wasPlaying.current = false; - isFrozen.current = false; - }, [noLoop, playAnimation]); - - const unfreezeAnimationOnRaf = useCallback(() => { - fastRaf(unfreezeAnimation); - }, [unfreezeAnimation]); - useSyncEffect(([prevNoLoop]) => { if (prevNoLoop !== undefined && noLoop !== prevNoLoop) { animation?.setNoLoop(noLoop); @@ -242,19 +222,13 @@ const AnimatedSticker: FC = ({ if (!animation) { return; } + if (canPlay) { - if (isFrozen.current) { - wasPlaying.current = true; - } else { + if (!isFrozen()) { playAnimation(noLoop); } } else { - // eslint-disable-next-line no-lonely-if - if (isFrozen.current) { - wasPlaying.current = false; - } else { - pauseAnimation(); - } + pauseAnimation(); } }, [animation, canPlay, noLoop, playAnimation, pauseAnimation]); @@ -269,11 +243,12 @@ const AnimatedSticker: FC = ({ } }, [playAnimation, animation, tgsUrl]); - useHeavyAnimationCheck(freezeAnimation, unfreezeAnimation, !canPlay || forceOnHeavyAnimation); + useHeavyAnimationCheck(pauseAnimation, playAnimation, !canPlay || forceOnHeavyAnimation); + usePriorityPlaybackCheck(pauseAnimation, playAnimation, !canPlay); // Pausing frame may not happen in background, // so we need to make sure it happens right after focusing, // then we can play again. - useBackgroundMode(freezeAnimation, unfreezeAnimationOnRaf, !canPlay); + useBackgroundMode(pauseAnimation, playAnimationOnRaf, !canPlay); if (sharedCanvas) { return undefined; @@ -294,3 +269,7 @@ const AnimatedSticker: FC = ({ }; export default memo(AnimatedSticker); + +function isFrozen() { + return isHeavyAnimating() || isPriorityPlaybackActive() || isBackgroundModeActive(); +} diff --git a/src/components/mediaViewer/MediaViewer.tsx b/src/components/mediaViewer/MediaViewer.tsx index b8c021a8f..81353909d 100644 --- a/src/components/mediaViewer/MediaViewer.tsx +++ b/src/components/mediaViewer/MediaViewer.tsx @@ -32,6 +32,7 @@ import { renderMessageText } from '../common/helpers/renderMessageText'; import useFlag from '../../hooks/useFlag'; import useForceUpdate from '../../hooks/useForceUpdate'; import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck'; +import { dispatchPriorityPlaybackEvent } from '../../hooks/usePriorityPlaybackCheck'; import { exitPictureInPictureIfNeeded } from '../../hooks/usePictureInPicture'; import useLang from '../../hooks/useLang'; import usePrevious from '../../hooks/usePrevious'; @@ -149,8 +150,12 @@ const MediaViewer: FC = ({ } disableDirectTextInput(); + const stopPriorityPlayback = dispatchPriorityPlaybackEvent(); - return enableDirectTextInput; + return () => { + stopPriorityPlayback(); + enableDirectTextInput(); + }; }, [isOpen]); useEffect(() => { diff --git a/src/components/middle/message/_message-content.scss b/src/components/middle/message/_message-content.scss index ee4390185..2c748ccf1 100644 --- a/src/components/middle/message/_message-content.scss +++ b/src/components/middle/message/_message-content.scss @@ -770,6 +770,10 @@ vertical-align: bottom; } + .MessageMeta { + z-index: 1; + } + .text-content { word-break: normal; line-height: var(--emoji-only-size); diff --git a/src/components/middle/message/hooks/useVideoAutoPause.ts b/src/components/middle/message/hooks/useVideoAutoPause.ts index a673d62aa..19aef38d4 100644 --- a/src/components/middle/message/hooks/useVideoAutoPause.ts +++ b/src/components/middle/message/hooks/useVideoAutoPause.ts @@ -1,9 +1,9 @@ import { useCallback, useEffect, useRef } from '../../../../lib/teact/teact'; import { fastRaf } from '../../../../util/schedulers'; -import useBackgroundMode from '../../../../hooks/useBackgroundMode'; -import useHeavyAnimationCheck from '../../../../hooks/useHeavyAnimationCheck'; -import usePlayPause from '../../../../hooks/usePlayPause'; +import useBackgroundMode, { isBackgroundModeActive } from '../../../../hooks/useBackgroundMode'; +import useHeavyAnimationCheck, { isHeavyAnimating } from '../../../../hooks/useHeavyAnimationCheck'; +import usePriorityPlaybackCheck, { isPriorityPlaybackActive } from '../../../../hooks/usePriorityPlaybackCheck'; export default function useVideoAutoPause(playerRef: { current: HTMLVideoElement | null }, canPlay: boolean) { const canPlayRef = useRef(); @@ -11,18 +11,8 @@ export default function useVideoAutoPause(playerRef: { current: HTMLVideoElement const { play, pause } = usePlayPause(playerRef); - const isFrozenRef = useRef(); - - const freezePlaying = useCallback(() => { - isFrozenRef.current = true; - - pause(); - }, [pause]); - const unfreezePlaying = useCallback(() => { - isFrozenRef.current = false; - - if (canPlayRef.current) { + if (canPlayRef.current && !isFrozen()) { play(); } }, [play]); @@ -31,18 +21,19 @@ export default function useVideoAutoPause(playerRef: { current: HTMLVideoElement fastRaf(unfreezePlaying); }, [unfreezePlaying]); - useBackgroundMode(freezePlaying, unfreezePlayingOnRaf, !canPlay); - useHeavyAnimationCheck(freezePlaying, unfreezePlaying, !canPlay); + useBackgroundMode(pause, unfreezePlayingOnRaf, !canPlay); + useHeavyAnimationCheck(pause, unfreezePlaying, !canPlay); + usePriorityPlaybackCheck(pause, unfreezePlaying, !canPlay); const handlePlaying = useCallback(() => { - if (!canPlayRef.current || isFrozenRef.current) { + if (!canPlayRef.current || isFrozen()) { pause(); } }, [pause]); useEffect(() => { if (canPlay) { - if (!isFrozenRef.current) { + if (!isFrozen()) { play(); } } else { @@ -52,3 +43,39 @@ export default function useVideoAutoPause(playerRef: { current: HTMLVideoElement return { handlePlaying }; } + +function usePlayPause(mediaRef: React.RefObject) { + const shouldPauseRef = useRef(false); + const isLoadingPlayRef = useRef(false); + + const play = useCallback(() => { + shouldPauseRef.current = false; + if (mediaRef.current && !isLoadingPlayRef.current && document.body.contains(mediaRef.current)) { + isLoadingPlayRef.current = true; + mediaRef.current.play().then(() => { + isLoadingPlayRef.current = false; + if (shouldPauseRef.current) { + mediaRef.current?.pause(); + shouldPauseRef.current = false; + } + }).catch((e) => { + // eslint-disable-next-line no-console + console.warn(e); + }); + } + }, [mediaRef]); + + const pause = useCallback(() => { + if (isLoadingPlayRef.current) { + shouldPauseRef.current = true; + } else { + mediaRef.current?.pause(); + } + }, [mediaRef]); + + return { play, pause }; +} + +function isFrozen() { + return isHeavyAnimating() || isPriorityPlaybackActive() || isBackgroundModeActive(); +} diff --git a/src/hooks/useBackgroundMode.ts b/src/hooks/useBackgroundMode.ts index 5d2a57a01..2c985cf83 100644 --- a/src/hooks/useBackgroundMode.ts +++ b/src/hooks/useBackgroundMode.ts @@ -1,49 +1,60 @@ -import { useCallback, useEffect, useRef } from '../lib/teact/teact'; +import { useEffect } from '../lib/teact/teact'; + +import { createCallbackManager } from '../util/callbacks'; + +const blurCallbacks = createCallbackManager(); +const focusCallbacks = createCallbackManager(); + +let isFocused = document.hasFocus(); + +window.addEventListener('blur', () => { + if (!isFocused) { + return; + } + + isFocused = false; + blurCallbacks.runCallbacks(); +}); + +window.addEventListener('focus', () => { + isFocused = true; + focusCallbacks.runCallbacks(); +}); export default function useBackgroundMode( onBlur?: AnyToVoidFunction, onFocus?: AnyToVoidFunction, isDisabled = false, ) { - const wasBlurred = useRef(false); - const handleBlur = useCallback(() => { - if (wasBlurred.current) { - return; - } - - onBlur?.(); - wasBlurred.current = true; - }, [onBlur]); - const handleFocus = useCallback(() => { - onFocus?.(); - wasBlurred.current = false; - }, [onFocus]); - useEffect(() => { if (isDisabled) { return undefined; } - if (onBlur && !document.hasFocus()) { - handleBlur(); + if (!isFocused) { + onBlur?.(); } if (onBlur) { - window.addEventListener('blur', handleBlur); + blurCallbacks.addCallback(onBlur); } if (onFocus) { - window.addEventListener('focus', handleFocus); + focusCallbacks.addCallback(onFocus); } return () => { if (onFocus) { - window.removeEventListener('focus', handleFocus); + focusCallbacks.removeCallback(onFocus); } if (onBlur) { - window.removeEventListener('blur', handleBlur); + blurCallbacks.removeCallback(onBlur); } }; - }, [handleBlur, handleFocus, isDisabled, onBlur, onFocus]); + }, [isDisabled, onBlur, onFocus]); +} + +export function isBackgroundModeActive() { + return !isFocused; } diff --git a/src/hooks/usePlayPause.ts b/src/hooks/usePlayPause.ts deleted file mode 100644 index dd4f6a97a..000000000 --- a/src/hooks/usePlayPause.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { useCallback, useRef } from '../lib/teact/teact'; - -export default function usePlayPause(mediaRef: React.RefObject) { - const shouldPauseRef = useRef(false); - const isLoadingPlayRef = useRef(false); - - const play = useCallback(() => { - shouldPauseRef.current = false; - if (mediaRef.current && !isLoadingPlayRef.current && document.body.contains(mediaRef.current)) { - isLoadingPlayRef.current = true; - mediaRef.current.play().then(() => { - isLoadingPlayRef.current = false; - if (shouldPauseRef.current) { - mediaRef.current?.pause(); - shouldPauseRef.current = false; - } - }).catch((e) => { - // eslint-disable-next-line no-console - console.warn(e); - }); - } - }, [mediaRef]); - - const pause = useCallback(() => { - if (isLoadingPlayRef.current) { - shouldPauseRef.current = true; - } else { - mediaRef.current?.pause(); - } - }, [mediaRef]); - - return { play, pause }; -} diff --git a/src/hooks/usePriorityPlaybackCheck.ts b/src/hooks/usePriorityPlaybackCheck.ts new file mode 100644 index 000000000..f87a2dce3 --- /dev/null +++ b/src/hooks/usePriorityPlaybackCheck.ts @@ -0,0 +1,63 @@ +import { useEffect } from '../lib/teact/teact'; +import { createCallbackManager } from '../util/callbacks'; + +const startCallbacks = createCallbackManager(); +const endCallbacks = createCallbackManager(); + +let timeout: number | undefined; +let isActive = false; + +const usePriorityPlaybackCheck = ( + handleAnimationStart: AnyToVoidFunction, + handleAnimationEnd: AnyToVoidFunction, + isDisabled = false, +) => { + useEffect(() => { + if (isDisabled) { + return undefined; + } + + if (isActive) { + handleAnimationStart(); + } + + startCallbacks.addCallback(handleAnimationStart); + endCallbacks.addCallback(handleAnimationEnd); + + return () => { + endCallbacks.removeCallback(handleAnimationEnd); + startCallbacks.removeCallback(handleAnimationStart); + }; + }, [isDisabled, handleAnimationEnd, handleAnimationStart]); +}; + +export function isPriorityPlaybackActive() { + return isActive; +} + +export function dispatchPriorityPlaybackEvent() { + if (!isActive) { + isActive = true; + startCallbacks.runCallbacks(); + } + + if (timeout) { + clearTimeout(timeout); + timeout = undefined; + } + + // Race condition may happen if another `dispatchPriorityPlaybackEvent` is called before `onEnd` + function onEnd() { + if (timeout) { + clearTimeout(timeout); + timeout = undefined; + } + + isActive = false; + endCallbacks.runCallbacks(); + } + + return onEnd; +} + +export default usePriorityPlaybackCheck; diff --git a/src/lib/rlottie/RLottie.ts b/src/lib/rlottie/RLottie.ts index f4443a27a..1eb2b3e0c 100644 --- a/src/lib/rlottie/RLottie.ts +++ b/src/lib/rlottie/RLottie.ts @@ -181,6 +181,8 @@ class RLottie { } if (!this.params.isLowPriority) { + this.isWaiting = false; + this.frames = this.frames.map((frame, i) => { if (i === this.prevFrameIndex) { return frame;