[Perf] Sticker View: Introduce shared canvas
This commit is contained in:
parent
11196c8d94
commit
a5cda0f209
@ -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}
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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>
|
||||
|
||||
83
src/components/common/MessageSummary.tsx
Normal file
83
src/components/common/MessageSummary.tsx
Normal 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);
|
||||
79
src/components/common/MessageText.tsx
Normal file
79
src/components/common/MessageText.tsx
Normal 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);
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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 [
|
||||
|
||||
@ -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];
|
||||
}
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
.Chat {
|
||||
--background-color: var(--color-background);
|
||||
--thumbs-background: var(--background-color);
|
||||
|
||||
body.is-ios &,
|
||||
body.is-macos & {
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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)!);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
26
src/hooks/useSharedCanvasCoords.ts
Normal file
26
src/hooks/useSharedCanvasCoords.ts
Normal 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]);
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user