From dd659cdb71ff28f20627746d4aa25f2190da1969 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Fri, 10 Mar 2023 02:34:01 +0100 Subject: [PATCH] Attachment Modal: Fix custom emoji playback in caption (#2759) --- src/components/common/AnimatedSticker.tsx | 34 ++++---- src/components/common/StickerView.tsx | 6 +- .../middle/composer/MessageInput.tsx | 2 +- .../composer/hooks/useInputCustomEmojis.ts | 64 ++++++++------- src/lib/rlottie/RLottie.ts | 80 ++++++++++--------- 5 files changed, 98 insertions(+), 88 deletions(-) diff --git a/src/components/common/AnimatedSticker.tsx b/src/components/common/AnimatedSticker.tsx index dde67df13..ae05f85b2 100644 --- a/src/components/common/AnimatedSticker.tsx +++ b/src/components/common/AnimatedSticker.tsx @@ -16,7 +16,7 @@ import useSyncEffect from '../../hooks/useSyncEffect'; export type OwnProps = { ref?: RefObject; - animationId?: string; + renderId?: string; className?: string; style?: string; tgsUrl?: string; @@ -60,7 +60,7 @@ setTimeout(ensureLottie, LOTTIE_LOAD_DELAY); const AnimatedSticker: FC = ({ ref, - animationId, + renderId, className, style, tgsUrl, @@ -86,7 +86,7 @@ const AnimatedSticker: FC = ({ containerRef = ref; } - const containerId = useMemo(() => generateIdFor(ID_STORE, true), []); + const viewId = useMemo(() => generateIdFor(ID_STORE, true), []); const [animation, setAnimation] = useState(); const animationRef = useRef(); @@ -128,11 +128,10 @@ const AnimatedSticker: FC = ({ } const newAnimation = RLottie.init( - containerId, - container, - onLoad, - animationId || generateIdFor(ID_STORE, true), tgsUrl, + container, + renderId || generateIdFor(ID_STORE, true), + viewId, { noLoop, size, @@ -141,6 +140,7 @@ const AnimatedSticker: FC = ({ coords: sharedCanvasCoords, }, color, + onLoad, onEnded, onLoop, ); @@ -165,8 +165,8 @@ const AnimatedSticker: FC = ({ }); } }, [ - animation, animationId, tgsUrl, color, isLowPriority, noLoop, onLoad, quality, size, speed, onEnded, onLoop, - containerId, sharedCanvas, sharedCanvasCoords, + animation, renderId, tgsUrl, color, isLowPriority, noLoop, onLoad, quality, size, speed, onEnded, onLoop, + viewId, sharedCanvas, sharedCanvasCoords, ]); useEffect(() => { @@ -177,27 +177,27 @@ const AnimatedSticker: FC = ({ useEffect(() => { return () => { - animationRef.current?.removeContainer(containerId); + animationRef.current?.removeView(viewId); }; - }, [containerId]); + }, [viewId]); const playAnimation = useCallback((shouldRestart = false) => { if (animation && (playRef.current || playSegmentRef.current)) { if (playSegmentRef.current) { animation.playSegment(playSegmentRef.current); } else { - animation.play(shouldRestart, containerId); + animation.play(shouldRestart, viewId); } } - }, [animation, containerId]); + }, [animation, viewId]); const pauseAnimation = useCallback(() => { if (!animation) { return; } - animation.pause(containerId); - }, [animation, containerId]); + animation.pause(viewId); + }, [animation, viewId]); const freezeAnimation = useCallback(() => { isFrozen.current = true; @@ -234,9 +234,9 @@ const AnimatedSticker: FC = ({ useSyncEffect(([prevSharedCanvasCoords]) => { if (prevSharedCanvasCoords !== undefined && sharedCanvasCoords !== prevSharedCanvasCoords) { - animation?.setSharedCanvasCoords(containerId, sharedCanvasCoords); + animation?.setSharedCanvasCoords(viewId, sharedCanvasCoords); } - }, [sharedCanvasCoords, containerId, animation]); + }, [sharedCanvasCoords, viewId, animation]); useEffect(() => { if (!animation) { diff --git a/src/components/common/StickerView.tsx b/src/components/common/StickerView.tsx index d9d0b6d1b..a4eaf9691 100644 --- a/src/components/common/StickerView.tsx +++ b/src/components/common/StickerView.tsx @@ -121,7 +121,7 @@ const StickerView: FC = ({ useMedia(previewMediaHash, !shouldLoad || !shouldPreloadPreview, undefined, cacheBuster); const randomIdPrefix = useMemo(() => generateIdFor(ID_STORE, true), []); - const idKey = [ + const renderId = [ (withSharedAnimation ? SHARED_PREFIX : randomIdPrefix), id, realSize, customColor?.join(','), ].filter(Boolean).join('_'); @@ -141,8 +141,8 @@ const StickerView: FC = ({ /> {isLottie ? ( = ({ sharedCanvasRef, sharedCanvasHqRef, absoluteContainerRef, - isAttachmentModalInput ? 'attachment' : undefined, + isAttachmentModalInput ? 'attachment' : 'composer', isActive, ); diff --git a/src/components/middle/composer/hooks/useInputCustomEmojis.ts b/src/components/middle/composer/hooks/useInputCustomEmojis.ts index 66bbe4471..51cb10b13 100644 --- a/src/components/middle/composer/hooks/useInputCustomEmojis.ts +++ b/src/components/middle/composer/hooks/useInputCustomEmojis.ts @@ -40,18 +40,18 @@ export default function useInputCustomEmojis( sharedCanvasRef: React.RefObject, sharedCanvasHqRef: React.RefObject, absoluteContainerRef: React.RefObject, - prefixId?: string, + prefixId: string, isActive?: boolean, ) { const { rgbColor: textColor } = useDynamicColorListener(inputRef); - const mapRef = useRef>(new Map()); + const playersById = useRef>(new Map()); - const removeContainers = useCallback((ids: string[]) => { + const clearPlayers = useCallback((ids: string[]) => { ids.forEach((id) => { - const player = mapRef.current.get(id); + const player = playersById.current.get(id); if (player) { player.destroy(); - mapRef.current.delete(id); + playersById.current.delete(id); } }); }, []); @@ -59,17 +59,17 @@ export default function useInputCustomEmojis( const synchronizeElements = useCallback(() => { if (!inputRef.current || !sharedCanvasRef.current || !sharedCanvasHqRef.current) return; const global = getGlobal(); - const removedContainers = new Set(mapRef.current.keys()); - const customEmojies = Array.from(inputRef.current.querySelectorAll('.custom-emoji')); + const playerIdsToClear = new Set(playersById.current.keys()); + const customEmojis = Array.from(inputRef.current.querySelectorAll('.custom-emoji')); - customEmojies.forEach((element) => { + customEmojis.forEach((element) => { if (!element.dataset.uniqueId) { return; } - const id = `${prefixId || ''}${element.dataset.uniqueId}${textColor?.join(',') || ''}`; + const playerId = `${prefixId}${element.dataset.uniqueId}${textColor?.join(',') || ''}`; const documentId = element.dataset.documentId!; - removedContainers.delete(id); + playerIdsToClear.delete(playerId); const mediaUrl = getCustomEmojiMediaDataForInput(documentId); if (!mediaUrl) { @@ -81,8 +81,8 @@ export default function useInputCustomEmojis( const x = round((elementBounds.left - canvasBounds.left) / canvasBounds.width, 4); const y = round((elementBounds.top - canvasBounds.top) / canvasBounds.height, 4); - if (mapRef.current.has(id)) { - const player = mapRef.current.get(id)!; + if (playersById.current.has(playerId)) { + const player = playersById.current.get(playerId)!; player.updatePosition(x, y); return; } @@ -92,14 +92,17 @@ export default function useInputCustomEmojis( 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, - uniqueId: id, - containerId: prefixId || id, + renderId, + viewId: playerId, mediaUrl, isHq, position: { x, y }, @@ -107,11 +110,11 @@ export default function useInputCustomEmojis( }); animation.play(); - mapRef.current.set(id, animation); + playersById.current.set(playerId, animation); }); - removeContainers(Array.from(removedContainers)); - }, [absoluteContainerRef, textColor, inputRef, prefixId, removeContainers, sharedCanvasHqRef, sharedCanvasRef]); + clearPlayers(Array.from(playerIdsToClear)); + }, [absoluteContainerRef, textColor, inputRef, prefixId, clearPlayers, sharedCanvasHqRef, sharedCanvasRef]); useEffect(() => { addCustomEmojiInputRenderCallback(synchronizeElements); @@ -123,7 +126,7 @@ export default function useInputCustomEmojis( useEffect(() => { if (!getHtml() || !inputRef.current || !sharedCanvasRef.current || !isActive) { - removeContainers(Array.from(mapRef.current.keys())); + clearPlayers(Array.from(playersById.current.keys())); return; } @@ -131,7 +134,7 @@ export default function useInputCustomEmojis( fastRaf(() => { synchronizeElements(); }); - }, [getHtml, synchronizeElements, inputRef, removeContainers, sharedCanvasRef, isActive]); + }, [getHtml, synchronizeElements, inputRef, clearPlayers, sharedCanvasRef, isActive]); useEffectWithPrevDeps(([prevTextColor]) => { if (textColor !== prevTextColor) { @@ -148,13 +151,13 @@ export default function useInputCustomEmojis( useResizeObserver(sharedCanvasRef, throttledSynchronizeElements); const freezeAnimation = useCallback(() => { - mapRef.current.forEach((player) => { + playersById.current.forEach((player) => { player.pause(); }); }, []); const unfreezeAnimation = useCallback(() => { - mapRef.current.forEach((player) => { + playersById.current.forEach((player) => { player.play(); }); }, []); @@ -174,8 +177,8 @@ function createPlayer({ sharedCanvasRef, sharedCanvasHqRef, absoluteContainerRef, - uniqueId, - containerId, + renderId, + viewId, mediaUrl, position, isHq, @@ -185,8 +188,8 @@ function createPlayer({ sharedCanvasRef: React.RefObject; sharedCanvasHqRef: React.RefObject; absoluteContainerRef: React.RefObject; - uniqueId: string; - containerId: string; + renderId: string; + viewId: string; mediaUrl: string; position: { x: number; y: number }; isHq?: boolean; @@ -194,11 +197,10 @@ function createPlayer({ }): CustomEmojiPlayer { if (customEmoji.isLottie) { const lottie = RLottie.init( - containerId, - isHq ? sharedCanvasHqRef.current! : sharedCanvasRef.current!, - undefined, - uniqueId, mediaUrl, + isHq ? sharedCanvasHqRef.current! : sharedCanvasRef.current!, + renderId, + viewId, { size: SIZE, coords: position, @@ -210,9 +212,9 @@ function createPlayer({ return { play: () => lottie.play(), pause: () => lottie.pause(), - destroy: () => lottie.removeContainer(uniqueId), + destroy: () => lottie.removeView(viewId), updatePosition: (x: number, y: number) => { - return lottie.setSharedCanvasCoords(uniqueId, { x, y }); + return lottie.setSharedCanvasCoords(viewId, { x, y }); }, }; } diff --git a/src/lib/rlottie/RLottie.ts b/src/lib/rlottie/RLottie.ts index c9d4360a6..95d4724fa 100644 --- a/src/lib/rlottie/RLottie.ts +++ b/src/lib/rlottie/RLottie.ts @@ -7,6 +7,7 @@ import { createConnector } from '../../util/PostMessageConnector'; import { animate } from '../../util/animation'; import cycleRestrict from '../../util/cycleRestrict'; import { fastRaf } from '../../util/schedulers'; +import generateIdFor from '../../util/generateIdFor'; interface Params { noLoop?: boolean; @@ -28,8 +29,9 @@ const LOW_PRIORITY_QUALITY = IS_ANDROID ? 0.5 : 0.75; const LOW_PRIORITY_QUALITY_SIZE_THRESHOLD = 24; const HIGH_PRIORITY_CACHE_MODULO = IS_SAFARI ? 2 : 4; const LOW_PRIORITY_CACHE_MODULO = 0; +const ID_STORE = {}; -const instancesById = new Map(); +const instancesByRenderId = new Map(); const workers = new Array(MAX_WORKERS).fill(undefined).map( () => createConnector(new Worker(new URL('./rlottie.worker.ts', import.meta.url))), @@ -39,7 +41,7 @@ let lastWorkerIndex = -1; class RLottie { // Config - private containers = new Map) { - const [container, canvas, onLoad, id, , params] = args; - let instance = instancesById.get(id); + const [ + , canvas, + renderId, + viewId = generateIdFor(ID_STORE, true), + params, , + onLoad, + ] = args; + let instance = instancesByRenderId.get(renderId); if (!instance) { // eslint-disable-next-line prefer-rest-params instance = new RLottie(...args); - instancesById.set(id, instance); + instancesByRenderId.set(renderId, instance); } else { - instance.addContainer(container, canvas, onLoad, params?.coords); + instance.addView(viewId, canvas, onLoad, params?.coords); } return instance; } constructor( - containerId: string, - container: HTMLDivElement | HTMLCanvasElement, - onLoad: NoneToVoidFunction | undefined, - private id: 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 onLoad?: NoneToVoidFunction | undefined, private onEnded?: (isDestroyed?: boolean) => void, private onLoop?: () => void, ) { - this.addContainer(containerId, container, onLoad, params.coords); + this.addView(viewId, container, onLoad, params.coords); this.initConfig(); this.initRenderer(); } - public removeContainer(containerId: string) { + public removeView(viewId: string) { const { canvas, ctx, isSharedCanvas, coords, - } = this.containers.get(containerId)!; + } = this.views.get(viewId)!; if (isSharedCanvas) { ctx.clearRect(coords!.x, coords!.y, this.imgSize, this.imgSize); @@ -131,9 +139,9 @@ class RLottie { canvas.remove(); } - this.containers.delete(containerId); + this.views.delete(viewId); - if (!this.containers.size) { + if (!this.views.size) { this.destroy(); } } @@ -142,9 +150,9 @@ class RLottie { return this.isAnimating || this.isWaiting; } - play(forceRestart = false, containerId?: string) { - if (containerId) { - this.containers.get(containerId)!.isPaused = false; + play(forceRestart = false, viewId?: string) { + if (viewId) { + this.views.get(viewId)!.isPaused = false; } if (this.isEnded && forceRestart) { @@ -156,11 +164,11 @@ class RLottie { this.doPlay(); } - pause(containerId?: string) { - if (containerId) { - this.containers.get(containerId)!.isPaused = true; + pause(viewId?: string) { + if (viewId) { + 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) { return; } @@ -202,8 +210,8 @@ class RLottie { this.params.noLoop = noLoop; } - setSharedCanvasCoords(containerId: string, newCoords: Params['coords']) { - const containerInfo = this.containers.get(containerId)!; + setSharedCanvasCoords(viewId: string, newCoords: Params['coords']) { + const containerInfo = this.views.get(viewId)!; const { canvas, ctx, } = containerInfo; @@ -230,8 +238,8 @@ class RLottie { } } - private addContainer( - containerId: string, + private addView( + viewId: string, container: HTMLDivElement | HTMLCanvasElement, onLoad?: NoneToVoidFunction, coords?: Params['coords'], @@ -272,7 +280,7 @@ class RLottie { container.appendChild(canvas); - this.containers.set(containerId, { + this.views.set(viewId, { canvas, ctx, onLoad, }); } else { @@ -287,7 +295,7 @@ class RLottie { imgSize = Math.round(this.params.size! * sizeFactor); - this.containers.set(containerId, { + this.views.set(viewId, { canvas, ctx, isSharedCanvas: true, @@ -328,7 +336,7 @@ class RLottie { this.clearCache(); this.destroyRenderer(); - instancesById.delete(this.id); + instancesByRenderId.delete(this.renderId); } private clearCache() { @@ -359,7 +367,7 @@ class RLottie { workers[this.workerIndex].request({ name: 'init', args: [ - this.id, + this.renderId, this.tgsUrl, this.imgSize, this.params.isLowPriority || false, @@ -372,7 +380,7 @@ class RLottie { private destroyRenderer() { workers[this.workerIndex].request({ name: 'destroy', - args: [this.id], + args: [this.renderId], }); } @@ -395,7 +403,7 @@ class RLottie { workers[this.workerIndex].request({ name: 'changeData', args: [ - this.id, + this.renderId, this.tgsUrl, this.params.isLowPriority || false, this.onChangeData.bind(this), @@ -441,7 +449,7 @@ class RLottie { // Paused from outside 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) { return false; } @@ -464,7 +472,7 @@ class RLottie { } if (frameIndex !== this.prevFrameIndex) { - this.containers.forEach((containerData) => { + this.views.forEach((containerData) => { const { ctx, isLoaded, isPaused, coords: { x, y } = {}, onLoad, } = containerData; @@ -554,7 +562,7 @@ class RLottie { workers[this.workerIndex].request({ name: 'renderFrames', - args: [this.id, frameIndex, this.onFrameLoad.bind(this)], + args: [this.renderId, frameIndex, this.onFrameLoad.bind(this)], }); }