Emoji Background: Fix resize after heavy animation (#4656)

This commit is contained in:
zubiden 2024-06-12 18:11:16 +02:00 committed by Alexander Zinchuk
parent 61339c3bf8
commit 1997008ba5
3 changed files with 62 additions and 40 deletions

View File

@ -44,7 +44,7 @@
"react-hooks-static-deps/exhaustive-deps": [ "react-hooks-static-deps/exhaustive-deps": [
"error", "error",
{ {
"additionalHooks": "(useSyncEffect|useAsync|useDebouncedCallback|useThrottledCallback|useEffectWithPrevDeps|useLayoutEffectWithPrevDeps|useDerivedState|useDerivedSignal|useThrottledResolver|useDebouncedResolver)$", "additionalHooks": "(useSyncEffect|useAsync|useDebouncedCallback|useThrottledCallback|useEffectWithPrevDeps|useLayoutEffectWithPrevDeps|useDerivedState|useDerivedSignal|useThrottledResolver|useDebouncedResolver|useThrottleForHeavyAnimation)$",
"staticHooks": { "staticHooks": {
"getActions": true, "getActions": true,
"useFlag": [false, true, true], "useFlag": [false, true, true],

View File

@ -9,9 +9,7 @@ import { preloadImage } from '../../../util/files';
import { REM } from '../helpers/mediaDimensions'; import { REM } from '../helpers/mediaDimensions';
import useDynamicColorListener from '../../../hooks/stickers/useDynamicColorListener'; import useDynamicColorListener from '../../../hooks/stickers/useDynamicColorListener';
import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps'; import { useThrottleForHeavyAnimation } from '../../../hooks/useHeavyAnimationCheck';
import useFlag from '../../../hooks/useFlag';
import useHeavyAnimationCheck, { isHeavyAnimating } from '../../../hooks/useHeavyAnimationCheck';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback'; import useLastCallback from '../../../hooks/useLastCallback';
import useMedia from '../../../hooks/useMedia'; import useMedia from '../../../hooks/useMedia';
@ -79,21 +77,20 @@ const EmojiIconBackground = ({
const lang = useLang(); const lang = useLang();
// Delay mounting until heavy animation ends
const [canUpdate, markCanUpdate, unmarkCanUpdate] = useFlag(!isHeavyAnimating());
useHeavyAnimationCheck(unmarkCanUpdate, markCanUpdate);
const { customEmoji } = useCustomEmoji(emojiDocumentId); const { customEmoji } = useCustomEmoji(emojiDocumentId);
const previewMediaHash = customEmoji ? getStickerPreviewHash(customEmoji.id) : undefined; const previewMediaHash = customEmoji ? getStickerPreviewHash(customEmoji.id) : undefined;
const previewUrl = useMedia(previewMediaHash); const previewUrl = useMedia(previewMediaHash);
const customColor = useDynamicColorListener(containerRef); const customColor = useDynamicColorListener(containerRef);
useEffect(() => { const preloadAfterHeavyAnimation = useThrottleForHeavyAnimation(() => {
if (!previewUrl || !canUpdate) return; if (!previewUrl) return;
preloadImage(previewUrl).then(setEmojiImage); preloadImage(previewUrl).then(setEmojiImage);
}, [previewUrl, canUpdate]); }, [previewUrl]);
useEffect(() => {
preloadAfterHeavyAnimation();
}, [preloadAfterHeavyAnimation]);
const updateCanvas = useLastCallback(() => { const updateCanvas = useLastCallback(() => {
const canvas = canvasRef.current; const canvas = canvasRef.current;
@ -129,46 +126,43 @@ const EmojiIconBackground = ({
context.restore(); context.restore();
}); });
useEffectWithPrevDeps(([prevEmojiImage, prevLangRtl, prevCustomColor]) => { const updateCanvasAfterHeavyAnimation = useThrottleForHeavyAnimation(updateCanvas, [updateCanvas]);
// No need to trigger update if only `canUpdate` changed
if (emojiImage === prevEmojiImage && lang.isRtl === prevLangRtl && customColor === prevCustomColor) return;
updateCanvas();
}, [emojiImage, lang.isRtl, customColor, canUpdate]);
const updateCanvasSize = useLastCallback((parentWidth: number, parentHeight: number) => { useEffect(() => {
const canvas = canvasRef.current; updateCanvasAfterHeavyAnimation();
if (!canvas || isHeavyAnimating()) return; }, [emojiImage, lang.isRtl, customColor, updateCanvasAfterHeavyAnimation]);
canvas.width = parentWidth * dpr; const updateCanvasSize = useThrottleForHeavyAnimation((parentWidth: number, parentHeight: number) => {
canvas.height = parentHeight * dpr; requestMutation(() => {
const canvas = canvasRef.current;
if (!canvas) return;
canvas.style.width = `${parentWidth}px`; canvas.width = parentWidth * dpr;
canvas.style.height = `${parentHeight}px`; canvas.height = parentHeight * dpr;
updateCanvas(); canvas.style.width = `${parentWidth}px`;
}); canvas.style.height = `${parentHeight}px`;
const handleResize = useLastCallback((entry: ResizeObserverEntry) => { updateCanvas();
});
}, [dpr]);
const handleResize = useThrottleForHeavyAnimation((entry: ResizeObserverEntry) => {
const { width, height } = entry.contentRect; const { width, height } = entry.contentRect;
requestMutation(() => { updateCanvasSize(width, height);
updateCanvasSize(width, height); }, [updateCanvasSize]);
});
});
useResizeObserver(containerRef, handleResize, !canUpdate); useResizeObserver(containerRef, handleResize);
useEffectWithPrevDeps(([prevDpr]) => { useEffect(() => {
if (dpr === prevDpr) return;
const container = containerRef.current; const container = containerRef.current;
if (!container || !canUpdate) return; if (!container) return;
const { width, height } = container.getBoundingClientRect(); const { width, height } = container.getBoundingClientRect();
requestMutation(() => { updateCanvasSize(width, height);
updateCanvasSize(width, height); }, [updateCanvasSize]);
});
}, [dpr, canUpdate]);
return ( return (
<div className={buildClassName(styles.root, className)} ref={containerRef}> <div className={buildClassName(styles.root, className)} ref={containerRef}>

View File

@ -1,4 +1,6 @@
import { useEffect } from '../lib/teact/teact'; import {
useCallback, useEffect, useMemo, useRef,
} from '../lib/teact/teact';
import { createCallbackManager } from '../util/callbacks'; import { createCallbackManager } from '../util/callbacks';
import useLastCallback from './useLastCallback'; import useLastCallback from './useLastCallback';
@ -39,6 +41,32 @@ const useHeavyAnimationCheck = (
}, [isDisabled, lastOnEnd, lastOnStart]); }, [isDisabled, lastOnEnd, lastOnStart]);
}; };
export function useThrottleForHeavyAnimation<T extends AnyToVoidFunction>(afterHeavyAnimation: T, deps: unknown[]) {
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
const fnMemo = useCallback(afterHeavyAnimation, deps);
const isScheduledRef = useRef(false);
return useMemo(() => {
return (...args: Parameters<T>) => {
if (!isScheduledRef.current) {
if (!isAnimating) {
fnMemo(...args);
return;
}
isScheduledRef.current = true;
const removeCallback = endCallbacks.addCallback(() => {
fnMemo(...args);
removeCallback();
isScheduledRef.current = false;
});
}
};
}, [fnMemo]);
}
export function isHeavyAnimating() { export function isHeavyAnimating() {
return isAnimating; return isAnimating;
} }