[Perf] Sticker View: Introduce shared canvas

This commit is contained in:
Alexander Zinchuk 2022-11-13 17:05:58 +04:00
parent 11196c8d94
commit a5cda0f209
37 changed files with 663 additions and 224 deletions

View File

@ -2,7 +2,7 @@ import type { RefObject } from 'react';
import type { FC } from '../../lib/teact/teact';
import React, {
useEffect, useRef, memo, useCallback, useState,
useEffect, useRef, memo, useCallback, useState, useMemo,
} from '../../lib/teact/teact';
import { fastRaf } from '../../util/schedulers';
@ -16,7 +16,7 @@ import generateIdFor from '../../util/generateIdFor';
export type OwnProps = {
ref?: RefObject<HTMLDivElement>;
id?: string;
animationId?: string;
className?: string;
style?: string;
tgsUrl?: string;
@ -26,9 +26,11 @@ export type OwnProps = {
noLoop?: boolean;
size: number;
quality?: number;
color?: [number, number, number];
isLowPriority?: boolean;
forceOnHeavyAnimation?: boolean;
color?: [number, number, number];
sharedCanvas?: HTMLCanvasElement;
sharedCanvasCoords?: { x: number; y: number };
onClick?: NoneToVoidFunction;
onLoad?: NoneToVoidFunction;
onEnded?: NoneToVoidFunction;
@ -57,7 +59,7 @@ setTimeout(ensureLottie, LOTTIE_LOAD_DELAY);
const AnimatedSticker: FC<OwnProps> = ({
ref,
id,
animationId,
className,
style,
tgsUrl,
@ -70,6 +72,8 @@ const AnimatedSticker: FC<OwnProps> = ({
isLowPriority,
color,
forceOnHeavyAnimation,
sharedCanvas,
sharedCanvasCoords,
onClick,
onLoad,
onEnded,
@ -81,6 +85,8 @@ const AnimatedSticker: FC<OwnProps> = ({
containerRef = ref;
}
const containerId = useMemo(() => generateIdFor(ID_STORE, true), []);
const [animation, setAnimation] = useState<RLottieInstance>();
const wasPlaying = useRef(false);
const isFrozen = useRef(false);
@ -92,25 +98,28 @@ const AnimatedSticker: FC<OwnProps> = ({
playSegmentRef.current = playSegment;
useEffect(() => {
if (animation || !tgsUrl) {
if (animation || !tgsUrl || (sharedCanvas && !sharedCanvasCoords)) {
return;
}
const exec = () => {
if (!containerRef.current) {
const container = containerRef.current || sharedCanvas;
if (!container) {
return;
}
const newAnimation = RLottie.init(
containerRef.current,
containerId,
container,
onLoad,
id || generateIdFor(ID_STORE, true),
animationId || generateIdFor(ID_STORE, true),
tgsUrl,
{
noLoop,
size,
quality,
isLowPriority,
coords: sharedCanvasCoords,
},
color,
onEnded,
@ -135,7 +144,10 @@ const AnimatedSticker: FC<OwnProps> = ({
});
});
}
}, [color, animation, tgsUrl, isLowPriority, noLoop, onLoad, quality, size, speed, onEnded, onLoop, id]);
}, [
animation, animationId, tgsUrl, color, isLowPriority, noLoop, onLoad, quality, size, speed, onEnded, onLoop,
containerId, sharedCanvas, sharedCanvasCoords,
]);
useEffect(() => {
if (!animation) return;
@ -144,32 +156,30 @@ const AnimatedSticker: FC<OwnProps> = ({
}, [color, animation]);
useEffect(() => {
const container = containerRef.current!;
return () => {
if (animation) {
animation.removeContainer(container);
animation.removeContainer(containerId);
}
};
}, [animation]);
}, [animation, containerId]);
const playAnimation = useCallback((shouldRestart = false) => {
if (animation && (playRef.current || playSegmentRef.current)) {
if (playSegmentRef.current) {
animation.playSegment(playSegmentRef.current);
} else {
animation.play(shouldRestart, containerRef.current!);
animation.play(shouldRestart, containerId);
}
}
}, [animation]);
}, [animation, containerId]);
const pauseAnimation = useCallback(() => {
if (!animation) {
return;
}
animation.pause(containerRef.current!);
}, [animation]);
animation.pause(containerId);
}, [animation, containerId]);
const freezeAnimation = useCallback(() => {
isFrozen.current = true;
@ -241,6 +251,10 @@ const AnimatedSticker: FC<OwnProps> = ({
// then we can play again.
useBackgroundMode(freezeAnimation, unfreezeAnimationOnRaf);
if (sharedCanvas) {
return undefined;
}
return (
<div
ref={containerRef}

View File

@ -12,7 +12,7 @@ import { getPropertyHexColor } from '../../util/themeStyle';
import { hexToRgb } from '../../util/switchTheme';
import buildClassName from '../../util/buildClassName';
import safePlay from '../../util/safePlay';
import { selectIsDefaultEmojiStatusPack } from '../../global/selectors';
import { selectIsAlwaysHighPriorityEmoji, selectIsDefaultEmojiStatusPack } from '../../global/selectors';
import useEnsureCustomEmoji from '../../hooks/useEnsureCustomEmoji';
import useCustomEmoji from './hooks/useCustomEmoji';
@ -31,8 +31,11 @@ type OwnProps = {
className?: string;
loopLimit?: number;
style?: string;
withSharedAnimation?: boolean;
withGridFix?: boolean;
withSharedAnimation?: boolean;
sharedCanvasRef?: React.RefObject<HTMLCanvasElement>;
sharedCanvasHqRef?: React.RefObject<HTMLCanvasElement>;
withTranslucentThumb?: boolean;
shouldPreloadPreview?: boolean;
forceOnHeavyAnimation?: boolean;
observeIntersectionForLoading?: ObserveFn;
@ -40,7 +43,7 @@ type OwnProps = {
onClick?: NoneToVoidFunction;
};
const STICKER_SIZE = 24;
const STICKER_SIZE = 20;
const CustomEmoji: FC<OwnProps> = ({
ref,
@ -50,9 +53,12 @@ const CustomEmoji: FC<OwnProps> = ({
loopLimit,
style,
withGridFix,
withSharedAnimation,
sharedCanvasRef,
sharedCanvasHqRef,
withTranslucentThumb,
shouldPreloadPreview,
forceOnHeavyAnimation,
withSharedAnimation,
observeIntersectionForLoading,
observeIntersectionForPlaying,
onClick,
@ -121,6 +127,8 @@ const CustomEmoji: FC<OwnProps> = ({
}
}, [loopLimit]);
const isHq = customEmoji?.stickerSetInfo && selectIsAlwaysHighPriorityEmoji(getGlobal(), customEmoji.stickerSetInfo);
return (
<div
ref={containerRef}
@ -157,6 +165,8 @@ const CustomEmoji: FC<OwnProps> = ({
observeIntersectionForLoading={observeIntersectionForLoading}
observeIntersectionForPlaying={observeIntersectionForPlaying}
withSharedAnimation={withSharedAnimation}
sharedCanvasRef={isHq ? sharedCanvasHqRef : sharedCanvasRef}
withTranslucentThumb={withTranslucentThumb}
onVideoEnded={handleVideoEnded}
onAnimatedStickerLoop={handleStickerLoop}
/>

View File

@ -13,7 +13,6 @@ import {
import renderText from './helpers/renderText';
import { getPictogramDimensions } from './helpers/mediaDimensions';
import buildClassName from '../../util/buildClassName';
import { renderMessageSummary } from './helpers/renderMessageText';
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
@ -22,11 +21,11 @@ import useThumbnail from '../../hooks/useThumbnail';
import useLang from '../../hooks/useLang';
import ActionMessage from '../middle/ActionMessage';
import MessageSummary from './MessageSummary';
import './EmbeddedMessage.scss';
type OwnProps = {
observeIntersection?: ObserveFn;
className?: string;
message?: ApiMessage;
sender?: ApiUser | ApiChat;
@ -35,6 +34,8 @@ type OwnProps = {
noUserColors?: boolean;
isProtected?: boolean;
hasContextMenu?: boolean;
observeIntersectionForLoading?: ObserveFn;
observeIntersectionForPlaying?: ObserveFn;
onClick: NoneToVoidFunction;
};
@ -49,12 +50,13 @@ const EmbeddedMessage: FC<OwnProps> = ({
isProtected,
noUserColors,
hasContextMenu,
observeIntersection,
observeIntersectionForLoading,
observeIntersectionForPlaying,
onClick,
}) => {
// eslint-disable-next-line no-null/no-null
const ref = useRef<HTMLDivElement>(null);
const isIntersecting = useIsIntersecting(ref, observeIntersection);
const isIntersecting = useIsIntersecting(ref, observeIntersectionForLoading);
const mediaBlobUrl = useMedia(message && getMessageMediaHash(message, 'pictogram'), !isIntersecting);
const mediaThumbnail = useThumbnail(message);
@ -80,9 +82,20 @@ const EmbeddedMessage: FC<OwnProps> = ({
{!message ? (
customText || NBSP
) : isActionMessage(message) ? (
<ActionMessage message={message} isEmbedded />
<ActionMessage
message={message}
isEmbedded
observeIntersectionForLoading={observeIntersectionForLoading}
observeIntersectionForPlaying={observeIntersectionForPlaying}
/>
) : (
renderMessageSummary(lang, message, Boolean(mediaThumbnail))
<MessageSummary
lang={lang}
message={message}
noEmoji={Boolean(mediaThumbnail)}
observeIntersectionForLoading={observeIntersectionForLoading}
observeIntersectionForPlaying={observeIntersectionForPlaying}
/>
)}
</p>
<div className="message-title" dir="auto">{renderText(senderTitle || title || NBSP)}</div>

View File

@ -0,0 +1,83 @@
import React, { memo } from '../../lib/teact/teact';
import type { ApiMessage } from '../../api/types';
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
import type { LangFn } from '../../hooks/useLang';
import { ApiMessageEntityTypes } from '../../api/types';
import trimText from '../../util/trimText';
import {
getMessageSummaryDescription,
getMessageSummaryEmoji,
getMessageSummaryText,
TRUNCATED_SUMMARY_LENGTH,
} from '../../global/helpers';
import renderText from './helpers/renderText';
import MessageText from './MessageText';
interface OwnProps {
lang: LangFn;
message: ApiMessage;
noEmoji?: boolean;
highlight?: string;
truncateLength?: number;
observeIntersectionForLoading?: ObserveFn;
observeIntersectionForPlaying?: ObserveFn;
withTranslucentThumbs?: boolean;
}
function MessageSummary({
lang,
message,
noEmoji = false,
highlight,
truncateLength = TRUNCATED_SUMMARY_LENGTH,
observeIntersectionForLoading,
observeIntersectionForPlaying,
withTranslucentThumbs,
}: OwnProps) {
const { text, entities } = message.content.text || {};
const hasSpoilers = entities?.some((e) => e.type === ApiMessageEntityTypes.Spoiler);
const hasCustomEmoji = entities?.some((e) => e.type === ApiMessageEntityTypes.CustomEmoji);
if (!text || (!hasSpoilers && !hasCustomEmoji)) {
const trimmedText = trimText(getMessageSummaryText(lang, message, noEmoji), truncateLength);
return (
<span>
{highlight ? (
renderText(trimmedText, ['emoji', 'highlight'], { highlight })
) : (
renderText(trimmedText)
)}
</span>
);
}
function renderMessageText() {
return (
<MessageText
message={message}
highlight={highlight}
isSimple
observeIntersectionForLoading={observeIntersectionForLoading}
observeIntersectionForPlaying={observeIntersectionForPlaying}
withTranslucentThumbs={withTranslucentThumbs}
/>
);
}
const emoji = !noEmoji && getMessageSummaryEmoji(message);
return (
<>
{[
emoji ? renderText(`${emoji} `) : undefined,
getMessageSummaryDescription(lang, message, renderMessageText()),
].flat().filter(Boolean)}
</>
);
}
export default memo(MessageSummary);

View File

@ -0,0 +1,79 @@
import React, { memo, useMemo, useRef } from '../../lib/teact/teact';
import type { ApiMessage } from '../../api/types';
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
import { ApiMessageEntityTypes } from '../../api/types';
import trimText from '../../util/trimText';
import { getMessageText } from '../../global/helpers';
import { renderTextWithEntities } from './helpers/renderTextWithEntities';
interface OwnProps {
message: ApiMessage;
emojiSize?: number;
highlight?: string;
isSimple?: boolean;
truncateLength?: number;
isProtected?: boolean;
observeIntersectionForLoading?: ObserveFn;
observeIntersectionForPlaying?: ObserveFn;
withTranslucentThumbs?: boolean;
shouldRenderAsHtml?: boolean;
}
const MIN_CUSTOM_EMOJIS_FOR_SHARED_CANVAS = 1;
function MessageText({
message,
emojiSize,
highlight,
isSimple,
truncateLength,
isProtected,
observeIntersectionForLoading,
observeIntersectionForPlaying,
withTranslucentThumbs,
shouldRenderAsHtml,
}: OwnProps) {
// eslint-disable-next-line no-null/no-null
const sharedCanvasRef = useRef<HTMLCanvasElement>(null);
// eslint-disable-next-line no-null/no-null
const sharedCanvasHqRef = useRef<HTMLCanvasElement>(null);
const { text, entities } = message.content.text || {};
const withSharedCanvas = useMemo(() => {
const customEmojisCount = entities?.filter((e) => e.type === ApiMessageEntityTypes.CustomEmoji).length || 0;
return customEmojisCount >= MIN_CUSTOM_EMOJIS_FOR_SHARED_CANVAS;
}, [entities]) || 0;
if (!text) {
const contentNotSupportedText = getMessageText(message);
return contentNotSupportedText ? [trimText(contentNotSupportedText, truncateLength)] : undefined as any;
}
return (
<>
{[
withSharedCanvas && <canvas ref={sharedCanvasRef} className="shared-canvas" />,
withSharedCanvas && <canvas ref={sharedCanvasHqRef} className="shared-canvas" />,
renderTextWithEntities(
trimText(text!, truncateLength),
entities,
highlight,
emojiSize,
shouldRenderAsHtml,
message.id,
isSimple,
isProtected,
observeIntersectionForLoading,
observeIntersectionForPlaying,
withTranslucentThumbs,
sharedCanvasRef,
sharedCanvasHqRef,
),
].flat().filter(Boolean)}
</>
);
}
export default memo(MessageText);

View File

@ -33,6 +33,7 @@ type OwnProps<T> = {
isSavedMessages?: boolean;
canViewSet?: boolean;
isCurrentUserPremium?: boolean;
sharedCanvasRef?: React.RefObject<HTMLCanvasElement>;
observeIntersection: ObserveFn;
onClick?: (arg: OwnProps<T>['clickArg'], isSilent?: boolean, shouldSchedule?: boolean) => void;
clickArg: T;
@ -47,16 +48,17 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
noAnimate,
title,
className,
clickArg,
noContextMenu,
isSavedMessages,
canViewSet,
observeIntersection,
isCurrentUserPremium,
sharedCanvasRef,
onClick,
clickArg,
onFaveClick,
onUnfaveClick,
onRemoveRecentClick,
isCurrentUserPremium,
}: OwnProps<T>) => {
const { openStickerSet, openPremiumModal } = getActions();
// eslint-disable-next-line no-null/no-null
@ -231,6 +233,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
noLoad={!shouldLoad}
noPlay={!shouldPlay}
withSharedAnimation
sharedCanvasRef={sharedCanvasRef}
/>
{isLocked && (
<div

View File

@ -71,6 +71,9 @@ const StickerSetModal: FC<OwnProps & StateProps> = ({
// eslint-disable-next-line no-null/no-null
const containerRef = useRef<HTMLDivElement>(null);
// eslint-disable-next-line no-null/no-null
const sharedCanvasRef = useRef<HTMLCanvasElement>(null);
const lang = useLang();
const prevStickerSet = usePrevious(stickerSet);
@ -148,17 +151,21 @@ const StickerSetModal: FC<OwnProps & StateProps> = ({
{renderingStickerSet?.stickers ? (
<>
<div ref={containerRef} className="stickers custom-scroll">
{renderingStickerSet.stickers.map((sticker) => (
<StickerButton
sticker={sticker}
size={isEmoji ? EMOJI_SIZE_MODAL : STICKER_SIZE_MODAL}
observeIntersection={observeIntersection}
onClick={canSendStickers && !isEmoji ? handleSelect : undefined}
clickArg={sticker}
isSavedMessages={isSavedMessages}
isCurrentUserPremium={isCurrentUserPremium}
/>
))}
<div className="shared-canvas-container">
<canvas ref={sharedCanvasRef} className="shared-canvas" />
{renderingStickerSet.stickers.map((sticker) => (
<StickerButton
sticker={sticker}
size={isEmoji ? EMOJI_SIZE_MODAL : STICKER_SIZE_MODAL}
observeIntersection={observeIntersection}
onClick={canSendStickers && !isEmoji ? handleSelect : undefined}
clickArg={sticker}
isSavedMessages={isSavedMessages}
isCurrentUserPremium={isCurrentUserPremium}
sharedCanvasRef={sharedCanvasRef}
/>
))}
</div>
</div>
<div className="button-wrapper">
<Button

View File

@ -1,10 +1,20 @@
:root {
--thumbs-background: var(--color-background);
}
.thumb {
width: 100%;
height: 100%;
z-index: 1;
&:global(.closing) {
transition-delay: 150ms;
}
&_opaque {
background: var(--thumbs-background);
transition-delay: 0s;
}
}
.noTransition {

View File

@ -1,4 +1,4 @@
import React, { memo, useState } from '../../lib/teact/teact';
import React, { memo, useMemo, useState } from '../../lib/teact/teact';
import { getGlobal } from '../../global';
import type { FC } from '../../lib/teact/teact';
@ -17,6 +17,7 @@ import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
import useThumbnail from '../../hooks/useThumbnail';
import useMediaTransition from '../../hooks/useMediaTransition';
import useFlag from '../../hooks/useFlag';
import useSharedCanvasCoords from '../../hooks/useSharedCanvasCoords';
import AnimatedSticker from './AnimatedSticker';
import OptimizedVideo from '../ui/OptimizedVideo';
@ -41,6 +42,8 @@ type OwnProps = {
noLoad?: boolean;
noPlay?: boolean;
withSharedAnimation?: boolean;
sharedCanvasRef?: React.RefObject<HTMLCanvasElement>;
withTranslucentThumb?: boolean; // With shared canvas thumbs are opaque by default to provide better transition effect
cacheBuster?: number;
onVideoEnded?: AnyToVoidFunction;
onAnimatedStickerLoop?: AnyToVoidFunction;
@ -68,6 +71,8 @@ const StickerView: FC<OwnProps> = ({
noLoad,
noPlay,
withSharedAnimation,
withTranslucentThumb,
sharedCanvasRef,
cacheBuster,
onVideoEnded,
onAnimatedStickerLoop,
@ -104,14 +109,17 @@ const StickerView: FC<OwnProps> = ({
const [isPlayerReady, markPlayerReady] = useFlag(Boolean(isLottie && fullMediaData && !preloadedPreviewData));
const isFullMediaReady = fullMediaData && (isStatic || isPlayerReady);
const isThumbOpaque = sharedCanvasRef && !withTranslucentThumb;
const thumbClassNames = useMediaTransition(thumbData && !isFullMediaReady);
const fullMediaClassNames = useMediaTransition(isFullMediaReady);
const noTransition = isLottie && preloadedPreviewData;
const sharedCanvasCoords = useSharedCanvasCoords(containerRef, sharedCanvasRef);
// Preload preview for Message Input and local message
useMedia(previewMediaHash, !shouldLoad || !shouldPreloadPreview, undefined, cacheBuster);
const [randomIdPrefix] = useState(generateIdFor(ID_STORE, true));
const randomIdPrefix = useMemo(() => generateIdFor(ID_STORE, true), []);
const idKey = [
(withSharedAnimation ? SHARED_PREFIX : randomIdPrefix), id, size, customColor?.join(','),
].filter(Boolean).join('_');
@ -120,16 +128,25 @@ const StickerView: FC<OwnProps> = ({
<>
<img
src={thumbData}
className={buildClassName(styles.thumb, noTransition && styles.noTransition, thumbClassName, thumbClassNames)}
className={buildClassName(
styles.thumb,
noTransition && styles.noTransition,
isThumbOpaque && styles.thumb_opaque,
thumbClassName,
thumbClassNames,
)}
alt=""
/>
{isLottie ? (
<AnimatedSticker
key={idKey}
id={idKey}
animationId={idKey}
size={size}
className={buildClassName(
styles.media, noTransition && styles.noTransition, fullMediaClassName, fullMediaClassNames,
styles.media,
(noTransition || isThumbOpaque) && styles.noTransition,
fullMediaClassName,
fullMediaClassNames,
)}
tgsUrl={fullMediaData}
play={shouldPlay}
@ -137,6 +154,8 @@ const StickerView: FC<OwnProps> = ({
noLoop={!shouldLoop}
forceOnHeavyAnimation={forceOnHeavyAnimation}
isLowPriority={isSmall && !selectIsAlwaysHighPriorityEmoji(getGlobal(), stickerSetInfo)}
sharedCanvas={sharedCanvasRef?.current || undefined}
sharedCanvasCoords={sharedCanvasCoords}
onLoad={markPlayerReady}
onLoop={onAnimatedStickerLoop}
onEnded={onAnimatedStickerLoop}

View File

@ -4,8 +4,9 @@ import type {
ApiChat, ApiMessage, ApiUser, ApiGroupCall,
} from '../../../api/types';
import type { TextPart } from '../../../types';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import type { LangFn } from '../../../hooks/useLang';
import {
getChatTitle,
getMessageSummaryText,
@ -13,17 +14,17 @@ import {
} from '../../../global/helpers';
import trimText from '../../../util/trimText';
import { formatCurrency } from '../../../util/formatCurrency';
import { renderMessageSummary } from './renderMessageText';
import renderText from './renderText';
import UserLink from '../UserLink';
import MessageLink from '../MessageLink';
import ChatLink from '../ChatLink';
import GroupCallLink from '../GroupCallLink';
import MessageSummary from '../MessageSummary';
interface RenderOptions {
asPlainText?: boolean;
asTextWithSpoilers?: boolean;
isEmbedded?: boolean;
}
const MAX_LENGTH = 32;
@ -38,6 +39,8 @@ export function renderActionMessageText(
targetMessage?: ApiMessage,
targetChatId?: string,
options: RenderOptions = {},
observeIntersectionForLoading?: ObserveFn,
observeIntersectionForPlaying?: ObserveFn,
) {
if (!message.content.action) {
return [];
@ -47,7 +50,7 @@ export function renderActionMessageText(
text, translationValues, amount, currency, call, score,
} = message.content.action;
const content: TextPart[] = [];
const noLinks = options.asPlainText || options.asTextWithSpoilers;
const noLinks = options.asPlainText || options.isEmbedded;
const translationKey = text === 'Chat.Service.Group.UpdatedPinnedMessage1' && !targetMessage
? 'Message.PinnedGenericMessage'
: text;
@ -124,7 +127,9 @@ export function renderActionMessageText(
unprocessed,
'%message%',
targetMessage
? renderMessageContent(lang, targetMessage, options)
? renderMessageContent(
lang, targetMessage, options, observeIntersectionForLoading, observeIntersectionForPlaying,
)
: 'a message',
);
unprocessed = processed.pop() as string;
@ -168,19 +173,32 @@ function renderProductContent(message: ApiMessage) {
: 'a product';
}
function renderMessageContent(lang: LangFn, message: ApiMessage, options: RenderOptions = {}) {
const { asPlainText, asTextWithSpoilers } = options;
function renderMessageContent(
lang: LangFn,
message: ApiMessage,
options: RenderOptions = {},
observeIntersectionForLoading?: ObserveFn,
observeIntersectionForPlaying?: ObserveFn,
) {
const { asPlainText, isEmbedded } = options;
if (asPlainText) {
return getMessageSummaryText(lang, message, undefined, MAX_LENGTH);
}
const messageSummary = renderMessageSummary(lang, message, undefined, undefined, MAX_LENGTH);
const messageSummary = (
<MessageSummary
lang={lang}
message={message}
truncateLength={MAX_LENGTH}
observeIntersectionForLoading={observeIntersectionForLoading}
observeIntersectionForPlaying={observeIntersectionForPlaying}
withTranslucentThumbs
/>
);
if (asTextWithSpoilers) {
return (
<span>{messageSummary}</span>
);
if (isEmbedded) {
return messageSummary;
}
return (

View File

@ -1,7 +1,6 @@
import type { ApiMessage } from '../../../api/types';
import { ApiMessageEntityTypes } from '../../../api/types';
import type { TextPart } from '../../../types';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import type { LangFn } from '../../../hooks/useLang';
import {
@ -22,8 +21,6 @@ export function renderMessageText(
isSimple?: boolean,
truncateLength?: number,
isProtected?: boolean,
observeIntersectionForLoading?: ObserveFn,
observeIntersectionForPlaying?: ObserveFn,
shouldRenderAsHtml?: boolean,
) {
const { text, entities } = message.content.text || {};
@ -42,18 +39,16 @@ export function renderMessageText(
message.id,
isSimple,
isProtected,
observeIntersectionForLoading,
observeIntersectionForPlaying,
);
}
// TODO Use Message Summary component instead
export function renderMessageSummary(
lang: LangFn,
message: ApiMessage,
noEmoji = false,
highlight?: string,
truncateLength = TRUNCATED_SUMMARY_LENGTH,
observeIntersection?: ObserveFn,
): TextPart[] {
const { entities } = message.content.text || {};
@ -72,9 +67,7 @@ export function renderMessageSummary(
const emoji = !noEmoji && getMessageSummaryEmoji(message);
const emojiWithSpace = emoji ? `${emoji} ` : '';
const text = renderMessageText(
message, highlight, undefined, true, truncateLength, undefined, observeIntersection,
);
const text = renderMessageText(message, highlight, undefined, true, truncateLength);
const description = getMessageSummaryDescription(lang, message, text);
return [

View File

@ -1,8 +1,9 @@
import React from '../../../lib/teact/teact';
import EMOJI_REGEX from '../../../lib/twemojiRegex';
import type { TeactNode } from '../../../lib/teact/teact';
import type { TextPart } from '../../../types';
import EMOJI_REGEX from '../../../lib/twemojiRegex';
import { RE_LINK_TEMPLATE, RE_MENTION_TEMPLATE } from '../../../config';
import { IS_EMOJI_SUPPORTED } from '../../../util/environment';
import {
@ -29,7 +30,7 @@ export default function renderText(
part: TextPart,
filters: Array<TextFilter> = ['emoji'],
params?: { highlight: string | undefined },
): TextPart[] {
): TeactNode[] {
if (typeof part !== 'string') {
return [part];
}

View File

@ -38,6 +38,9 @@ export function renderTextWithEntities(
isProtected?: boolean,
observeIntersectionForLoading?: ObserveFn,
observeIntersectionForPlaying?: ObserveFn,
withTranslucentThumbs?: boolean,
sharedCanvasRef?: React.RefObject<HTMLCanvasElement>,
sharedCanvasHqRef?: React.RefObject<HTMLCanvasElement>,
) {
if (!entities || !entities.length) {
return renderMessagePart(text, highlight, emojiSize, shouldRenderAsHtml, isSimple);
@ -122,7 +125,10 @@ export function renderTextWithEntities(
isProtected,
observeIntersectionForLoading,
observeIntersectionForPlaying,
withTranslucentThumbs,
emojiSize,
sharedCanvasRef,
sharedCanvasHqRef,
);
if (Array.isArray(newEntity)) {
@ -301,7 +307,10 @@ function processEntity(
isProtected?: boolean,
observeIntersectionForLoading?: ObserveFn,
observeIntersectionForPlaying?: ObserveFn,
withTranslucentThumbs?: boolean,
emojiSize?: number,
sharedCanvasRef?: React.RefObject<HTMLCanvasElement>,
sharedCanvasHqRef?: React.RefObject<HTMLCanvasElement>,
) {
const entityText = typeof entityContent === 'string' && entityContent;
const renderedContent = nestedEntityContent.length ? nestedEntityContent : entityContent;
@ -328,9 +337,12 @@ function processEntity(
documentId={entity.documentId}
size={emojiSize}
withSharedAnimation
sharedCanvasRef={sharedCanvasRef}
sharedCanvasHqRef={sharedCanvasHqRef}
withGridFix={!emojiSize}
observeIntersectionForLoading={observeIntersectionForLoading}
observeIntersectionForPlaying={observeIntersectionForPlaying}
withTranslucentThumb={withTranslucentThumbs}
/>
);
}
@ -449,9 +461,12 @@ function processEntity(
documentId={entity.documentId}
size={emojiSize}
withSharedAnimation
sharedCanvasRef={sharedCanvasRef}
sharedCanvasHqRef={sharedCanvasHqRef}
withGridFix={!emojiSize}
observeIntersectionForLoading={observeIntersectionForLoading}
observeIntersectionForPlaying={observeIntersectionForPlaying}
withTranslucentThumb={withTranslucentThumbs}
/>
);
default:

View File

@ -1,5 +1,6 @@
.Chat {
--background-color: var(--color-background);
--thumbs-background: var(--background-color);
body.is-ios &,
body.is-macos & {

View File

@ -37,7 +37,6 @@ import renderText from '../../common/helpers/renderText';
import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities';
import { fastRaf } from '../../../util/schedulers';
import buildClassName from '../../../util/buildClassName';
import { renderMessageSummary } from '../../common/helpers/renderMessageText';
import useEnsureMessage from '../../../hooks/useEnsureMessage';
import useChatContextActions from '../../../hooks/useChatContextActions';
@ -56,6 +55,7 @@ import ChatFolderModal from '../ChatFolderModal.async';
import ChatCallStatus from './ChatCallStatus';
import ReportModal from '../../common/ReportModal';
import FullNameTitle from '../../common/FullNameTitle';
import MessageSummary from '../../common/MessageSummary';
import './Chat.scss';
@ -272,7 +272,7 @@ const Chat: FC<OwnProps & StateProps> = ({
actionTargetUsers,
actionTargetMessage,
actionTargetChatId,
{ asTextWithSpoilers: true },
{ isEmbedded: true },
)}
</p>
);
@ -379,15 +379,19 @@ const Chat: FC<OwnProps & StateProps> = ({
function renderSummary(
lang: LangFn, message: ApiMessage, observeIntersection?: ObserveFn, blobUrl?: string, isRoundVideo?: boolean,
) {
const messageSummary = (
<MessageSummary lang={lang} message={message} observeIntersectionForLoading={observeIntersection} />
);
if (!blobUrl) {
return renderMessageSummary(lang, message, undefined, undefined, undefined, observeIntersection);
return messageSummary;
}
return (
<span className="media-preview">
<img src={blobUrl} alt="" className={buildClassName('media-preview--image', isRoundVideo && 'round')} />
{getMessageVideo(message) && <i className="icon-play" />}
{renderMessageSummary(lang, message, true, undefined, undefined, observeIntersection)}
{messageSummary}
</span>
);
}

View File

@ -33,8 +33,9 @@ import AnimatedIconFromSticker from '../common/AnimatedIconFromSticker';
type OwnProps = {
message: ApiMessage;
observeIntersection?: ObserveFn;
observeIntersectionForAnimation?: ObserveFn;
observeIntersectionForReading?: ObserveFn;
observeIntersectionForLoading?: ObserveFn;
observeIntersectionForPlaying?: ObserveFn;
isEmbedded?: boolean;
appearanceOrder?: number;
isLastInList?: boolean;
@ -58,8 +59,9 @@ const APPEARANCE_DELAY = 10;
const ActionMessage: FC<OwnProps & StateProps> = ({
message,
observeIntersection,
observeIntersectionForAnimation,
observeIntersectionForReading,
observeIntersectionForLoading,
observeIntersectionForPlaying,
isEmbedded,
appearanceOrder = 0,
isLastInList,
@ -82,7 +84,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
// eslint-disable-next-line no-null/no-null
const ref = useRef<HTMLDivElement>(null);
useOnIntersect(ref, observeIntersection);
useOnIntersect(ref, observeIntersectionForReading);
useEnsureMessage(message.chatId, message.replyToMessageId, targetMessage);
useFocusMessage(ref, message.chatId, isFocused, focusDirection, noFocusHighlight);
@ -98,7 +100,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
setTimeout(markShown, appearanceOrder * APPEARANCE_DELAY);
}, [appearanceOrder, markShown, noAppearanceAnimation]);
const isVisible = useIsIntersecting(ref, observeIntersectionForAnimation);
const isVisible = useIsIntersecting(ref, observeIntersectionForPlaying);
const shouldShowConfettiRef = useRef((() => {
const isUnread = memoFirstUnreadIdRef?.current && message.id >= memoFirstUnreadIdRef.current;
@ -128,7 +130,9 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
targetUsers,
targetMessage,
targetChatId,
{ asTextWithSpoilers: isEmbedded },
{ isEmbedded },
observeIntersectionForLoading,
observeIntersectionForPlaying,
);
const {
isContextMenuOpen, contextMenuPosition,

View File

@ -6,7 +6,6 @@ import type { ApiMessage } from '../../api/types';
import { getPictogramDimensions } from '../common/helpers/mediaDimensions';
import { getMessageMediaHash, getMessageSingleInlineButton } from '../../global/helpers';
import { renderMessageSummary } from '../common/helpers/renderMessageText';
import buildClassName from '../../util/buildClassName';
import { IS_TOUCH_ENV } from '../../util/environment';
@ -19,6 +18,7 @@ import RippleEffect from '../ui/RippleEffect';
import ConfirmDialog from '../ui/ConfirmDialog';
import Button from '../ui/Button';
import PinnedMessageNavigation from './PinnedMessageNavigation';
import MessageSummary from '../common/MessageSummary';
type OwnProps = {
message: ApiMessage;
@ -39,7 +39,6 @@ const HeaderPinnedMessage: FC<OwnProps> = ({
const mediaThumbnail = useThumbnail(message);
const mediaBlobUrl = useMedia(getMessageMediaHash(message, 'pictogram'));
const text = renderMessageSummary(lang, message, Boolean(mediaThumbnail));
const [isUnpinDialogOpen, openUnpinDialog, closeUnpinDialog] = useFlag();
const handleUnpinMessage = useCallback(() => {
@ -107,7 +106,9 @@ const HeaderPinnedMessage: FC<OwnProps> = ({
<div className="title" dir="auto">
{customTitle || `${lang('PinnedMessage')} ${index > 0 ? `#${count - index}` : ''}`}
</div>
<p dir="auto">{text}</p>
<p dir="auto">
<MessageSummary lang={lang} message={message} noEmoji={Boolean(mediaThumbnail)} />
</p>
<RippleEffect />
</div>
{inlineButton && (

View File

@ -143,8 +143,9 @@ const MessageListContent: FC<OwnProps> = ({
<ActionMessage
key={message.id}
message={message}
observeIntersection={observeIntersectionForReading}
observeIntersectionForAnimation={observeIntersectionForAnimatedStickers}
observeIntersectionForReading={observeIntersectionForReading}
observeIntersectionForLoading={observeIntersectionForMedia}
observeIntersectionForPlaying={observeIntersectionForAnimatedStickers}
memoFirstUnreadIdRef={memoFirstUnreadIdRef}
appearanceOrder={messageCountToAnimate - ++appearanceIndex}
isLastInList={isLastInList}

View File

@ -2,7 +2,7 @@ import type { FC } from '../../../lib/teact/teact';
import React, {
useState, useEffect, memo, useRef, useMemo, useCallback,
} from '../../../lib/teact/teact';
import { withGlobal } from '../../../global';
import { getGlobal, withGlobal } from '../../../global';
import type { ApiStickerSet, ApiSticker, ApiChat } from '../../../api/types';
import type { StickerSetOrRecent } from '../../../types';
@ -12,7 +12,7 @@ import {
FAVORITE_SYMBOL_SET_ID,
PREMIUM_STICKER_SET_ID,
RECENT_SYMBOL_SET_ID,
SLIDE_TRANSITION_DURATION,
SLIDE_TRANSITION_DURATION, STICKER_PICKER_MAX_SHARED_COVERS,
STICKER_SIZE_PICKER_HEADER,
} from '../../../config';
import { IS_TOUCH_ENV } from '../../../util/environment';
@ -21,7 +21,11 @@ import fastSmoothScroll from '../../../util/fastSmoothScroll';
import buildClassName from '../../../util/buildClassName';
import fastSmoothScrollHorizontal from '../../../util/fastSmoothScrollHorizontal';
import { pickTruthy } from '../../../util/iteratees';
import { selectIsChatWithSelf, selectIsCurrentUserPremium } from '../../../global/selectors';
import {
selectIsAlwaysHighPriorityEmoji,
selectIsChatWithSelf,
selectIsCurrentUserPremium,
} from '../../../global/selectors';
import useAsyncRendering from '../../right/hooks/useAsyncRendering';
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
@ -76,6 +80,11 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
const containerRef = useRef<HTMLDivElement>(null);
// eslint-disable-next-line no-null/no-null
const headerRef = useRef<HTMLDivElement>(null);
// eslint-disable-next-line no-null/no-null
const sharedCanvasRef = useRef<HTMLCanvasElement>(null);
// eslint-disable-next-line no-null/no-null
const sharedCanvasHqRef = useRef<HTMLCanvasElement>(null);
const [activeSetIndex, setActiveSetIndex] = useState<number>(0);
const { observe: observeIntersection } = useIntersectionObserver({
@ -179,12 +188,16 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
index === activeSetIndex && 'activated',
);
const withSharedCanvas = index < STICKER_PICKER_MAX_SHARED_COVERS;
const isHq = selectIsAlwaysHighPriorityEmoji(getGlobal(), stickerSet as ApiStickerSet);
if (stickerSet.id === RECENT_SYMBOL_SET_ID
|| stickerSet.id === FAVORITE_SYMBOL_SET_ID
|| stickerSet.id === CHAT_STICKER_SET_ID
|| stickerSet.id === PREMIUM_STICKER_SET_ID
|| stickerSet.hasThumbnail
|| !firstSticker) {
|| !firstSticker
) {
return (
<Button
key={stickerSet.id}
@ -203,6 +216,7 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
stickerSet={stickerSet as ApiStickerSet}
noAnimate={!canAnimate || !loadAndPlay}
observeIntersection={observeIntersectionForCovers}
sharedCanvasRef={withSharedCanvas ? (isHq ? sharedCanvasHqRef : sharedCanvasRef) : undefined}
/>
)}
</Button>
@ -219,6 +233,7 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
observeIntersection={observeIntersectionForCovers}
noContextMenu
isCurrentUserPremium
sharedCanvasRef={withSharedCanvas ? (isHq ? sharedCanvasHqRef : sharedCanvasRef) : undefined}
onClick={selectStickerSet}
clickArg={index}
/>
@ -246,7 +261,11 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
ref={headerRef}
className="StickerPicker-header no-selection no-scrollbar"
>
{allSets.map(renderCover)}
<div className="shared-canvas-container">
<canvas ref={sharedCanvasRef} className="shared-canvas" />
<canvas ref={sharedCanvasHqRef} className="shared-canvas" />
{allSets.map(renderCover)}
</div>
</div>
<div
ref={containerRef}

View File

@ -80,6 +80,15 @@
background-color: var(--color-background-selected);
}
}
.shared-canvas-container {
display: inline-block;
}
.shared-canvas {
max-width: 1280px; // STICKER_PICKER_MAX_SHARED_COVERS * (STICKER_SIZE_PICKER_HEADER + 10 * 2)
z-index: 1;
}
}
.symbol-set-container {

View File

@ -12,7 +12,7 @@ import {
FAVORITE_SYMBOL_SET_ID,
PREMIUM_STICKER_SET_ID,
RECENT_SYMBOL_SET_ID,
SLIDE_TRANSITION_DURATION,
SLIDE_TRANSITION_DURATION, STICKER_PICKER_MAX_SHARED_COVERS,
STICKER_SIZE_PICKER_HEADER,
} from '../../../config';
import { IS_TOUCH_ENV } from '../../../util/environment';
@ -94,6 +94,9 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
const containerRef = useRef<HTMLDivElement>(null);
// eslint-disable-next-line no-null/no-null
const headerRef = useRef<HTMLDivElement>(null);
// eslint-disable-next-line no-null/no-null
const sharedCanvasRef = useRef<HTMLCanvasElement>(null);
const [activeSetIndex, setActiveSetIndex] = useState<number>(0);
const sendMessageAction = useSendMessageAction(chat!.id, threadId);
@ -260,12 +263,15 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
index === activeSetIndex && 'activated',
);
const withSharedCanvas = index < STICKER_PICKER_MAX_SHARED_COVERS;
if (stickerSet.id === RECENT_SYMBOL_SET_ID
|| stickerSet.id === FAVORITE_SYMBOL_SET_ID
|| stickerSet.id === CHAT_STICKER_SET_ID
|| stickerSet.id === PREMIUM_STICKER_SET_ID
|| stickerSet.hasThumbnail
|| !firstSticker) {
|| !firstSticker
) {
return (
<Button
key={stickerSet.id}
@ -290,6 +296,7 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
stickerSet={stickerSet as ApiStickerSet}
noAnimate={!canAnimate || !loadAndPlay}
observeIntersection={observeIntersectionForCovers}
sharedCanvasRef={withSharedCanvas ? sharedCanvasRef : undefined}
/>
)}
</Button>
@ -306,6 +313,7 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
observeIntersection={observeIntersectionForCovers}
noContextMenu
isCurrentUserPremium
sharedCanvasRef={withSharedCanvas ? sharedCanvasRef : undefined}
onClick={selectStickerSet}
clickArg={index}
/>
@ -335,7 +343,10 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
ref={headerRef}
className="StickerPicker-header no-selection no-scrollbar"
>
{allSets.map(renderCover)}
<div className="shared-canvas-container">
<canvas ref={sharedCanvasRef} className="shared-canvas" />
{allSets.map(renderCover)}
</div>
</div>
<div
ref={containerRef}

View File

@ -1,7 +1,7 @@
import React, {
memo, useCallback, useMemo, useRef,
} from '../../../lib/teact/teact';
import { getActions } from '../../../global';
import { getActions, getGlobal } from '../../../global';
import type { FC } from '../../../lib/teact/teact';
import type { ApiSticker } from '../../../api/types';
@ -15,7 +15,7 @@ import {
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
import windowSize from '../../../util/windowSize';
import buildClassName from '../../../util/buildClassName';
import { selectIsSetPremium } from '../../../global/selectors';
import { selectIsAlwaysHighPriorityEmoji, selectIsSetPremium } from '../../../global/selectors';
import useLang from '../../../hooks/useLang';
import useFlag from '../../../hooks/useFlag';
@ -68,10 +68,16 @@ const StickerSet: FC<OwnProps> = ({
openPremiumModal,
toggleStickerSet,
} = getActions();
// eslint-disable-next-line no-null/no-null
const ref = useRef<HTMLDivElement>(null);
// eslint-disable-next-line no-null/no-null
const sharedCanvasRef = useRef<HTMLCanvasElement>(null);
// eslint-disable-next-line no-null/no-null
const sharedCanvas2Ref = useRef<HTMLCanvasElement>(null);
const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useFlag();
const [isExpanded, expand] = useFlag(!stickerSet.isEmoji);
const lang = useLang();
useOnIntersect(ref, observeIntersection);
@ -79,6 +85,7 @@ const StickerSet: FC<OwnProps> = ({
const transitionClassNames = useMediaTransition(shouldRender);
const isRecent = stickerSet.id === RECENT_SYMBOL_SET_ID;
const isFavorite = stickerSet.id === FAVORITE_SYMBOL_SET_ID;
const isEmoji = stickerSet.isEmoji;
const isPremiumSet = !isRecent && selectIsSetPremium(stickerSet);
@ -113,11 +120,11 @@ const StickerSet: FC<OwnProps> = ({
? Math.floor((windowSize.get().width - MOBILE_CONTAINER_PADDING) / (itemSize + margin))
: itemsPerRow;
const shouldCutSet = isEmoji && !isExpanded && !stickerSet.installedDate && stickerSet.id !== RECENT_SYMBOL_SET_ID;
const itemsBeforeCutout = shouldCutSet ? stickersPerRow * 3 : Infinity;
const height = Math.ceil((
!shouldCutSet ? stickerSet.count : Math.min(itemsBeforeCutout, stickerSet.count))
/ stickersPerRow) * (itemSize + margin);
const canCut = !stickerSet.installedDate && stickerSet.id !== RECENT_SYMBOL_SET_ID;
const [isCut, , expand] = useFlag(canCut);
const itemsBeforeCutout = stickersPerRow * 3 - 1;
const heightWhenCut = Math.ceil(Math.min(itemsBeforeCutout, stickerSet.count) / stickersPerRow) * (itemSize + margin);
const height = isCut ? heightWhenCut : Math.ceil(stickerSet.count / stickersPerRow) * (itemSize + margin);
const favoriteStickerIdsSet = useMemo(() => (
favoriteStickers ? new Set(favoriteStickers.map(({ id }) => id)) : undefined
@ -154,32 +161,46 @@ const StickerSet: FC<OwnProps> = ({
)}
</div>
<div
className={buildClassName('symbol-set-container', transitionClassNames)}
className={buildClassName('symbol-set-container shared-canvas-container', transitionClassNames)}
style={`height: ${height}px;`}
>
<canvas
ref={sharedCanvasRef}
className="shared-canvas"
style={canCut ? `height: ${heightWhenCut}px;` : undefined}
/>
{(isRecent || isFavorite || canCut) && <canvas ref={sharedCanvas2Ref} className="shared-canvas" />}
{shouldRender && stickerSet.stickers && stickerSet.stickers
.slice(0, !isExpanded ? (itemsBeforeCutout - 1) : stickerSet.stickers.length)
.map((sticker) => (
<StickerButton
key={sticker.id}
sticker={sticker}
size={itemSize}
observeIntersection={observeIntersection}
noAnimate={!loadAndPlay}
isSavedMessages={isSavedMessages}
canViewSet
isCurrentUserPremium={isCurrentUserPremium}
onClick={onStickerSelect}
clickArg={sticker}
onUnfaveClick={stickerSet.id === FAVORITE_SYMBOL_SET_ID && favoriteStickerIdsSet?.has(sticker.id)
? onStickerUnfave : undefined}
onFaveClick={!favoriteStickerIdsSet?.has(sticker.id) ? onStickerFave : undefined}
onRemoveRecentClick={isRecent ? onStickerRemoveRecent : undefined}
/>
))}
{!isExpanded && stickerSet.count > itemsBeforeCutout && (
.slice(0, isCut ? itemsBeforeCutout : stickerSet.stickers.length)
.map((sticker, i) => {
const isHqEmoji = (isRecent || isFavorite)
&& selectIsAlwaysHighPriorityEmoji(getGlobal(), sticker.stickerSetInfo);
const canvasRef = (canCut && i >= itemsBeforeCutout) || isHqEmoji
? sharedCanvas2Ref
: sharedCanvasRef;
return (
<StickerButton
key={sticker.id}
sticker={sticker}
size={itemSize}
observeIntersection={observeIntersection}
noAnimate={!loadAndPlay}
isSavedMessages={isSavedMessages}
canViewSet
isCurrentUserPremium={isCurrentUserPremium}
sharedCanvasRef={canvasRef}
onClick={onStickerSelect}
clickArg={sticker}
onUnfaveClick={isFavorite && favoriteStickerIdsSet?.has(sticker.id) ? onStickerUnfave : undefined}
onFaveClick={!favoriteStickerIdsSet?.has(sticker.id) ? onStickerFave : undefined}
onRemoveRecentClick={isRecent ? onStickerRemoveRecent : undefined}
/>
);
})}
{isCut && stickerSet.count > itemsBeforeCutout && (
<Button className="StickerButton custom-emoji set-expand" round color="translucent" onClick={expand}>
+{stickerSet.count - itemsBeforeCutout + 1}
+{stickerSet.count - itemsBeforeCutout}
</Button>
)}
</div>

View File

@ -1,16 +1,19 @@
import type { FC } from '../../../lib/teact/teact';
import React, { memo, useRef } from '../../../lib/teact/teact';
import { getGlobal } from '../../../global';
import type { ApiStickerSet } from '../../../api/types';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import { STICKER_SIZE_PICKER_HEADER } from '../../../config';
import { selectIsAlwaysHighPriorityEmoji } from '../../../global/selectors';
import { IS_WEBM_SUPPORTED } from '../../../util/environment';
import { getFirstLetters } from '../../../util/textFormat';
import buildClassName from '../../../util/buildClassName';
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
import useMedia from '../../../hooks/useMedia';
import useMediaTransition from '../../../hooks/useMediaTransition';
import useSharedCanvasCoords from '../../../hooks/useSharedCanvasCoords';
import AnimatedSticker from '../../common/AnimatedSticker';
import OptimizedVideo from '../../ui/OptimizedVideo';
@ -22,6 +25,7 @@ type OwnProps = {
size?: number;
noAnimate?: boolean;
observeIntersection: ObserveFn;
sharedCanvasRef?: React.RefObject<HTMLCanvasElement>;
};
const StickerSetCover: FC<OwnProps> = ({
@ -29,20 +33,23 @@ const StickerSetCover: FC<OwnProps> = ({
size = STICKER_SIZE_PICKER_HEADER,
noAnimate,
observeIntersection,
sharedCanvasRef,
}) => {
// eslint-disable-next-line no-null/no-null
const ref = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const { hasThumbnail, isLottie, isVideos: isVideo } = stickerSet;
const isIntersecting = useIsIntersecting(ref, observeIntersection);
const isIntersecting = useIsIntersecting(containerRef, observeIntersection);
const mediaData = useMedia((hasThumbnail || isLottie) && `stickerSet${stickerSet.id}`, !isIntersecting);
const isReady = mediaData && (!isVideo || IS_WEBM_SUPPORTED);
const transitionClassNames = useMediaTransition(isReady);
const sharedCanvasCoords = useSharedCanvasCoords(containerRef, sharedCanvasRef);
return (
<div ref={ref} className="sticker-set-cover">
<div ref={containerRef} className="sticker-set-cover">
{isReady ? (
isLottie ? (
<AnimatedSticker
@ -50,6 +57,9 @@ const StickerSetCover: FC<OwnProps> = ({
tgsUrl={mediaData}
size={size}
play={isIntersecting && !noAnimate}
isLowPriority={!selectIsAlwaysHighPriorityEmoji(getGlobal(), stickerSet)}
sharedCanvas={sharedCanvasRef?.current || undefined}
sharedCanvasCoords={sharedCanvasCoords}
/>
) : isVideo ? (
<OptimizedVideo

View File

@ -38,6 +38,5 @@ export function buildCustomEmojiHtmlFromEntity(rawText: string, entity: ApiMessa
export function getCustomEmojiSize(maxEmojisInLine: number): number | undefined {
if (maxEmojisInLine > EMOJI_SIZES) return undefined;
const size = (6 - (maxEmojisInLine * 0.625)) * REM; // Should be the same as in _message-content.scss
return size;
return (6 - (maxEmojisInLine * 0.625)) * REM; // Should be the same as in _message-content.scss
}

View File

@ -23,6 +23,7 @@
var(--meta-safe-area-base) + var(--meta-safe-author-width) + var(--meta-safe-area-extra-width)
);
--color-voice-transcribe: var(--color-voice-transcribe-button);
--thumbs-background: var(--color-background);
--deleting-translate-x: -50%;
--select-message-scale: 0.9;
@ -201,6 +202,7 @@
--deleting-translate-x: 50%;
--color-text-green: var(--color-accent-own);
--color-voice-transcribe: var(--color-voice-transcribe-button-own);
--thumbs-background: var(--color-background-own);
@media (min-width: 1921px) {
--max-width: 30vw;

View File

@ -74,12 +74,11 @@ import {
areReactionsEmpty,
getMessageHtmlId,
isGeoLiveExpired,
getMessageSingleCustomEmoji,
getMessageSingleCustomEmoji, hasMessageText,
} from '../../../global/helpers';
import buildClassName from '../../../util/buildClassName';
import useEnsureMessage from '../../../hooks/useEnsureMessage';
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
import { renderMessageText } from '../../common/helpers/renderMessageText';
import { calculateDimensionsForMessageMedia, ROUND_VIDEO_DIMENSIONS_PX } from '../../common/helpers/mediaDimensions';
import { buildContentClassName } from './helpers/buildContentClassName';
import { getMinMediaWidth, calculateMediaDimensions } from './helpers/mediaDimensions';
@ -96,7 +95,6 @@ import useOuterHandlers from './hooks/useOuterHandlers';
import useInnerHandlers from './hooks/useInnerHandlers';
import { getServerTime } from '../../../util/serverTime';
import { isElementInViewport } from '../../../util/isElementInViewport';
import { getCustomEmojiSize } from '../composer/helpers/customEmoji';
import Button from '../../ui/Button';
import Avatar from '../../common/Avatar';
@ -130,6 +128,8 @@ import PremiumIcon from '../../common/PremiumIcon';
import FakeIcon from '../../common/FakeIcon';
import './Message.scss';
import MessageText from '../../common/MessageText';
import { getCustomEmojiSize } from '../composer/helpers/customEmoji';
type MessagePositionProperties = {
isFirstInGroup: boolean;
@ -356,15 +356,15 @@ const Message: FC<OwnProps & StateProps> = ({
const isScheduled = messageListType === 'scheduled' || message.isScheduled;
const hasReply = isReplyMessage(message) && !shouldHideReply;
const hasThread = Boolean(threadInfo) && messageListType === 'thread';
const customShape = getMessageCustomShape(message);
const hasAnimatedEmoji = customShape && (animatedEmoji || animatedCustomEmoji);
const isCustomShape = getMessageCustomShape(message);
const hasAnimatedEmoji = isCustomShape && (animatedEmoji || animatedCustomEmoji);
const hasReactions = reactionMessage?.reactions && !areReactionsEmpty(reactionMessage.reactions);
const asForwarded = (
forwardInfo
&& (!isChatWithSelf || isScheduled)
&& !isRepliesChat
&& !forwardInfo.isLinkedChannelPost
&& !customShape
&& !isCustomShape
);
const isAlbum = Boolean(album) && album!.messages.length > 1
&& !album?.messages.some((msg) => Object.keys(msg.content).length === 0);
@ -509,7 +509,7 @@ const Message: FC<OwnProps & StateProps> = ({
const contentClassName = buildContentClassName(message, {
hasReply,
customShape,
isCustomShape,
isLastInGroup,
asForwarded,
hasThread,
@ -522,24 +522,15 @@ const Message: FC<OwnProps & StateProps> = ({
});
const withAppendix = contentClassName.includes('has-appendix');
const hasText = hasMessageText(message);
const emojiSize = message.emojiOnlyCount && getCustomEmojiSize(message.emojiOnlyCount);
const textParts = renderMessageText(
message,
highlight,
emojiSize,
undefined,
undefined,
isProtected,
observeIntersectionForMedia,
observeIntersectionForAnimatedStickers,
);
let metaPosition!: MetaPosition;
if (phoneCall) {
metaPosition = 'none';
} else if (isInDocumentGroupNotLast) {
metaPosition = 'none';
} else if (textParts && !webPage && !hasAnimatedEmoji) {
} else if (hasText && !webPage && !hasAnimatedEmoji) {
metaPosition = 'in-text';
} else {
metaPosition = 'standalone';
@ -549,7 +540,7 @@ const Message: FC<OwnProps & StateProps> = ({
if (areReactionsInMeta) {
reactionsPosition = 'in-meta';
} else if (hasReactions) {
if (customShape || ((photo || video) && !textParts)) {
if (isCustomShape || ((photo || video) && !hasText)) {
reactionsPosition = 'outside';
} else if (asForwarded) {
metaPosition = 'standalone';
@ -686,7 +677,7 @@ const Message: FC<OwnProps & StateProps> = ({
hasReply && 'reply-message',
noMediaCorners && 'no-media-corners',
);
const hasCustomAppendix = isLastInGroup && !textParts && !asForwarded && !hasThread;
const hasCustomAppendix = isLastInGroup && !hasText && !asForwarded && !hasThread;
const textContentClass = buildClassName(
'text-content',
metaPosition === 'in-text' && 'with-meta',
@ -702,7 +693,8 @@ const Message: FC<OwnProps & StateProps> = ({
noUserColors={isOwn}
isProtected={isProtected}
sender={replyMessageSender}
observeIntersection={observeIntersectionForMedia}
observeIntersectionForLoading={observeIntersectionForMedia}
observeIntersectionForPlaying={observeIntersectionForAnimatedStickers}
onClick={handleReplyClick}
/>
)}
@ -877,9 +869,17 @@ const Message: FC<OwnProps & StateProps> = ({
</p>
)}
{!hasAnimatedEmoji && textParts && (
{!hasAnimatedEmoji && hasText && (
<div className={textContentClass} dir="auto">
{textParts}
<MessageText
message={message}
emojiSize={emojiSize}
highlight={highlight}
isProtected={isProtected}
observeIntersectionForLoading={observeIntersectionForMedia}
observeIntersectionForPlaying={observeIntersectionForAnimatedStickers}
withTranslucentThumbs={isCustomShape}
/>
{metaPosition === 'in-text' && renderReactionsAndMeta()}
</div>
)}
@ -925,9 +925,9 @@ const Message: FC<OwnProps & StateProps> = ({
function renderSenderName() {
const media = photo || video || location;
const shouldRender = !(customShape && !viaBotId) && (
const shouldRender = !(isCustomShape && !viaBotId) && (
(withSenderName && !media) || asForwarded || viaBotId || forceSenderName
) && !isInDocumentGroupNotFirst && !(hasReply && customShape);
) && !isInDocumentGroupNotFirst && !(hasReply && isCustomShape);
if (!shouldRender) {
return undefined;
@ -935,7 +935,7 @@ const Message: FC<OwnProps & StateProps> = ({
let senderTitle;
let senderColor;
if (senderPeer && !(customShape && viaBotId)) {
if (senderPeer && !(isCustomShape && viaBotId)) {
senderTitle = getSenderTitle(lang, senderPeer);
if (!asForwarded) {

View File

@ -7,7 +7,7 @@ export function buildContentClassName(
message: ApiMessage,
{
hasReply,
customShape,
isCustomShape,
isLastInGroup,
asForwarded,
hasThread,
@ -19,7 +19,7 @@ export function buildContentClassName(
withVoiceTranscription,
}: {
hasReply?: boolean;
customShape?: boolean | number;
isCustomShape?: boolean | number;
isLastInGroup?: boolean;
asForwarded?: boolean;
hasThread?: boolean;
@ -54,7 +54,7 @@ export function buildContentClassName(
classNames.push('has-action-button');
}
if (customShape) {
if (isCustomShape) {
classNames.push('custom-shape');
if (video?.isRound) {
classNames.push('round');
@ -115,7 +115,7 @@ export function buildContentClassName(
classNames.push('force-sender-name');
}
if (!customShape) {
if (!isCustomShape) {
classNames.push('has-shadow');
if (isMedia && hasComments) {

View File

@ -74,7 +74,7 @@ export function getMessageCopyOptions(
document.execCommand('copy');
} else {
const clipboardText = renderMessageText(
message, undefined, undefined, undefined, undefined, undefined, undefined, undefined, true,
message, undefined, undefined, undefined, undefined, undefined, true,
);
if (clipboardText) copyHtmlToClipboard(clipboardText.join(''), getMessageTextWithSpoilers(message)!);
}

View File

@ -1,6 +1,6 @@
import type { FC } from '../../lib/teact/teact';
import React, {
useEffect, memo, useMemo, useCallback,
useEffect, memo, useMemo, useCallback, useRef,
} from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global';
@ -36,6 +36,9 @@ const StickerSetResult: FC<OwnProps & StateProps> = ({
}) => {
const { loadStickers, toggleStickerSet, openStickerSet } = getActions();
// eslint-disable-next-line no-null/no-null
const sharedCanvasRef = useRef<HTMLCanvasElement>(null);
const lang = useLang();
const isAdded = set && Boolean(set.installedDate);
const areStickersLoaded = Boolean(set?.stickers);
@ -96,7 +99,8 @@ const StickerSetResult: FC<OwnProps & StateProps> = ({
{lang(isAdded ? 'Stickers.Installed' : 'Stickers.Install')}
</Button>
</div>
<div className="sticker-set-main">
<div className="sticker-set-main shared-canvas-container">
<canvas ref={sharedCanvasRef} className="shared-canvas" />
{!canRenderStickers && <Spinner />}
{canRenderStickers && displayedStickers.map((sticker) => (
<StickerButton
@ -108,6 +112,7 @@ const StickerSetResult: FC<OwnProps & StateProps> = ({
onClick={handleStickerClick}
noContextMenu
isCurrentUserPremium={isCurrentUserPremium}
sharedCanvasRef={sharedCanvasRef}
/>
))}
</div>

View File

@ -142,6 +142,7 @@ export const EMOJI_SIZE_PICKER = 40;
export const COMPOSER_EMOJI_SIZE_PICKER = 32;
export const STICKER_SIZE_GENERAL_SETTINGS = 48;
export const STICKER_SIZE_PICKER_HEADER = 32;
export const STICKER_PICKER_MAX_SHARED_COVERS = 20;
export const STICKER_SIZE_SEARCH = 64;
export const STICKER_SIZE_MODAL = 64;
export const EMOJI_SIZE_MODAL = 40;

View File

@ -1,8 +1,8 @@
import type { TeactNode } from '../../lib/teact/teact';
import type { ApiMessage } from '../../api/types';
import { ApiMessageEntityTypes } from '../../api/types';
import { CONTENT_NOT_SUPPORTED } from '../../config';
import type { TextPart } from '../../types';
import type { LangFn } from '../../hooks/useLang';
import trimText from '../../util/trimText';
@ -108,7 +108,7 @@ export function getMessageSummaryEmoji(message: ApiMessage, noReactions = true)
export function getMessageSummaryDescription(
lang: LangFn,
message: ApiMessage,
truncatedText?: string | TextPart[],
truncatedText?: string | TeactNode,
noReactions = true,
isExtended = false,
) {
@ -127,7 +127,7 @@ export function getMessageSummaryDescription(
game,
} = message.content;
let summary: string | TextPart[] | undefined;
let summary: string | TeactNode | undefined;
if (message.groupedId) {
summary = truncatedText || lang('lng_in_dlg_album');

View File

@ -50,22 +50,20 @@ export function getMessageTranscription(message: ApiMessage) {
return transcriptionId && global.transcriptions[transcriptionId]?.text;
}
export function getMessageText(message: ApiMessage) {
export function hasMessageText(message: ApiMessage) {
const {
text, sticker, photo, video, audio, voice, document, poll, webPage, contact, invoice, location,
game, action,
} = message.content;
if (text) {
return text.text;
}
return Boolean(text) || !(
sticker || photo || video || audio || voice || document || contact || poll || webPage || invoice || location
|| game || action?.phoneCall
);
}
if (sticker || photo || video || audio || voice || document
|| contact || poll || webPage || invoice || location || game || action?.phoneCall) {
return undefined;
}
return CONTENT_NOT_SUPPORTED;
export function getMessageText(message: ApiMessage) {
return hasMessageText(message) ? message.content.text?.text || CONTENT_NOT_SUPPORTED : undefined;
}
export function getMessageCustomShape(message: ApiMessage): boolean {

View File

@ -10,7 +10,7 @@ export function renderMessageSummaryHtml(
const emoji = getMessageSummaryEmoji(message);
const emojiWithSpace = emoji ? `${emoji} ` : '';
const text = renderMessageText(
message, undefined, undefined, undefined, undefined, undefined, undefined, undefined, true,
message, undefined, undefined, undefined, undefined, undefined, true,
)?.join('');
const description = getMessageSummaryDescription(lang, message, text, true, true);

View File

@ -139,10 +139,11 @@ export function selectLocalAnimatedEmojiEffectByName(name: string) {
return name === 'Cumshot' ? '🍆' : undefined;
}
export function selectIsDefaultEmojiStatusPack(global: GlobalState, pack: ApiStickerSetInfo) {
return 'id' in pack && pack.id === global.appConfig?.defaultEmojiStatusesStickerSetId;
export function selectIsDefaultEmojiStatusPack(global: GlobalState, stickerSet: ApiStickerSetInfo | ApiStickerSet) {
return 'id' in stickerSet && stickerSet.id === global.appConfig?.defaultEmojiStatusesStickerSetId;
}
export function selectIsAlwaysHighPriorityEmoji(global: GlobalState, pack: ApiStickerSetInfo) {
return selectIsDefaultEmojiStatusPack(global, pack) || ('id' in pack && pack.id === RESTRICTED_EMOJI_SET_ID);
export function selectIsAlwaysHighPriorityEmoji(global: GlobalState, stickerSet: ApiStickerSetInfo | ApiStickerSet) {
return selectIsDefaultEmojiStatusPack(global, stickerSet)
|| ('id' in stickerSet && stickerSet.id === RESTRICTED_EMOJI_SET_ID);
}

View File

@ -0,0 +1,26 @@
import { useEffect, useMemo, useState } from '../lib/teact/teact';
export default function useSharedCanvasCoords(
containerRef: React.RefObject<HTMLDivElement>,
sharedCanvasRef?: React.RefObject<HTMLCanvasElement>,
) {
const [x, setX] = useState<number>();
const [y, setY] = useState<number>();
useEffect(() => {
if (!sharedCanvasRef?.current) {
return;
}
const container = containerRef.current!;
const target = container.classList.contains('sticker-set-cover') ? container : container.querySelector('img')!;
const targetBounds = target.getBoundingClientRect();
const canvasBounds = sharedCanvasRef!.current!.getBoundingClientRect();
// Factor coords are used to support rendering while being rescaled (e.g. message appearance animation)
setX((targetBounds.left - canvasBounds.left) / canvasBounds.width);
setY((targetBounds.top - canvasBounds.top) / canvasBounds.height);
}, [containerRef, sharedCanvasRef]);
return useMemo(() => (x !== undefined && y !== undefined ? { x, y } : undefined), [x, y]);
}

View File

@ -13,6 +13,7 @@ interface Params {
size?: number;
quality?: number;
isLowPriority?: boolean;
coords?: { x: number; y: number };
}
type Frames = ArrayBuffer[];
@ -36,11 +37,13 @@ let lastWorkerIndex = -1;
class RLottie {
// Config
private containers = new Map<HTMLDivElement, {
private containers = new Map<string, {
canvas: HTMLCanvasElement;
ctx: CanvasRenderingContext2D;
isLoaded?: boolean;
isPaused?: boolean;
isSharedCanvas?: boolean;
coords?: Params['coords'];
onLoad?: NoneToVoidFunction;
}>();
@ -87,7 +90,7 @@ class RLottie {
private lastRenderAt?: number;
static init(...args: ConstructorParameters<typeof RLottie>) {
const [container, onLoad, id] = args;
const [container, canvas, onLoad, id, , params] = args;
let instance = instancesById.get(id);
if (!instance) {
@ -95,14 +98,15 @@ class RLottie {
instance = new RLottie(...args);
instancesById.set(id, instance);
} else {
instance.addContainer(container, onLoad);
instance.addContainer(container, canvas, onLoad, params?.coords);
}
return instance;
}
constructor(
container: HTMLDivElement,
containerId: string,
container: HTMLDivElement | HTMLCanvasElement,
onLoad: NoneToVoidFunction | undefined,
private id: string,
private tgsUrl: string,
@ -111,14 +115,18 @@ class RLottie {
private onEnded?: (isDestroyed?: boolean) => void,
private onLoop?: () => void,
) {
this.addContainer(container, onLoad);
this.addContainer(containerId, container, onLoad, params.coords);
this.initConfig();
this.initRenderer();
}
public removeContainer(container: HTMLDivElement) {
this.containers.get(container)!.canvas.remove();
this.containers.delete(container);
public removeContainer(containerId: string) {
const containerData = this.containers.get(containerId)!;
if (!containerData.isSharedCanvas) {
this.containers.get(containerId)!.canvas.remove();
}
this.containers.delete(containerId);
if (!this.containers.size) {
this.destroy();
@ -129,9 +137,9 @@ class RLottie {
return this.isAnimating || this.isWaiting;
}
play(forceRestart = false, container?: HTMLDivElement) {
if (container) {
this.containers.get(container)!.isPaused = false;
play(forceRestart = false, containerId?: string) {
if (containerId) {
this.containers.get(containerId)!.isPaused = false;
}
if (this.isEnded && forceRestart) {
@ -143,9 +151,9 @@ class RLottie {
this.doPlay();
}
pause(container?: HTMLDivElement) {
if (container) {
this.containers.get(container)!.isPaused = true;
pause(containerId?: string) {
if (containerId) {
this.containers.get(containerId)!.isPaused = true;
const areAllContainersPaused = Array.from(this.containers.values()).every(({ isPaused }) => isPaused);
if (!areAllContainersPaused) {
@ -178,47 +186,85 @@ class RLottie {
this.params.noLoop = noLoop;
}
private addContainer(container: HTMLDivElement, onLoad?: NoneToVoidFunction) {
if (!(container.parentNode instanceof HTMLElement)) {
throw new Error('[RLottie] Container is not mounted');
}
private addContainer(
containerId: string,
container: HTMLDivElement | HTMLCanvasElement,
onLoad?: NoneToVoidFunction,
coords?: Params['coords'],
) {
const { isLowPriority, quality = isLowPriority ? LOW_PRIORITY_QUALITY : HIGH_PRIORITY_QUALITY } = this.params;
let imgSize: number;
// Reduced quality only looks acceptable on high DPR screens
const sizeFactor = Math.max(DPR * quality, 1);
let { size } = this.params;
if (container instanceof HTMLDivElement) {
if (!(container.parentNode instanceof HTMLElement)) {
throw new Error('[RLottie] Container is not mounted');
}
if (!size) {
size = (
container.offsetWidth
|| parseInt(container.style.width, 10)
|| container.parentNode.offsetWidth
);
let { size } = this.params;
if (!size) {
throw new Error('[RLottie] Failed to detect width from container');
size = (
container.offsetWidth
|| parseInt(container.style.width, 10)
|| container.parentNode.offsetWidth
);
if (!size) {
throw new Error('[RLottie] Failed to detect width from container');
}
}
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d')!;
canvas.style.width = `${size}px`;
canvas.style.height = `${size}px`;
imgSize = Math.round(size * sizeFactor);
canvas.width = imgSize;
canvas.height = imgSize;
container.appendChild(canvas);
this.containers.set(containerId, {
canvas, ctx, onLoad,
});
} else {
if (!container.offsetParent) {
throw new Error('[RLottie] Shared canvas is not mounted');
}
const canvas = container;
const ctx = canvas.getContext('2d')!;
imgSize = Math.round(this.params.size! * sizeFactor);
const expectedWidth = Math.round(canvas.offsetWidth * sizeFactor);
const expectedHeight = Math.round(canvas.offsetHeight * sizeFactor);
if (canvas.width !== expectedWidth || canvas.height !== expectedHeight) {
canvas.width = expectedWidth;
canvas.height = expectedHeight;
}
this.containers.set(containerId, {
canvas,
ctx,
isSharedCanvas: true,
coords: {
x: Math.round((coords?.x || 0) * canvas.width),
y: Math.round((coords?.y || 0) * canvas.height),
},
onLoad,
});
}
const canvas = document.createElement('canvas');
canvas.dataset.id = this.id;
const ctx = canvas.getContext('2d')!;
canvas.style.width = `${size}px`;
canvas.style.height = `${size}px`;
const { isLowPriority, quality = isLowPriority ? LOW_PRIORITY_QUALITY : HIGH_PRIORITY_QUALITY } = this.params;
// Reduced quality only looks acceptable on high DPR screens
const imgSize = Math.round(size * Math.max(DPR * quality, 1));
canvas.width = imgSize;
canvas.height = imgSize;
container.appendChild(canvas);
if (!this.imgSize) {
this.imgSize = imgSize;
}
this.containers.set(container, { canvas, ctx, onLoad });
if (this.isRendererInited) {
this.doPlay();
}
@ -371,15 +417,16 @@ class RLottie {
/* eslint-enable prefer-destructuring */
}
}
const imageData = new ImageData(arr, this.imgSize, this.imgSize);
this.containers.forEach((containerData) => {
const {
ctx, isLoaded, isPaused, onLoad,
ctx, isLoaded, isPaused, coords: { x, y } = {}, onLoad,
} = containerData;
if (!isLoaded || !isPaused) {
ctx.putImageData(imageData, 0, 0);
ctx.putImageData(imageData, x || 0, y || 0);
}
if (!isLoaded) {

View File

@ -286,6 +286,20 @@ div[role="button"] {
left: 0;
}
.shared-canvas-container {
position: relative;
}
.shared-canvas {
position: absolute;
top: 0;
left: 0;
z-index: 1;
width: 100%;
height: 100%;
pointer-events: none;
}
@keyframes grow-icon {
0% {
transform: scale(0.5);