234 lines
7.1 KiB
TypeScript
234 lines
7.1 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,
|
|
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,
|
|
});
|
|
animation.play();
|
|
|
|
playersById.current.set(playerId, animation);
|
|
});
|
|
|
|
clearPlayers(Array.from(playerIdsToClear));
|
|
}, [absoluteContainerRef, textColor, inputRef, prefixId, clearPlayers, sharedCanvasHqRef, sharedCanvasRef]);
|
|
|
|
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(() => {
|
|
playersById.current?.forEach((player) => {
|
|
player.play();
|
|
});
|
|
}, []);
|
|
|
|
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');
|
|
}
|