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 = {
ref?: RefObject<HTMLDivElement>;
animationId?: string;
renderId?: string;
className?: string;
style?: string;
tgsUrl?: string;
@ -60,7 +60,7 @@ setTimeout(ensureLottie, LOTTIE_LOAD_DELAY);
const AnimatedSticker: FC<OwnProps> = ({
ref,
animationId,
renderId,
className,
style,
tgsUrl,
@ -86,7 +86,7 @@ const AnimatedSticker: FC<OwnProps> = ({
containerRef = ref;
}
const containerId = useMemo(() => generateIdFor(ID_STORE, true), []);
const viewId = useMemo(() => generateIdFor(ID_STORE, true), []);
const [animation, setAnimation] = useState<RLottieInstance>();
const animationRef = useRef<RLottieInstance>();
@ -128,11 +128,10 @@ const AnimatedSticker: FC<OwnProps> = ({
}
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<OwnProps> = ({
coords: sharedCanvasCoords,
},
color,
onLoad,
onEnded,
onLoop,
);
@ -165,8 +165,8 @@ const AnimatedSticker: FC<OwnProps> = ({
});
}
}, [
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<OwnProps> = ({
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<OwnProps> = ({
useSyncEffect(([prevSharedCanvasCoords]) => {
if (prevSharedCanvasCoords !== undefined && sharedCanvasCoords !== prevSharedCanvasCoords) {
animation?.setSharedCanvasCoords(containerId, sharedCanvasCoords);
animation?.setSharedCanvasCoords(viewId, sharedCanvasCoords);
}
}, [sharedCanvasCoords, containerId, animation]);
}, [sharedCanvasCoords, viewId, animation]);
useEffect(() => {
if (!animation) {

View File

@ -121,7 +121,7 @@ const StickerView: FC<OwnProps> = ({
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<OwnProps> = ({
/>
{isLottie ? (
<AnimatedSticker
key={idKey}
animationId={idKey}
key={renderId}
renderId={renderId}
size={realSize}
className={buildClassName(
styles.media,

View File

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

View File

@ -40,18 +40,18 @@ export default function useInputCustomEmojis(
sharedCanvasRef: React.RefObject<HTMLCanvasElement>,
sharedCanvasHqRef: React.RefObject<HTMLCanvasElement>,
absoluteContainerRef: React.RefObject<HTMLElement>,
prefixId?: string,
prefixId: string,
isActive?: boolean,
) {
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) => {
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<HTMLElement>('.custom-emoji'));
const playerIdsToClear = new Set(playersById.current.keys());
const customEmojis = Array.from(inputRef.current.querySelectorAll<HTMLElement>('.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<HTMLCanvasElement>;
sharedCanvasHqRef: React.RefObject<HTMLCanvasElement>;
absoluteContainerRef: React.RefObject<HTMLElement>;
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 });
},
};
}

View File

@ -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<string, RLottie>();
const instancesByRenderId = new Map<string, RLottie>();
const workers = new Array(MAX_WORKERS).fill(undefined).map(
() => createConnector<RLottieApi>(new Worker(new URL('./rlottie.worker.ts', import.meta.url))),
@ -39,7 +41,7 @@ let lastWorkerIndex = -1;
class RLottie {
// Config
private containers = new Map<string, {
private views = new Map<string, {
canvas: HTMLCanvasElement;
ctx: CanvasRenderingContext2D;
isLoaded?: boolean;
@ -90,40 +92,46 @@ class RLottie {
private lastRenderAt?: number;
static init(...args: ConstructorParameters<typeof RLottie>) {
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)],
});
}