Attachment Modal: Fix custom emoji playback in caption (#2759)

This commit is contained in:
Alexander Zinchuk 2023-03-10 02:34:01 +01:00
parent 4090cf093d
commit dd659cdb71
5 changed files with 98 additions and 88 deletions

View File

@ -16,7 +16,7 @@ import useSyncEffect from '../../hooks/useSyncEffect';
export type OwnProps = { export type OwnProps = {
ref?: RefObject<HTMLDivElement>; ref?: RefObject<HTMLDivElement>;
animationId?: string; renderId?: string;
className?: string; className?: string;
style?: string; style?: string;
tgsUrl?: string; tgsUrl?: string;
@ -60,7 +60,7 @@ setTimeout(ensureLottie, LOTTIE_LOAD_DELAY);
const AnimatedSticker: FC<OwnProps> = ({ const AnimatedSticker: FC<OwnProps> = ({
ref, ref,
animationId, renderId,
className, className,
style, style,
tgsUrl, tgsUrl,
@ -86,7 +86,7 @@ const AnimatedSticker: FC<OwnProps> = ({
containerRef = ref; containerRef = ref;
} }
const containerId = useMemo(() => generateIdFor(ID_STORE, true), []); const viewId = useMemo(() => generateIdFor(ID_STORE, true), []);
const [animation, setAnimation] = useState<RLottieInstance>(); const [animation, setAnimation] = useState<RLottieInstance>();
const animationRef = useRef<RLottieInstance>(); const animationRef = useRef<RLottieInstance>();
@ -128,11 +128,10 @@ const AnimatedSticker: FC<OwnProps> = ({
} }
const newAnimation = RLottie.init( const newAnimation = RLottie.init(
containerId,
container,
onLoad,
animationId || generateIdFor(ID_STORE, true),
tgsUrl, tgsUrl,
container,
renderId || generateIdFor(ID_STORE, true),
viewId,
{ {
noLoop, noLoop,
size, size,
@ -141,6 +140,7 @@ const AnimatedSticker: FC<OwnProps> = ({
coords: sharedCanvasCoords, coords: sharedCanvasCoords,
}, },
color, color,
onLoad,
onEnded, onEnded,
onLoop, onLoop,
); );
@ -165,8 +165,8 @@ const AnimatedSticker: FC<OwnProps> = ({
}); });
} }
}, [ }, [
animation, animationId, tgsUrl, color, isLowPriority, noLoop, onLoad, quality, size, speed, onEnded, onLoop, animation, renderId, tgsUrl, color, isLowPriority, noLoop, onLoad, quality, size, speed, onEnded, onLoop,
containerId, sharedCanvas, sharedCanvasCoords, viewId, sharedCanvas, sharedCanvasCoords,
]); ]);
useEffect(() => { useEffect(() => {
@ -177,27 +177,27 @@ const AnimatedSticker: FC<OwnProps> = ({
useEffect(() => { useEffect(() => {
return () => { return () => {
animationRef.current?.removeContainer(containerId); animationRef.current?.removeView(viewId);
}; };
}, [containerId]); }, [viewId]);
const playAnimation = useCallback((shouldRestart = false) => { const playAnimation = useCallback((shouldRestart = false) => {
if (animation && (playRef.current || playSegmentRef.current)) { if (animation && (playRef.current || playSegmentRef.current)) {
if (playSegmentRef.current) { if (playSegmentRef.current) {
animation.playSegment(playSegmentRef.current); animation.playSegment(playSegmentRef.current);
} else { } else {
animation.play(shouldRestart, containerId); animation.play(shouldRestart, viewId);
} }
} }
}, [animation, containerId]); }, [animation, viewId]);
const pauseAnimation = useCallback(() => { const pauseAnimation = useCallback(() => {
if (!animation) { if (!animation) {
return; return;
} }
animation.pause(containerId); animation.pause(viewId);
}, [animation, containerId]); }, [animation, viewId]);
const freezeAnimation = useCallback(() => { const freezeAnimation = useCallback(() => {
isFrozen.current = true; isFrozen.current = true;
@ -234,9 +234,9 @@ const AnimatedSticker: FC<OwnProps> = ({
useSyncEffect(([prevSharedCanvasCoords]) => { useSyncEffect(([prevSharedCanvasCoords]) => {
if (prevSharedCanvasCoords !== undefined && sharedCanvasCoords !== prevSharedCanvasCoords) { if (prevSharedCanvasCoords !== undefined && sharedCanvasCoords !== prevSharedCanvasCoords) {
animation?.setSharedCanvasCoords(containerId, sharedCanvasCoords); animation?.setSharedCanvasCoords(viewId, sharedCanvasCoords);
} }
}, [sharedCanvasCoords, containerId, animation]); }, [sharedCanvasCoords, viewId, animation]);
useEffect(() => { useEffect(() => {
if (!animation) { if (!animation) {

View File

@ -121,7 +121,7 @@ const StickerView: FC<OwnProps> = ({
useMedia(previewMediaHash, !shouldLoad || !shouldPreloadPreview, undefined, cacheBuster); useMedia(previewMediaHash, !shouldLoad || !shouldPreloadPreview, undefined, cacheBuster);
const randomIdPrefix = useMemo(() => generateIdFor(ID_STORE, true), []); const randomIdPrefix = useMemo(() => generateIdFor(ID_STORE, true), []);
const idKey = [ const renderId = [
(withSharedAnimation ? SHARED_PREFIX : randomIdPrefix), id, realSize, customColor?.join(','), (withSharedAnimation ? SHARED_PREFIX : randomIdPrefix), id, realSize, customColor?.join(','),
].filter(Boolean).join('_'); ].filter(Boolean).join('_');
@ -141,8 +141,8 @@ const StickerView: FC<OwnProps> = ({
/> />
{isLottie ? ( {isLottie ? (
<AnimatedSticker <AnimatedSticker
key={idKey} key={renderId}
animationId={idKey} renderId={renderId}
size={realSize} size={realSize}
className={buildClassName( className={buildClassName(
styles.media, styles.media,

View File

@ -155,7 +155,7 @@ const MessageInput: FC<OwnProps & StateProps> = ({
sharedCanvasRef, sharedCanvasRef,
sharedCanvasHqRef, sharedCanvasHqRef,
absoluteContainerRef, absoluteContainerRef,
isAttachmentModalInput ? 'attachment' : undefined, isAttachmentModalInput ? 'attachment' : 'composer',
isActive, isActive,
); );

View File

@ -40,18 +40,18 @@ export default function useInputCustomEmojis(
sharedCanvasRef: React.RefObject<HTMLCanvasElement>, sharedCanvasRef: React.RefObject<HTMLCanvasElement>,
sharedCanvasHqRef: React.RefObject<HTMLCanvasElement>, sharedCanvasHqRef: React.RefObject<HTMLCanvasElement>,
absoluteContainerRef: React.RefObject<HTMLElement>, absoluteContainerRef: React.RefObject<HTMLElement>,
prefixId?: string, prefixId: string,
isActive?: boolean, isActive?: boolean,
) { ) {
const { rgbColor: textColor } = useDynamicColorListener(inputRef); const { rgbColor: textColor } = useDynamicColorListener(inputRef);
const mapRef = useRef<Map<string, CustomEmojiPlayer>>(new Map()); const playersById = useRef<Map<string, CustomEmojiPlayer>>(new Map());
const removeContainers = useCallback((ids: string[]) => { const clearPlayers = useCallback((ids: string[]) => {
ids.forEach((id) => { ids.forEach((id) => {
const player = mapRef.current.get(id); const player = playersById.current.get(id);
if (player) { if (player) {
player.destroy(); player.destroy();
mapRef.current.delete(id); playersById.current.delete(id);
} }
}); });
}, []); }, []);
@ -59,17 +59,17 @@ export default function useInputCustomEmojis(
const synchronizeElements = useCallback(() => { const synchronizeElements = useCallback(() => {
if (!inputRef.current || !sharedCanvasRef.current || !sharedCanvasHqRef.current) return; if (!inputRef.current || !sharedCanvasRef.current || !sharedCanvasHqRef.current) return;
const global = getGlobal(); const global = getGlobal();
const removedContainers = new Set(mapRef.current.keys()); const playerIdsToClear = new Set(playersById.current.keys());
const customEmojies = Array.from(inputRef.current.querySelectorAll<HTMLElement>('.custom-emoji')); const customEmojis = Array.from(inputRef.current.querySelectorAll<HTMLElement>('.custom-emoji'));
customEmojies.forEach((element) => { customEmojis.forEach((element) => {
if (!element.dataset.uniqueId) { if (!element.dataset.uniqueId) {
return; return;
} }
const id = `${prefixId || ''}${element.dataset.uniqueId}${textColor?.join(',') || ''}`; const playerId = `${prefixId}${element.dataset.uniqueId}${textColor?.join(',') || ''}`;
const documentId = element.dataset.documentId!; const documentId = element.dataset.documentId!;
removedContainers.delete(id); playerIdsToClear.delete(playerId);
const mediaUrl = getCustomEmojiMediaDataForInput(documentId); const mediaUrl = getCustomEmojiMediaDataForInput(documentId);
if (!mediaUrl) { if (!mediaUrl) {
@ -81,8 +81,8 @@ export default function useInputCustomEmojis(
const x = round((elementBounds.left - canvasBounds.left) / canvasBounds.width, 4); const x = round((elementBounds.left - canvasBounds.left) / canvasBounds.width, 4);
const y = round((elementBounds.top - canvasBounds.top) / canvasBounds.height, 4); const y = round((elementBounds.top - canvasBounds.top) / canvasBounds.height, 4);
if (mapRef.current.has(id)) { if (playersById.current.has(playerId)) {
const player = mapRef.current.get(id)!; const player = playersById.current.get(playerId)!;
player.updatePosition(x, y); player.updatePosition(x, y);
return; return;
} }
@ -92,14 +92,17 @@ export default function useInputCustomEmojis(
return; return;
} }
const isHq = customEmoji?.stickerSetInfo && selectIsAlwaysHighPriorityEmoji(global, customEmoji.stickerSetInfo); const isHq = customEmoji?.stickerSetInfo && selectIsAlwaysHighPriorityEmoji(global, customEmoji.stickerSetInfo);
const renderId = [
prefixId, documentId, textColor?.join(','),
].filter(Boolean).join('_');
const animation = createPlayer({ const animation = createPlayer({
customEmoji, customEmoji,
sharedCanvasRef, sharedCanvasRef,
sharedCanvasHqRef, sharedCanvasHqRef,
absoluteContainerRef, absoluteContainerRef,
uniqueId: id, renderId,
containerId: prefixId || id, viewId: playerId,
mediaUrl, mediaUrl,
isHq, isHq,
position: { x, y }, position: { x, y },
@ -107,11 +110,11 @@ export default function useInputCustomEmojis(
}); });
animation.play(); animation.play();
mapRef.current.set(id, animation); playersById.current.set(playerId, animation);
}); });
removeContainers(Array.from(removedContainers)); clearPlayers(Array.from(playerIdsToClear));
}, [absoluteContainerRef, textColor, inputRef, prefixId, removeContainers, sharedCanvasHqRef, sharedCanvasRef]); }, [absoluteContainerRef, textColor, inputRef, prefixId, clearPlayers, sharedCanvasHqRef, sharedCanvasRef]);
useEffect(() => { useEffect(() => {
addCustomEmojiInputRenderCallback(synchronizeElements); addCustomEmojiInputRenderCallback(synchronizeElements);
@ -123,7 +126,7 @@ export default function useInputCustomEmojis(
useEffect(() => { useEffect(() => {
if (!getHtml() || !inputRef.current || !sharedCanvasRef.current || !isActive) { if (!getHtml() || !inputRef.current || !sharedCanvasRef.current || !isActive) {
removeContainers(Array.from(mapRef.current.keys())); clearPlayers(Array.from(playersById.current.keys()));
return; return;
} }
@ -131,7 +134,7 @@ export default function useInputCustomEmojis(
fastRaf(() => { fastRaf(() => {
synchronizeElements(); synchronizeElements();
}); });
}, [getHtml, synchronizeElements, inputRef, removeContainers, sharedCanvasRef, isActive]); }, [getHtml, synchronizeElements, inputRef, clearPlayers, sharedCanvasRef, isActive]);
useEffectWithPrevDeps(([prevTextColor]) => { useEffectWithPrevDeps(([prevTextColor]) => {
if (textColor !== prevTextColor) { if (textColor !== prevTextColor) {
@ -148,13 +151,13 @@ export default function useInputCustomEmojis(
useResizeObserver(sharedCanvasRef, throttledSynchronizeElements); useResizeObserver(sharedCanvasRef, throttledSynchronizeElements);
const freezeAnimation = useCallback(() => { const freezeAnimation = useCallback(() => {
mapRef.current.forEach((player) => { playersById.current.forEach((player) => {
player.pause(); player.pause();
}); });
}, []); }, []);
const unfreezeAnimation = useCallback(() => { const unfreezeAnimation = useCallback(() => {
mapRef.current.forEach((player) => { playersById.current.forEach((player) => {
player.play(); player.play();
}); });
}, []); }, []);
@ -174,8 +177,8 @@ function createPlayer({
sharedCanvasRef, sharedCanvasRef,
sharedCanvasHqRef, sharedCanvasHqRef,
absoluteContainerRef, absoluteContainerRef,
uniqueId, renderId,
containerId, viewId,
mediaUrl, mediaUrl,
position, position,
isHq, isHq,
@ -185,8 +188,8 @@ function createPlayer({
sharedCanvasRef: React.RefObject<HTMLCanvasElement>; sharedCanvasRef: React.RefObject<HTMLCanvasElement>;
sharedCanvasHqRef: React.RefObject<HTMLCanvasElement>; sharedCanvasHqRef: React.RefObject<HTMLCanvasElement>;
absoluteContainerRef: React.RefObject<HTMLElement>; absoluteContainerRef: React.RefObject<HTMLElement>;
uniqueId: string; renderId: string;
containerId: string; viewId: string;
mediaUrl: string; mediaUrl: string;
position: { x: number; y: number }; position: { x: number; y: number };
isHq?: boolean; isHq?: boolean;
@ -194,11 +197,10 @@ function createPlayer({
}): CustomEmojiPlayer { }): CustomEmojiPlayer {
if (customEmoji.isLottie) { if (customEmoji.isLottie) {
const lottie = RLottie.init( const lottie = RLottie.init(
containerId,
isHq ? sharedCanvasHqRef.current! : sharedCanvasRef.current!,
undefined,
uniqueId,
mediaUrl, mediaUrl,
isHq ? sharedCanvasHqRef.current! : sharedCanvasRef.current!,
renderId,
viewId,
{ {
size: SIZE, size: SIZE,
coords: position, coords: position,
@ -210,9 +212,9 @@ function createPlayer({
return { return {
play: () => lottie.play(), play: () => lottie.play(),
pause: () => lottie.pause(), pause: () => lottie.pause(),
destroy: () => lottie.removeContainer(uniqueId), destroy: () => lottie.removeView(viewId),
updatePosition: (x: number, y: number) => { updatePosition: (x: number, y: number) => {
return lottie.setSharedCanvasCoords(uniqueId, { x, y }); return lottie.setSharedCanvasCoords(viewId, { x, y });
}, },
}; };
} }

View File

@ -7,6 +7,7 @@ import { createConnector } from '../../util/PostMessageConnector';
import { animate } from '../../util/animation'; import { animate } from '../../util/animation';
import cycleRestrict from '../../util/cycleRestrict'; import cycleRestrict from '../../util/cycleRestrict';
import { fastRaf } from '../../util/schedulers'; import { fastRaf } from '../../util/schedulers';
import generateIdFor from '../../util/generateIdFor';
interface Params { interface Params {
noLoop?: boolean; noLoop?: boolean;
@ -28,8 +29,9 @@ const LOW_PRIORITY_QUALITY = IS_ANDROID ? 0.5 : 0.75;
const LOW_PRIORITY_QUALITY_SIZE_THRESHOLD = 24; const LOW_PRIORITY_QUALITY_SIZE_THRESHOLD = 24;
const HIGH_PRIORITY_CACHE_MODULO = IS_SAFARI ? 2 : 4; const HIGH_PRIORITY_CACHE_MODULO = IS_SAFARI ? 2 : 4;
const LOW_PRIORITY_CACHE_MODULO = 0; const LOW_PRIORITY_CACHE_MODULO = 0;
const ID_STORE = {};
const instancesById = new Map<string, RLottie>(); const instancesByRenderId = new Map<string, RLottie>();
const workers = new Array(MAX_WORKERS).fill(undefined).map( const workers = new Array(MAX_WORKERS).fill(undefined).map(
() => createConnector<RLottieApi>(new Worker(new URL('./rlottie.worker.ts', import.meta.url))), () => createConnector<RLottieApi>(new Worker(new URL('./rlottie.worker.ts', import.meta.url))),
@ -39,7 +41,7 @@ let lastWorkerIndex = -1;
class RLottie { class RLottie {
// Config // Config
private containers = new Map<string, { private views = new Map<string, {
canvas: HTMLCanvasElement; canvas: HTMLCanvasElement;
ctx: CanvasRenderingContext2D; ctx: CanvasRenderingContext2D;
isLoaded?: boolean; isLoaded?: boolean;
@ -90,40 +92,46 @@ class RLottie {
private lastRenderAt?: number; private lastRenderAt?: number;
static init(...args: ConstructorParameters<typeof RLottie>) { static init(...args: ConstructorParameters<typeof RLottie>) {
const [container, canvas, onLoad, id, , params] = args; const [
let instance = instancesById.get(id); , canvas,
renderId,
viewId = generateIdFor(ID_STORE, true),
params, ,
onLoad,
] = args;
let instance = instancesByRenderId.get(renderId);
if (!instance) { if (!instance) {
// eslint-disable-next-line prefer-rest-params // eslint-disable-next-line prefer-rest-params
instance = new RLottie(...args); instance = new RLottie(...args);
instancesById.set(id, instance); instancesByRenderId.set(renderId, instance);
} else { } else {
instance.addContainer(container, canvas, onLoad, params?.coords); instance.addView(viewId, canvas, onLoad, params?.coords);
} }
return instance; return instance;
} }
constructor( constructor(
containerId: string,
container: HTMLDivElement | HTMLCanvasElement,
onLoad: NoneToVoidFunction | undefined,
private id: string,
private tgsUrl: string, private tgsUrl: string,
private params: Params = { }, private container: HTMLDivElement | HTMLCanvasElement,
private renderId: string,
private viewId: string = generateIdFor(ID_STORE, true),
private params: Params = {},
private customColor?: [number, number, number], private customColor?: [number, number, number],
private onLoad?: NoneToVoidFunction | undefined,
private onEnded?: (isDestroyed?: boolean) => void, private onEnded?: (isDestroyed?: boolean) => void,
private onLoop?: () => void, private onLoop?: () => void,
) { ) {
this.addContainer(containerId, container, onLoad, params.coords); this.addView(viewId, container, onLoad, params.coords);
this.initConfig(); this.initConfig();
this.initRenderer(); this.initRenderer();
} }
public removeContainer(containerId: string) { public removeView(viewId: string) {
const { const {
canvas, ctx, isSharedCanvas, coords, canvas, ctx, isSharedCanvas, coords,
} = this.containers.get(containerId)!; } = this.views.get(viewId)!;
if (isSharedCanvas) { if (isSharedCanvas) {
ctx.clearRect(coords!.x, coords!.y, this.imgSize, this.imgSize); ctx.clearRect(coords!.x, coords!.y, this.imgSize, this.imgSize);
@ -131,9 +139,9 @@ class RLottie {
canvas.remove(); canvas.remove();
} }
this.containers.delete(containerId); this.views.delete(viewId);
if (!this.containers.size) { if (!this.views.size) {
this.destroy(); this.destroy();
} }
} }
@ -142,9 +150,9 @@ class RLottie {
return this.isAnimating || this.isWaiting; return this.isAnimating || this.isWaiting;
} }
play(forceRestart = false, containerId?: string) { play(forceRestart = false, viewId?: string) {
if (containerId) { if (viewId) {
this.containers.get(containerId)!.isPaused = false; this.views.get(viewId)!.isPaused = false;
} }
if (this.isEnded && forceRestart) { if (this.isEnded && forceRestart) {
@ -156,11 +164,11 @@ class RLottie {
this.doPlay(); this.doPlay();
} }
pause(containerId?: string) { pause(viewId?: string) {
if (containerId) { if (viewId) {
this.containers.get(containerId)!.isPaused = true; this.views.get(viewId)!.isPaused = true;
const areAllContainersPaused = Array.from(this.containers.values()).every(({ isPaused }) => isPaused); const areAllContainersPaused = Array.from(this.views.values()).every(({ isPaused }) => isPaused);
if (!areAllContainersPaused) { if (!areAllContainersPaused) {
return; return;
} }
@ -202,8 +210,8 @@ class RLottie {
this.params.noLoop = noLoop; this.params.noLoop = noLoop;
} }
setSharedCanvasCoords(containerId: string, newCoords: Params['coords']) { setSharedCanvasCoords(viewId: string, newCoords: Params['coords']) {
const containerInfo = this.containers.get(containerId)!; const containerInfo = this.views.get(viewId)!;
const { const {
canvas, ctx, canvas, ctx,
} = containerInfo; } = containerInfo;
@ -230,8 +238,8 @@ class RLottie {
} }
} }
private addContainer( private addView(
containerId: string, viewId: string,
container: HTMLDivElement | HTMLCanvasElement, container: HTMLDivElement | HTMLCanvasElement,
onLoad?: NoneToVoidFunction, onLoad?: NoneToVoidFunction,
coords?: Params['coords'], coords?: Params['coords'],
@ -272,7 +280,7 @@ class RLottie {
container.appendChild(canvas); container.appendChild(canvas);
this.containers.set(containerId, { this.views.set(viewId, {
canvas, ctx, onLoad, canvas, ctx, onLoad,
}); });
} else { } else {
@ -287,7 +295,7 @@ class RLottie {
imgSize = Math.round(this.params.size! * sizeFactor); imgSize = Math.round(this.params.size! * sizeFactor);
this.containers.set(containerId, { this.views.set(viewId, {
canvas, canvas,
ctx, ctx,
isSharedCanvas: true, isSharedCanvas: true,
@ -328,7 +336,7 @@ class RLottie {
this.clearCache(); this.clearCache();
this.destroyRenderer(); this.destroyRenderer();
instancesById.delete(this.id); instancesByRenderId.delete(this.renderId);
} }
private clearCache() { private clearCache() {
@ -359,7 +367,7 @@ class RLottie {
workers[this.workerIndex].request({ workers[this.workerIndex].request({
name: 'init', name: 'init',
args: [ args: [
this.id, this.renderId,
this.tgsUrl, this.tgsUrl,
this.imgSize, this.imgSize,
this.params.isLowPriority || false, this.params.isLowPriority || false,
@ -372,7 +380,7 @@ class RLottie {
private destroyRenderer() { private destroyRenderer() {
workers[this.workerIndex].request({ workers[this.workerIndex].request({
name: 'destroy', name: 'destroy',
args: [this.id], args: [this.renderId],
}); });
} }
@ -395,7 +403,7 @@ class RLottie {
workers[this.workerIndex].request({ workers[this.workerIndex].request({
name: 'changeData', name: 'changeData',
args: [ args: [
this.id, this.renderId,
this.tgsUrl, this.tgsUrl,
this.params.isLowPriority || false, this.params.isLowPriority || false,
this.onChangeData.bind(this), this.onChangeData.bind(this),
@ -441,7 +449,7 @@ class RLottie {
// Paused from outside // Paused from outside
if (!this.isAnimating) { if (!this.isAnimating) {
const areAllLoaded = Array.from(this.containers.values()).every(({ isLoaded }) => isLoaded); const areAllLoaded = Array.from(this.views.values()).every(({ isLoaded }) => isLoaded);
if (areAllLoaded) { if (areAllLoaded) {
return false; return false;
} }
@ -464,7 +472,7 @@ class RLottie {
} }
if (frameIndex !== this.prevFrameIndex) { if (frameIndex !== this.prevFrameIndex) {
this.containers.forEach((containerData) => { this.views.forEach((containerData) => {
const { const {
ctx, isLoaded, isPaused, coords: { x, y } = {}, onLoad, ctx, isLoaded, isPaused, coords: { x, y } = {}, onLoad,
} = containerData; } = containerData;
@ -554,7 +562,7 @@ class RLottie {
workers[this.workerIndex].request({ workers[this.workerIndex].request({
name: 'renderFrames', name: 'renderFrames',
args: [this.id, frameIndex, this.onFrameLoad.bind(this)], args: [this.renderId, frameIndex, this.onFrameLoad.bind(this)],
}); });
} }