TelegramPWA/src/components/middle/composer/hooks/useInputCustomEmojis.ts
Alexander Zinchuk 4f42b676ce Settings: Performance mode (#3045)
Co-authored-by: Alexander Zinchuk <alx.zinchuk@gmail.com>
2023-04-25 17:28:03 +04:00

244 lines
7.3 KiB
TypeScript

import {
useCallback, useEffect, useRef,
} from '../../../../lib/teact/teact';
import RLottie from '../../../../lib/rlottie/RLottie';
import { requestMeasure } from '../../../../lib/fasterdom/fasterdom';
import type { ApiSticker } from '../../../../api/types';
import type { Signal } from '../../../../util/signals';
import { getGlobal } from '../../../../global';
import { selectIsAlwaysHighPriorityEmoji } from '../../../../global/selectors';
import {
addCustomEmojiInputRenderCallback,
getCustomEmojiMediaDataForInput,
removeCustomEmojiInputRenderCallback,
} from '../../../../util/customEmojiManager';
import { round } from '../../../../util/math';
import AbsoluteVideo from '../../../../util/AbsoluteVideo';
import { REM } from '../../../common/helpers/mediaDimensions';
import useResizeObserver from '../../../../hooks/useResizeObserver';
import useBackgroundMode from '../../../../hooks/useBackgroundMode';
import useThrottledCallback from '../../../../hooks/useThrottledCallback';
import useDynamicColorListener from '../../../../hooks/useDynamicColorListener';
import useEffectWithPrevDeps from '../../../../hooks/useEffectWithPrevDeps';
const SIZE = 1.25 * REM;
const THROTTLE_MS = 300;
type CustomEmojiPlayer = {
play: () => void;
pause: () => void;
destroy: () => void;
updatePosition: (x: number, y: number) => void;
};
export default function useInputCustomEmojis(
getHtml: Signal<string>,
inputRef: React.RefObject<HTMLDivElement>,
sharedCanvasRef: React.RefObject<HTMLCanvasElement>,
sharedCanvasHqRef: React.RefObject<HTMLCanvasElement>,
absoluteContainerRef: React.RefObject<HTMLElement>,
prefixId: string,
canPlayAnimatedEmojis: boolean,
isActive?: boolean,
) {
const { rgbColor: textColor } = useDynamicColorListener(inputRef);
const playersById = useRef<Map<string, CustomEmojiPlayer>>(new Map());
const clearPlayers = useCallback((ids: string[]) => {
ids.forEach((id) => {
const player = playersById.current.get(id);
if (player) {
player.destroy();
playersById.current.delete(id);
}
});
}, []);
const synchronizeElements = useCallback(() => {
if (!inputRef.current || !sharedCanvasRef.current || !sharedCanvasHqRef.current) return;
const global = getGlobal();
const playerIdsToClear = new Set(playersById.current.keys());
const customEmojis = Array.from(inputRef.current.querySelectorAll<HTMLElement>('.custom-emoji'));
customEmojis.forEach((element) => {
if (!element.dataset.uniqueId) {
return;
}
const playerId = `${prefixId}${element.dataset.uniqueId}${textColor?.join(',') || ''}`;
const documentId = element.dataset.documentId!;
playerIdsToClear.delete(playerId);
const mediaUrl = getCustomEmojiMediaDataForInput(documentId);
if (!mediaUrl) {
return;
}
const canvasBounds = sharedCanvasRef.current!.getBoundingClientRect();
const elementBounds = element.getBoundingClientRect();
const x = round((elementBounds.left - canvasBounds.left) / canvasBounds.width, 4);
const y = round((elementBounds.top - canvasBounds.top) / canvasBounds.height, 4);
if (playersById.current.has(playerId)) {
const player = playersById.current.get(playerId)!;
player.updatePosition(x, y);
return;
}
const customEmoji = global.customEmojis.byId[documentId];
if (!customEmoji) {
return;
}
const isHq = customEmoji?.stickerSetInfo && selectIsAlwaysHighPriorityEmoji(global, customEmoji.stickerSetInfo);
const renderId = [
prefixId, documentId, textColor?.join(','),
].filter(Boolean).join('_');
const animation = createPlayer({
customEmoji,
sharedCanvasRef,
sharedCanvasHqRef,
absoluteContainerRef,
renderId,
viewId: playerId,
mediaUrl,
isHq,
position: { x, y },
textColor,
});
if (canPlayAnimatedEmojis) {
animation.play();
}
playersById.current.set(playerId, animation);
});
clearPlayers(Array.from(playerIdsToClear));
}, [
inputRef, sharedCanvasRef, sharedCanvasHqRef, clearPlayers, prefixId, textColor, absoluteContainerRef,
canPlayAnimatedEmojis,
]);
useEffect(() => {
addCustomEmojiInputRenderCallback(synchronizeElements);
return () => {
removeCustomEmojiInputRenderCallback(synchronizeElements);
};
}, [synchronizeElements]);
useEffect(() => {
if (!getHtml() || !inputRef.current || !sharedCanvasRef.current || !isActive) {
clearPlayers(Array.from(playersById.current.keys()));
return;
}
// Wait one frame for DOM to update
requestMeasure(() => {
synchronizeElements();
});
}, [getHtml, synchronizeElements, inputRef, clearPlayers, sharedCanvasRef, isActive]);
useEffectWithPrevDeps(([prevTextColor]) => {
if (textColor !== prevTextColor) {
synchronizeElements();
}
}, [textColor, synchronizeElements]);
const throttledSynchronizeElements = useThrottledCallback(
synchronizeElements,
[synchronizeElements],
THROTTLE_MS,
false,
);
useResizeObserver(sharedCanvasRef, throttledSynchronizeElements);
const freezeAnimation = useCallback(() => {
playersById.current.forEach((player) => {
player.pause();
});
}, []);
const unfreezeAnimation = useCallback(() => {
if (!canPlayAnimatedEmojis) {
return;
}
playersById.current?.forEach((player) => {
player.play();
});
}, [canPlayAnimatedEmojis]);
const unfreezeAnimationOnRaf = useCallback(() => {
requestMeasure(unfreezeAnimation);
}, [unfreezeAnimation]);
// 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);
}
function createPlayer({
customEmoji,
sharedCanvasRef,
sharedCanvasHqRef,
absoluteContainerRef,
renderId,
viewId,
mediaUrl,
position,
isHq,
textColor,
}: {
customEmoji: ApiSticker;
sharedCanvasRef: React.RefObject<HTMLCanvasElement>;
sharedCanvasHqRef: React.RefObject<HTMLCanvasElement>;
absoluteContainerRef: React.RefObject<HTMLElement>;
renderId: string;
viewId: string;
mediaUrl: string;
position: { x: number; y: number };
isHq?: boolean;
textColor?: [number, number, number];
}): CustomEmojiPlayer {
if (customEmoji.isLottie) {
const lottie = RLottie.init(
mediaUrl,
isHq ? sharedCanvasHqRef.current! : sharedCanvasRef.current!,
renderId,
{
size: SIZE,
coords: position,
isLowPriority: !isHq,
},
viewId,
customEmoji.shouldUseTextColor ? textColor : undefined,
);
return {
play: () => lottie.play(),
pause: () => lottie.pause(),
destroy: () => lottie.removeView(viewId),
updatePosition: (x: number, y: number) => {
return lottie.setSharedCanvasCoords(viewId, { x, y });
},
};
}
if (customEmoji.isVideo) {
const absoluteVideo = new AbsoluteVideo(mediaUrl, absoluteContainerRef.current!, { size: SIZE, position });
return {
play: () => absoluteVideo.play(),
pause: () => absoluteVideo.pause(),
destroy: () => absoluteVideo.destroy(),
updatePosition: (x: number, y: number) => absoluteVideo.updatePosition({ x, y }),
};
}
throw new Error('Unsupported custom emoji type');
}