[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 type { FC } from '../../lib/teact/teact';
|
||||||
|
|
||||||
import React, {
|
import React, {
|
||||||
useEffect, useRef, memo, useCallback, useState,
|
useEffect, useRef, memo, useCallback, useState, useMemo,
|
||||||
} from '../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
|
|
||||||
import { fastRaf } from '../../util/schedulers';
|
import { fastRaf } from '../../util/schedulers';
|
||||||
@ -16,7 +16,7 @@ import generateIdFor from '../../util/generateIdFor';
|
|||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
ref?: RefObject<HTMLDivElement>;
|
ref?: RefObject<HTMLDivElement>;
|
||||||
id?: string;
|
animationId?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
style?: string;
|
style?: string;
|
||||||
tgsUrl?: string;
|
tgsUrl?: string;
|
||||||
@ -26,9 +26,11 @@ export type OwnProps = {
|
|||||||
noLoop?: boolean;
|
noLoop?: boolean;
|
||||||
size: number;
|
size: number;
|
||||||
quality?: number;
|
quality?: number;
|
||||||
|
color?: [number, number, number];
|
||||||
isLowPriority?: boolean;
|
isLowPriority?: boolean;
|
||||||
forceOnHeavyAnimation?: boolean;
|
forceOnHeavyAnimation?: boolean;
|
||||||
color?: [number, number, number];
|
sharedCanvas?: HTMLCanvasElement;
|
||||||
|
sharedCanvasCoords?: { x: number; y: number };
|
||||||
onClick?: NoneToVoidFunction;
|
onClick?: NoneToVoidFunction;
|
||||||
onLoad?: NoneToVoidFunction;
|
onLoad?: NoneToVoidFunction;
|
||||||
onEnded?: NoneToVoidFunction;
|
onEnded?: NoneToVoidFunction;
|
||||||
@ -57,7 +59,7 @@ setTimeout(ensureLottie, LOTTIE_LOAD_DELAY);
|
|||||||
|
|
||||||
const AnimatedSticker: FC<OwnProps> = ({
|
const AnimatedSticker: FC<OwnProps> = ({
|
||||||
ref,
|
ref,
|
||||||
id,
|
animationId,
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
tgsUrl,
|
tgsUrl,
|
||||||
@ -70,6 +72,8 @@ const AnimatedSticker: FC<OwnProps> = ({
|
|||||||
isLowPriority,
|
isLowPriority,
|
||||||
color,
|
color,
|
||||||
forceOnHeavyAnimation,
|
forceOnHeavyAnimation,
|
||||||
|
sharedCanvas,
|
||||||
|
sharedCanvasCoords,
|
||||||
onClick,
|
onClick,
|
||||||
onLoad,
|
onLoad,
|
||||||
onEnded,
|
onEnded,
|
||||||
@ -81,6 +85,8 @@ const AnimatedSticker: FC<OwnProps> = ({
|
|||||||
containerRef = ref;
|
containerRef = ref;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const containerId = useMemo(() => generateIdFor(ID_STORE, true), []);
|
||||||
|
|
||||||
const [animation, setAnimation] = useState<RLottieInstance>();
|
const [animation, setAnimation] = useState<RLottieInstance>();
|
||||||
const wasPlaying = useRef(false);
|
const wasPlaying = useRef(false);
|
||||||
const isFrozen = useRef(false);
|
const isFrozen = useRef(false);
|
||||||
@ -92,25 +98,28 @@ const AnimatedSticker: FC<OwnProps> = ({
|
|||||||
playSegmentRef.current = playSegment;
|
playSegmentRef.current = playSegment;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (animation || !tgsUrl) {
|
if (animation || !tgsUrl || (sharedCanvas && !sharedCanvasCoords)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const exec = () => {
|
const exec = () => {
|
||||||
if (!containerRef.current) {
|
const container = containerRef.current || sharedCanvas;
|
||||||
|
if (!container) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newAnimation = RLottie.init(
|
const newAnimation = RLottie.init(
|
||||||
containerRef.current,
|
containerId,
|
||||||
|
container,
|
||||||
onLoad,
|
onLoad,
|
||||||
id || generateIdFor(ID_STORE, true),
|
animationId || generateIdFor(ID_STORE, true),
|
||||||
tgsUrl,
|
tgsUrl,
|
||||||
{
|
{
|
||||||
noLoop,
|
noLoop,
|
||||||
size,
|
size,
|
||||||
quality,
|
quality,
|
||||||
isLowPriority,
|
isLowPriority,
|
||||||
|
coords: sharedCanvasCoords,
|
||||||
},
|
},
|
||||||
color,
|
color,
|
||||||
onEnded,
|
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(() => {
|
useEffect(() => {
|
||||||
if (!animation) return;
|
if (!animation) return;
|
||||||
@ -144,32 +156,30 @@ const AnimatedSticker: FC<OwnProps> = ({
|
|||||||
}, [color, animation]);
|
}, [color, animation]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const container = containerRef.current!;
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (animation) {
|
if (animation) {
|
||||||
animation.removeContainer(container);
|
animation.removeContainer(containerId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [animation]);
|
}, [animation, containerId]);
|
||||||
|
|
||||||
const playAnimation = useCallback((shouldRestart = false) => {
|
const playAnimation = useCallback((shouldRestart = false) => {
|
||||||
if (animation && (playRef.current || playSegmentRef.current)) {
|
if (animation && (playRef.current || playSegmentRef.current)) {
|
||||||
if (playSegmentRef.current) {
|
if (playSegmentRef.current) {
|
||||||
animation.playSegment(playSegmentRef.current);
|
animation.playSegment(playSegmentRef.current);
|
||||||
} else {
|
} else {
|
||||||
animation.play(shouldRestart, containerRef.current!);
|
animation.play(shouldRestart, containerId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [animation]);
|
}, [animation, containerId]);
|
||||||
|
|
||||||
const pauseAnimation = useCallback(() => {
|
const pauseAnimation = useCallback(() => {
|
||||||
if (!animation) {
|
if (!animation) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
animation.pause(containerRef.current!);
|
animation.pause(containerId);
|
||||||
}, [animation]);
|
}, [animation, containerId]);
|
||||||
|
|
||||||
const freezeAnimation = useCallback(() => {
|
const freezeAnimation = useCallback(() => {
|
||||||
isFrozen.current = true;
|
isFrozen.current = true;
|
||||||
@ -241,6 +251,10 @@ const AnimatedSticker: FC<OwnProps> = ({
|
|||||||
// then we can play again.
|
// then we can play again.
|
||||||
useBackgroundMode(freezeAnimation, unfreezeAnimationOnRaf);
|
useBackgroundMode(freezeAnimation, unfreezeAnimationOnRaf);
|
||||||
|
|
||||||
|
if (sharedCanvas) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import { getPropertyHexColor } from '../../util/themeStyle';
|
|||||||
import { hexToRgb } from '../../util/switchTheme';
|
import { hexToRgb } from '../../util/switchTheme';
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
import safePlay from '../../util/safePlay';
|
import safePlay from '../../util/safePlay';
|
||||||
import { selectIsDefaultEmojiStatusPack } from '../../global/selectors';
|
import { selectIsAlwaysHighPriorityEmoji, selectIsDefaultEmojiStatusPack } from '../../global/selectors';
|
||||||
|
|
||||||
import useEnsureCustomEmoji from '../../hooks/useEnsureCustomEmoji';
|
import useEnsureCustomEmoji from '../../hooks/useEnsureCustomEmoji';
|
||||||
import useCustomEmoji from './hooks/useCustomEmoji';
|
import useCustomEmoji from './hooks/useCustomEmoji';
|
||||||
@ -31,8 +31,11 @@ type OwnProps = {
|
|||||||
className?: string;
|
className?: string;
|
||||||
loopLimit?: number;
|
loopLimit?: number;
|
||||||
style?: string;
|
style?: string;
|
||||||
withSharedAnimation?: boolean;
|
|
||||||
withGridFix?: boolean;
|
withGridFix?: boolean;
|
||||||
|
withSharedAnimation?: boolean;
|
||||||
|
sharedCanvasRef?: React.RefObject<HTMLCanvasElement>;
|
||||||
|
sharedCanvasHqRef?: React.RefObject<HTMLCanvasElement>;
|
||||||
|
withTranslucentThumb?: boolean;
|
||||||
shouldPreloadPreview?: boolean;
|
shouldPreloadPreview?: boolean;
|
||||||
forceOnHeavyAnimation?: boolean;
|
forceOnHeavyAnimation?: boolean;
|
||||||
observeIntersectionForLoading?: ObserveFn;
|
observeIntersectionForLoading?: ObserveFn;
|
||||||
@ -40,7 +43,7 @@ type OwnProps = {
|
|||||||
onClick?: NoneToVoidFunction;
|
onClick?: NoneToVoidFunction;
|
||||||
};
|
};
|
||||||
|
|
||||||
const STICKER_SIZE = 24;
|
const STICKER_SIZE = 20;
|
||||||
|
|
||||||
const CustomEmoji: FC<OwnProps> = ({
|
const CustomEmoji: FC<OwnProps> = ({
|
||||||
ref,
|
ref,
|
||||||
@ -50,9 +53,12 @@ const CustomEmoji: FC<OwnProps> = ({
|
|||||||
loopLimit,
|
loopLimit,
|
||||||
style,
|
style,
|
||||||
withGridFix,
|
withGridFix,
|
||||||
|
withSharedAnimation,
|
||||||
|
sharedCanvasRef,
|
||||||
|
sharedCanvasHqRef,
|
||||||
|
withTranslucentThumb,
|
||||||
shouldPreloadPreview,
|
shouldPreloadPreview,
|
||||||
forceOnHeavyAnimation,
|
forceOnHeavyAnimation,
|
||||||
withSharedAnimation,
|
|
||||||
observeIntersectionForLoading,
|
observeIntersectionForLoading,
|
||||||
observeIntersectionForPlaying,
|
observeIntersectionForPlaying,
|
||||||
onClick,
|
onClick,
|
||||||
@ -121,6 +127,8 @@ const CustomEmoji: FC<OwnProps> = ({
|
|||||||
}
|
}
|
||||||
}, [loopLimit]);
|
}, [loopLimit]);
|
||||||
|
|
||||||
|
const isHq = customEmoji?.stickerSetInfo && selectIsAlwaysHighPriorityEmoji(getGlobal(), customEmoji.stickerSetInfo);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
@ -157,6 +165,8 @@ const CustomEmoji: FC<OwnProps> = ({
|
|||||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||||
withSharedAnimation={withSharedAnimation}
|
withSharedAnimation={withSharedAnimation}
|
||||||
|
sharedCanvasRef={isHq ? sharedCanvasHqRef : sharedCanvasRef}
|
||||||
|
withTranslucentThumb={withTranslucentThumb}
|
||||||
onVideoEnded={handleVideoEnded}
|
onVideoEnded={handleVideoEnded}
|
||||||
onAnimatedStickerLoop={handleStickerLoop}
|
onAnimatedStickerLoop={handleStickerLoop}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -13,7 +13,6 @@ import {
|
|||||||
import renderText from './helpers/renderText';
|
import renderText from './helpers/renderText';
|
||||||
import { getPictogramDimensions } from './helpers/mediaDimensions';
|
import { getPictogramDimensions } from './helpers/mediaDimensions';
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
import { renderMessageSummary } from './helpers/renderMessageText';
|
|
||||||
|
|
||||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||||
import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
||||||
@ -22,11 +21,11 @@ import useThumbnail from '../../hooks/useThumbnail';
|
|||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
|
|
||||||
import ActionMessage from '../middle/ActionMessage';
|
import ActionMessage from '../middle/ActionMessage';
|
||||||
|
import MessageSummary from './MessageSummary';
|
||||||
|
|
||||||
import './EmbeddedMessage.scss';
|
import './EmbeddedMessage.scss';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
observeIntersection?: ObserveFn;
|
|
||||||
className?: string;
|
className?: string;
|
||||||
message?: ApiMessage;
|
message?: ApiMessage;
|
||||||
sender?: ApiUser | ApiChat;
|
sender?: ApiUser | ApiChat;
|
||||||
@ -35,6 +34,8 @@ type OwnProps = {
|
|||||||
noUserColors?: boolean;
|
noUserColors?: boolean;
|
||||||
isProtected?: boolean;
|
isProtected?: boolean;
|
||||||
hasContextMenu?: boolean;
|
hasContextMenu?: boolean;
|
||||||
|
observeIntersectionForLoading?: ObserveFn;
|
||||||
|
observeIntersectionForPlaying?: ObserveFn;
|
||||||
onClick: NoneToVoidFunction;
|
onClick: NoneToVoidFunction;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -49,12 +50,13 @@ const EmbeddedMessage: FC<OwnProps> = ({
|
|||||||
isProtected,
|
isProtected,
|
||||||
noUserColors,
|
noUserColors,
|
||||||
hasContextMenu,
|
hasContextMenu,
|
||||||
observeIntersection,
|
observeIntersectionForLoading,
|
||||||
|
observeIntersectionForPlaying,
|
||||||
onClick,
|
onClick,
|
||||||
}) => {
|
}) => {
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const ref = useRef<HTMLDivElement>(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 mediaBlobUrl = useMedia(message && getMessageMediaHash(message, 'pictogram'), !isIntersecting);
|
||||||
const mediaThumbnail = useThumbnail(message);
|
const mediaThumbnail = useThumbnail(message);
|
||||||
@ -80,9 +82,20 @@ const EmbeddedMessage: FC<OwnProps> = ({
|
|||||||
{!message ? (
|
{!message ? (
|
||||||
customText || NBSP
|
customText || NBSP
|
||||||
) : isActionMessage(message) ? (
|
) : 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>
|
</p>
|
||||||
<div className="message-title" dir="auto">{renderText(senderTitle || title || NBSP)}</div>
|
<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;
|
isSavedMessages?: boolean;
|
||||||
canViewSet?: boolean;
|
canViewSet?: boolean;
|
||||||
isCurrentUserPremium?: boolean;
|
isCurrentUserPremium?: boolean;
|
||||||
|
sharedCanvasRef?: React.RefObject<HTMLCanvasElement>;
|
||||||
observeIntersection: ObserveFn;
|
observeIntersection: ObserveFn;
|
||||||
onClick?: (arg: OwnProps<T>['clickArg'], isSilent?: boolean, shouldSchedule?: boolean) => void;
|
onClick?: (arg: OwnProps<T>['clickArg'], isSilent?: boolean, shouldSchedule?: boolean) => void;
|
||||||
clickArg: T;
|
clickArg: T;
|
||||||
@ -47,16 +48,17 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
|||||||
noAnimate,
|
noAnimate,
|
||||||
title,
|
title,
|
||||||
className,
|
className,
|
||||||
clickArg,
|
|
||||||
noContextMenu,
|
noContextMenu,
|
||||||
isSavedMessages,
|
isSavedMessages,
|
||||||
canViewSet,
|
canViewSet,
|
||||||
observeIntersection,
|
observeIntersection,
|
||||||
|
isCurrentUserPremium,
|
||||||
|
sharedCanvasRef,
|
||||||
onClick,
|
onClick,
|
||||||
|
clickArg,
|
||||||
onFaveClick,
|
onFaveClick,
|
||||||
onUnfaveClick,
|
onUnfaveClick,
|
||||||
onRemoveRecentClick,
|
onRemoveRecentClick,
|
||||||
isCurrentUserPremium,
|
|
||||||
}: OwnProps<T>) => {
|
}: OwnProps<T>) => {
|
||||||
const { openStickerSet, openPremiumModal } = getActions();
|
const { openStickerSet, openPremiumModal } = getActions();
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
@ -231,6 +233,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
|||||||
noLoad={!shouldLoad}
|
noLoad={!shouldLoad}
|
||||||
noPlay={!shouldPlay}
|
noPlay={!shouldPlay}
|
||||||
withSharedAnimation
|
withSharedAnimation
|
||||||
|
sharedCanvasRef={sharedCanvasRef}
|
||||||
/>
|
/>
|
||||||
{isLocked && (
|
{isLocked && (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -71,6 +71,9 @@ const StickerSetModal: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const sharedCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
const prevStickerSet = usePrevious(stickerSet);
|
const prevStickerSet = usePrevious(stickerSet);
|
||||||
@ -148,17 +151,21 @@ const StickerSetModal: FC<OwnProps & StateProps> = ({
|
|||||||
{renderingStickerSet?.stickers ? (
|
{renderingStickerSet?.stickers ? (
|
||||||
<>
|
<>
|
||||||
<div ref={containerRef} className="stickers custom-scroll">
|
<div ref={containerRef} className="stickers custom-scroll">
|
||||||
{renderingStickerSet.stickers.map((sticker) => (
|
<div className="shared-canvas-container">
|
||||||
<StickerButton
|
<canvas ref={sharedCanvasRef} className="shared-canvas" />
|
||||||
sticker={sticker}
|
{renderingStickerSet.stickers.map((sticker) => (
|
||||||
size={isEmoji ? EMOJI_SIZE_MODAL : STICKER_SIZE_MODAL}
|
<StickerButton
|
||||||
observeIntersection={observeIntersection}
|
sticker={sticker}
|
||||||
onClick={canSendStickers && !isEmoji ? handleSelect : undefined}
|
size={isEmoji ? EMOJI_SIZE_MODAL : STICKER_SIZE_MODAL}
|
||||||
clickArg={sticker}
|
observeIntersection={observeIntersection}
|
||||||
isSavedMessages={isSavedMessages}
|
onClick={canSendStickers && !isEmoji ? handleSelect : undefined}
|
||||||
isCurrentUserPremium={isCurrentUserPremium}
|
clickArg={sticker}
|
||||||
/>
|
isSavedMessages={isSavedMessages}
|
||||||
))}
|
isCurrentUserPremium={isCurrentUserPremium}
|
||||||
|
sharedCanvasRef={sharedCanvasRef}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="button-wrapper">
|
<div className="button-wrapper">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -1,10 +1,20 @@
|
|||||||
|
:root {
|
||||||
|
--thumbs-background: var(--color-background);
|
||||||
|
}
|
||||||
|
|
||||||
.thumb {
|
.thumb {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
&:global(.closing) {
|
&:global(.closing) {
|
||||||
transition-delay: 150ms;
|
transition-delay: 150ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&_opaque {
|
||||||
|
background: var(--thumbs-background);
|
||||||
|
transition-delay: 0s;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.noTransition {
|
.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 { getGlobal } from '../../global';
|
||||||
|
|
||||||
import type { FC } from '../../lib/teact/teact';
|
import type { FC } from '../../lib/teact/teact';
|
||||||
@ -17,6 +17,7 @@ import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
|||||||
import useThumbnail from '../../hooks/useThumbnail';
|
import useThumbnail from '../../hooks/useThumbnail';
|
||||||
import useMediaTransition from '../../hooks/useMediaTransition';
|
import useMediaTransition from '../../hooks/useMediaTransition';
|
||||||
import useFlag from '../../hooks/useFlag';
|
import useFlag from '../../hooks/useFlag';
|
||||||
|
import useSharedCanvasCoords from '../../hooks/useSharedCanvasCoords';
|
||||||
|
|
||||||
import AnimatedSticker from './AnimatedSticker';
|
import AnimatedSticker from './AnimatedSticker';
|
||||||
import OptimizedVideo from '../ui/OptimizedVideo';
|
import OptimizedVideo from '../ui/OptimizedVideo';
|
||||||
@ -41,6 +42,8 @@ type OwnProps = {
|
|||||||
noLoad?: boolean;
|
noLoad?: boolean;
|
||||||
noPlay?: boolean;
|
noPlay?: boolean;
|
||||||
withSharedAnimation?: boolean;
|
withSharedAnimation?: boolean;
|
||||||
|
sharedCanvasRef?: React.RefObject<HTMLCanvasElement>;
|
||||||
|
withTranslucentThumb?: boolean; // With shared canvas thumbs are opaque by default to provide better transition effect
|
||||||
cacheBuster?: number;
|
cacheBuster?: number;
|
||||||
onVideoEnded?: AnyToVoidFunction;
|
onVideoEnded?: AnyToVoidFunction;
|
||||||
onAnimatedStickerLoop?: AnyToVoidFunction;
|
onAnimatedStickerLoop?: AnyToVoidFunction;
|
||||||
@ -68,6 +71,8 @@ const StickerView: FC<OwnProps> = ({
|
|||||||
noLoad,
|
noLoad,
|
||||||
noPlay,
|
noPlay,
|
||||||
withSharedAnimation,
|
withSharedAnimation,
|
||||||
|
withTranslucentThumb,
|
||||||
|
sharedCanvasRef,
|
||||||
cacheBuster,
|
cacheBuster,
|
||||||
onVideoEnded,
|
onVideoEnded,
|
||||||
onAnimatedStickerLoop,
|
onAnimatedStickerLoop,
|
||||||
@ -104,14 +109,17 @@ const StickerView: FC<OwnProps> = ({
|
|||||||
const [isPlayerReady, markPlayerReady] = useFlag(Boolean(isLottie && fullMediaData && !preloadedPreviewData));
|
const [isPlayerReady, markPlayerReady] = useFlag(Boolean(isLottie && fullMediaData && !preloadedPreviewData));
|
||||||
const isFullMediaReady = fullMediaData && (isStatic || isPlayerReady);
|
const isFullMediaReady = fullMediaData && (isStatic || isPlayerReady);
|
||||||
|
|
||||||
|
const isThumbOpaque = sharedCanvasRef && !withTranslucentThumb;
|
||||||
const thumbClassNames = useMediaTransition(thumbData && !isFullMediaReady);
|
const thumbClassNames = useMediaTransition(thumbData && !isFullMediaReady);
|
||||||
const fullMediaClassNames = useMediaTransition(isFullMediaReady);
|
const fullMediaClassNames = useMediaTransition(isFullMediaReady);
|
||||||
const noTransition = isLottie && preloadedPreviewData;
|
const noTransition = isLottie && preloadedPreviewData;
|
||||||
|
|
||||||
|
const sharedCanvasCoords = useSharedCanvasCoords(containerRef, sharedCanvasRef);
|
||||||
|
|
||||||
// Preload preview for Message Input and local message
|
// Preload preview for Message Input and local message
|
||||||
useMedia(previewMediaHash, !shouldLoad || !shouldPreloadPreview, undefined, cacheBuster);
|
useMedia(previewMediaHash, !shouldLoad || !shouldPreloadPreview, undefined, cacheBuster);
|
||||||
|
|
||||||
const [randomIdPrefix] = useState(generateIdFor(ID_STORE, true));
|
const randomIdPrefix = useMemo(() => generateIdFor(ID_STORE, true), []);
|
||||||
const idKey = [
|
const idKey = [
|
||||||
(withSharedAnimation ? SHARED_PREFIX : randomIdPrefix), id, size, customColor?.join(','),
|
(withSharedAnimation ? SHARED_PREFIX : randomIdPrefix), id, size, customColor?.join(','),
|
||||||
].filter(Boolean).join('_');
|
].filter(Boolean).join('_');
|
||||||
@ -120,16 +128,25 @@ const StickerView: FC<OwnProps> = ({
|
|||||||
<>
|
<>
|
||||||
<img
|
<img
|
||||||
src={thumbData}
|
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=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
{isLottie ? (
|
{isLottie ? (
|
||||||
<AnimatedSticker
|
<AnimatedSticker
|
||||||
key={idKey}
|
key={idKey}
|
||||||
id={idKey}
|
animationId={idKey}
|
||||||
size={size}
|
size={size}
|
||||||
className={buildClassName(
|
className={buildClassName(
|
||||||
styles.media, noTransition && styles.noTransition, fullMediaClassName, fullMediaClassNames,
|
styles.media,
|
||||||
|
(noTransition || isThumbOpaque) && styles.noTransition,
|
||||||
|
fullMediaClassName,
|
||||||
|
fullMediaClassNames,
|
||||||
)}
|
)}
|
||||||
tgsUrl={fullMediaData}
|
tgsUrl={fullMediaData}
|
||||||
play={shouldPlay}
|
play={shouldPlay}
|
||||||
@ -137,6 +154,8 @@ const StickerView: FC<OwnProps> = ({
|
|||||||
noLoop={!shouldLoop}
|
noLoop={!shouldLoop}
|
||||||
forceOnHeavyAnimation={forceOnHeavyAnimation}
|
forceOnHeavyAnimation={forceOnHeavyAnimation}
|
||||||
isLowPriority={isSmall && !selectIsAlwaysHighPriorityEmoji(getGlobal(), stickerSetInfo)}
|
isLowPriority={isSmall && !selectIsAlwaysHighPriorityEmoji(getGlobal(), stickerSetInfo)}
|
||||||
|
sharedCanvas={sharedCanvasRef?.current || undefined}
|
||||||
|
sharedCanvasCoords={sharedCanvasCoords}
|
||||||
onLoad={markPlayerReady}
|
onLoad={markPlayerReady}
|
||||||
onLoop={onAnimatedStickerLoop}
|
onLoop={onAnimatedStickerLoop}
|
||||||
onEnded={onAnimatedStickerLoop}
|
onEnded={onAnimatedStickerLoop}
|
||||||
|
|||||||
@ -4,8 +4,9 @@ import type {
|
|||||||
ApiChat, ApiMessage, ApiUser, ApiGroupCall,
|
ApiChat, ApiMessage, ApiUser, ApiGroupCall,
|
||||||
} from '../../../api/types';
|
} from '../../../api/types';
|
||||||
import type { TextPart } from '../../../types';
|
import type { TextPart } from '../../../types';
|
||||||
|
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||||
import type { LangFn } from '../../../hooks/useLang';
|
import type { LangFn } from '../../../hooks/useLang';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getChatTitle,
|
getChatTitle,
|
||||||
getMessageSummaryText,
|
getMessageSummaryText,
|
||||||
@ -13,17 +14,17 @@ import {
|
|||||||
} from '../../../global/helpers';
|
} from '../../../global/helpers';
|
||||||
import trimText from '../../../util/trimText';
|
import trimText from '../../../util/trimText';
|
||||||
import { formatCurrency } from '../../../util/formatCurrency';
|
import { formatCurrency } from '../../../util/formatCurrency';
|
||||||
import { renderMessageSummary } from './renderMessageText';
|
|
||||||
import renderText from './renderText';
|
import renderText from './renderText';
|
||||||
|
|
||||||
import UserLink from '../UserLink';
|
import UserLink from '../UserLink';
|
||||||
import MessageLink from '../MessageLink';
|
import MessageLink from '../MessageLink';
|
||||||
import ChatLink from '../ChatLink';
|
import ChatLink from '../ChatLink';
|
||||||
import GroupCallLink from '../GroupCallLink';
|
import GroupCallLink from '../GroupCallLink';
|
||||||
|
import MessageSummary from '../MessageSummary';
|
||||||
|
|
||||||
interface RenderOptions {
|
interface RenderOptions {
|
||||||
asPlainText?: boolean;
|
asPlainText?: boolean;
|
||||||
asTextWithSpoilers?: boolean;
|
isEmbedded?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_LENGTH = 32;
|
const MAX_LENGTH = 32;
|
||||||
@ -38,6 +39,8 @@ export function renderActionMessageText(
|
|||||||
targetMessage?: ApiMessage,
|
targetMessage?: ApiMessage,
|
||||||
targetChatId?: string,
|
targetChatId?: string,
|
||||||
options: RenderOptions = {},
|
options: RenderOptions = {},
|
||||||
|
observeIntersectionForLoading?: ObserveFn,
|
||||||
|
observeIntersectionForPlaying?: ObserveFn,
|
||||||
) {
|
) {
|
||||||
if (!message.content.action) {
|
if (!message.content.action) {
|
||||||
return [];
|
return [];
|
||||||
@ -47,7 +50,7 @@ export function renderActionMessageText(
|
|||||||
text, translationValues, amount, currency, call, score,
|
text, translationValues, amount, currency, call, score,
|
||||||
} = message.content.action;
|
} = message.content.action;
|
||||||
const content: TextPart[] = [];
|
const content: TextPart[] = [];
|
||||||
const noLinks = options.asPlainText || options.asTextWithSpoilers;
|
const noLinks = options.asPlainText || options.isEmbedded;
|
||||||
const translationKey = text === 'Chat.Service.Group.UpdatedPinnedMessage1' && !targetMessage
|
const translationKey = text === 'Chat.Service.Group.UpdatedPinnedMessage1' && !targetMessage
|
||||||
? 'Message.PinnedGenericMessage'
|
? 'Message.PinnedGenericMessage'
|
||||||
: text;
|
: text;
|
||||||
@ -124,7 +127,9 @@ export function renderActionMessageText(
|
|||||||
unprocessed,
|
unprocessed,
|
||||||
'%message%',
|
'%message%',
|
||||||
targetMessage
|
targetMessage
|
||||||
? renderMessageContent(lang, targetMessage, options)
|
? renderMessageContent(
|
||||||
|
lang, targetMessage, options, observeIntersectionForLoading, observeIntersectionForPlaying,
|
||||||
|
)
|
||||||
: 'a message',
|
: 'a message',
|
||||||
);
|
);
|
||||||
unprocessed = processed.pop() as string;
|
unprocessed = processed.pop() as string;
|
||||||
@ -168,19 +173,32 @@ function renderProductContent(message: ApiMessage) {
|
|||||||
: 'a product';
|
: 'a product';
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMessageContent(lang: LangFn, message: ApiMessage, options: RenderOptions = {}) {
|
function renderMessageContent(
|
||||||
const { asPlainText, asTextWithSpoilers } = options;
|
lang: LangFn,
|
||||||
|
message: ApiMessage,
|
||||||
|
options: RenderOptions = {},
|
||||||
|
observeIntersectionForLoading?: ObserveFn,
|
||||||
|
observeIntersectionForPlaying?: ObserveFn,
|
||||||
|
) {
|
||||||
|
const { asPlainText, isEmbedded } = options;
|
||||||
|
|
||||||
if (asPlainText) {
|
if (asPlainText) {
|
||||||
return getMessageSummaryText(lang, message, undefined, MAX_LENGTH);
|
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) {
|
if (isEmbedded) {
|
||||||
return (
|
return messageSummary;
|
||||||
<span>{messageSummary}</span>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import type { ApiMessage } from '../../../api/types';
|
import type { ApiMessage } from '../../../api/types';
|
||||||
import { ApiMessageEntityTypes } from '../../../api/types';
|
import { ApiMessageEntityTypes } from '../../../api/types';
|
||||||
import type { TextPart } from '../../../types';
|
import type { TextPart } from '../../../types';
|
||||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
|
||||||
import type { LangFn } from '../../../hooks/useLang';
|
import type { LangFn } from '../../../hooks/useLang';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -22,8 +21,6 @@ export function renderMessageText(
|
|||||||
isSimple?: boolean,
|
isSimple?: boolean,
|
||||||
truncateLength?: number,
|
truncateLength?: number,
|
||||||
isProtected?: boolean,
|
isProtected?: boolean,
|
||||||
observeIntersectionForLoading?: ObserveFn,
|
|
||||||
observeIntersectionForPlaying?: ObserveFn,
|
|
||||||
shouldRenderAsHtml?: boolean,
|
shouldRenderAsHtml?: boolean,
|
||||||
) {
|
) {
|
||||||
const { text, entities } = message.content.text || {};
|
const { text, entities } = message.content.text || {};
|
||||||
@ -42,18 +39,16 @@ export function renderMessageText(
|
|||||||
message.id,
|
message.id,
|
||||||
isSimple,
|
isSimple,
|
||||||
isProtected,
|
isProtected,
|
||||||
observeIntersectionForLoading,
|
|
||||||
observeIntersectionForPlaying,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO Use Message Summary component instead
|
||||||
export function renderMessageSummary(
|
export function renderMessageSummary(
|
||||||
lang: LangFn,
|
lang: LangFn,
|
||||||
message: ApiMessage,
|
message: ApiMessage,
|
||||||
noEmoji = false,
|
noEmoji = false,
|
||||||
highlight?: string,
|
highlight?: string,
|
||||||
truncateLength = TRUNCATED_SUMMARY_LENGTH,
|
truncateLength = TRUNCATED_SUMMARY_LENGTH,
|
||||||
observeIntersection?: ObserveFn,
|
|
||||||
): TextPart[] {
|
): TextPart[] {
|
||||||
const { entities } = message.content.text || {};
|
const { entities } = message.content.text || {};
|
||||||
|
|
||||||
@ -72,9 +67,7 @@ export function renderMessageSummary(
|
|||||||
const emoji = !noEmoji && getMessageSummaryEmoji(message);
|
const emoji = !noEmoji && getMessageSummaryEmoji(message);
|
||||||
const emojiWithSpace = emoji ? `${emoji} ` : '';
|
const emojiWithSpace = emoji ? `${emoji} ` : '';
|
||||||
|
|
||||||
const text = renderMessageText(
|
const text = renderMessageText(message, highlight, undefined, true, truncateLength);
|
||||||
message, highlight, undefined, true, truncateLength, undefined, observeIntersection,
|
|
||||||
);
|
|
||||||
const description = getMessageSummaryDescription(lang, message, text);
|
const description = getMessageSummaryDescription(lang, message, text);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import React from '../../../lib/teact/teact';
|
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 type { TextPart } from '../../../types';
|
||||||
|
|
||||||
|
import EMOJI_REGEX from '../../../lib/twemojiRegex';
|
||||||
import { RE_LINK_TEMPLATE, RE_MENTION_TEMPLATE } from '../../../config';
|
import { RE_LINK_TEMPLATE, RE_MENTION_TEMPLATE } from '../../../config';
|
||||||
import { IS_EMOJI_SUPPORTED } from '../../../util/environment';
|
import { IS_EMOJI_SUPPORTED } from '../../../util/environment';
|
||||||
import {
|
import {
|
||||||
@ -29,7 +30,7 @@ export default function renderText(
|
|||||||
part: TextPart,
|
part: TextPart,
|
||||||
filters: Array<TextFilter> = ['emoji'],
|
filters: Array<TextFilter> = ['emoji'],
|
||||||
params?: { highlight: string | undefined },
|
params?: { highlight: string | undefined },
|
||||||
): TextPart[] {
|
): TeactNode[] {
|
||||||
if (typeof part !== 'string') {
|
if (typeof part !== 'string') {
|
||||||
return [part];
|
return [part];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,6 +38,9 @@ export function renderTextWithEntities(
|
|||||||
isProtected?: boolean,
|
isProtected?: boolean,
|
||||||
observeIntersectionForLoading?: ObserveFn,
|
observeIntersectionForLoading?: ObserveFn,
|
||||||
observeIntersectionForPlaying?: ObserveFn,
|
observeIntersectionForPlaying?: ObserveFn,
|
||||||
|
withTranslucentThumbs?: boolean,
|
||||||
|
sharedCanvasRef?: React.RefObject<HTMLCanvasElement>,
|
||||||
|
sharedCanvasHqRef?: React.RefObject<HTMLCanvasElement>,
|
||||||
) {
|
) {
|
||||||
if (!entities || !entities.length) {
|
if (!entities || !entities.length) {
|
||||||
return renderMessagePart(text, highlight, emojiSize, shouldRenderAsHtml, isSimple);
|
return renderMessagePart(text, highlight, emojiSize, shouldRenderAsHtml, isSimple);
|
||||||
@ -122,7 +125,10 @@ export function renderTextWithEntities(
|
|||||||
isProtected,
|
isProtected,
|
||||||
observeIntersectionForLoading,
|
observeIntersectionForLoading,
|
||||||
observeIntersectionForPlaying,
|
observeIntersectionForPlaying,
|
||||||
|
withTranslucentThumbs,
|
||||||
emojiSize,
|
emojiSize,
|
||||||
|
sharedCanvasRef,
|
||||||
|
sharedCanvasHqRef,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (Array.isArray(newEntity)) {
|
if (Array.isArray(newEntity)) {
|
||||||
@ -301,7 +307,10 @@ function processEntity(
|
|||||||
isProtected?: boolean,
|
isProtected?: boolean,
|
||||||
observeIntersectionForLoading?: ObserveFn,
|
observeIntersectionForLoading?: ObserveFn,
|
||||||
observeIntersectionForPlaying?: ObserveFn,
|
observeIntersectionForPlaying?: ObserveFn,
|
||||||
|
withTranslucentThumbs?: boolean,
|
||||||
emojiSize?: number,
|
emojiSize?: number,
|
||||||
|
sharedCanvasRef?: React.RefObject<HTMLCanvasElement>,
|
||||||
|
sharedCanvasHqRef?: React.RefObject<HTMLCanvasElement>,
|
||||||
) {
|
) {
|
||||||
const entityText = typeof entityContent === 'string' && entityContent;
|
const entityText = typeof entityContent === 'string' && entityContent;
|
||||||
const renderedContent = nestedEntityContent.length ? nestedEntityContent : entityContent;
|
const renderedContent = nestedEntityContent.length ? nestedEntityContent : entityContent;
|
||||||
@ -328,9 +337,12 @@ function processEntity(
|
|||||||
documentId={entity.documentId}
|
documentId={entity.documentId}
|
||||||
size={emojiSize}
|
size={emojiSize}
|
||||||
withSharedAnimation
|
withSharedAnimation
|
||||||
|
sharedCanvasRef={sharedCanvasRef}
|
||||||
|
sharedCanvasHqRef={sharedCanvasHqRef}
|
||||||
withGridFix={!emojiSize}
|
withGridFix={!emojiSize}
|
||||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||||
|
withTranslucentThumb={withTranslucentThumbs}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -449,9 +461,12 @@ function processEntity(
|
|||||||
documentId={entity.documentId}
|
documentId={entity.documentId}
|
||||||
size={emojiSize}
|
size={emojiSize}
|
||||||
withSharedAnimation
|
withSharedAnimation
|
||||||
|
sharedCanvasRef={sharedCanvasRef}
|
||||||
|
sharedCanvasHqRef={sharedCanvasHqRef}
|
||||||
withGridFix={!emojiSize}
|
withGridFix={!emojiSize}
|
||||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||||
|
withTranslucentThumb={withTranslucentThumbs}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
.Chat {
|
.Chat {
|
||||||
--background-color: var(--color-background);
|
--background-color: var(--color-background);
|
||||||
|
--thumbs-background: var(--background-color);
|
||||||
|
|
||||||
body.is-ios &,
|
body.is-ios &,
|
||||||
body.is-macos & {
|
body.is-macos & {
|
||||||
|
|||||||
@ -37,7 +37,6 @@ import renderText from '../../common/helpers/renderText';
|
|||||||
import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities';
|
import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities';
|
||||||
import { fastRaf } from '../../../util/schedulers';
|
import { fastRaf } from '../../../util/schedulers';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import { renderMessageSummary } from '../../common/helpers/renderMessageText';
|
|
||||||
|
|
||||||
import useEnsureMessage from '../../../hooks/useEnsureMessage';
|
import useEnsureMessage from '../../../hooks/useEnsureMessage';
|
||||||
import useChatContextActions from '../../../hooks/useChatContextActions';
|
import useChatContextActions from '../../../hooks/useChatContextActions';
|
||||||
@ -56,6 +55,7 @@ import ChatFolderModal from '../ChatFolderModal.async';
|
|||||||
import ChatCallStatus from './ChatCallStatus';
|
import ChatCallStatus from './ChatCallStatus';
|
||||||
import ReportModal from '../../common/ReportModal';
|
import ReportModal from '../../common/ReportModal';
|
||||||
import FullNameTitle from '../../common/FullNameTitle';
|
import FullNameTitle from '../../common/FullNameTitle';
|
||||||
|
import MessageSummary from '../../common/MessageSummary';
|
||||||
|
|
||||||
import './Chat.scss';
|
import './Chat.scss';
|
||||||
|
|
||||||
@ -272,7 +272,7 @@ const Chat: FC<OwnProps & StateProps> = ({
|
|||||||
actionTargetUsers,
|
actionTargetUsers,
|
||||||
actionTargetMessage,
|
actionTargetMessage,
|
||||||
actionTargetChatId,
|
actionTargetChatId,
|
||||||
{ asTextWithSpoilers: true },
|
{ isEmbedded: true },
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
@ -379,15 +379,19 @@ const Chat: FC<OwnProps & StateProps> = ({
|
|||||||
function renderSummary(
|
function renderSummary(
|
||||||
lang: LangFn, message: ApiMessage, observeIntersection?: ObserveFn, blobUrl?: string, isRoundVideo?: boolean,
|
lang: LangFn, message: ApiMessage, observeIntersection?: ObserveFn, blobUrl?: string, isRoundVideo?: boolean,
|
||||||
) {
|
) {
|
||||||
|
const messageSummary = (
|
||||||
|
<MessageSummary lang={lang} message={message} observeIntersectionForLoading={observeIntersection} />
|
||||||
|
);
|
||||||
|
|
||||||
if (!blobUrl) {
|
if (!blobUrl) {
|
||||||
return renderMessageSummary(lang, message, undefined, undefined, undefined, observeIntersection);
|
return messageSummary;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="media-preview">
|
<span className="media-preview">
|
||||||
<img src={blobUrl} alt="" className={buildClassName('media-preview--image', isRoundVideo && 'round')} />
|
<img src={blobUrl} alt="" className={buildClassName('media-preview--image', isRoundVideo && 'round')} />
|
||||||
{getMessageVideo(message) && <i className="icon-play" />}
|
{getMessageVideo(message) && <i className="icon-play" />}
|
||||||
{renderMessageSummary(lang, message, true, undefined, undefined, observeIntersection)}
|
{messageSummary}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,8 +33,9 @@ import AnimatedIconFromSticker from '../common/AnimatedIconFromSticker';
|
|||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
message: ApiMessage;
|
message: ApiMessage;
|
||||||
observeIntersection?: ObserveFn;
|
observeIntersectionForReading?: ObserveFn;
|
||||||
observeIntersectionForAnimation?: ObserveFn;
|
observeIntersectionForLoading?: ObserveFn;
|
||||||
|
observeIntersectionForPlaying?: ObserveFn;
|
||||||
isEmbedded?: boolean;
|
isEmbedded?: boolean;
|
||||||
appearanceOrder?: number;
|
appearanceOrder?: number;
|
||||||
isLastInList?: boolean;
|
isLastInList?: boolean;
|
||||||
@ -58,8 +59,9 @@ const APPEARANCE_DELAY = 10;
|
|||||||
|
|
||||||
const ActionMessage: FC<OwnProps & StateProps> = ({
|
const ActionMessage: FC<OwnProps & StateProps> = ({
|
||||||
message,
|
message,
|
||||||
observeIntersection,
|
observeIntersectionForReading,
|
||||||
observeIntersectionForAnimation,
|
observeIntersectionForLoading,
|
||||||
|
observeIntersectionForPlaying,
|
||||||
isEmbedded,
|
isEmbedded,
|
||||||
appearanceOrder = 0,
|
appearanceOrder = 0,
|
||||||
isLastInList,
|
isLastInList,
|
||||||
@ -82,7 +84,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
|||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useOnIntersect(ref, observeIntersection);
|
useOnIntersect(ref, observeIntersectionForReading);
|
||||||
useEnsureMessage(message.chatId, message.replyToMessageId, targetMessage);
|
useEnsureMessage(message.chatId, message.replyToMessageId, targetMessage);
|
||||||
useFocusMessage(ref, message.chatId, isFocused, focusDirection, noFocusHighlight);
|
useFocusMessage(ref, message.chatId, isFocused, focusDirection, noFocusHighlight);
|
||||||
|
|
||||||
@ -98,7 +100,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
|||||||
setTimeout(markShown, appearanceOrder * APPEARANCE_DELAY);
|
setTimeout(markShown, appearanceOrder * APPEARANCE_DELAY);
|
||||||
}, [appearanceOrder, markShown, noAppearanceAnimation]);
|
}, [appearanceOrder, markShown, noAppearanceAnimation]);
|
||||||
|
|
||||||
const isVisible = useIsIntersecting(ref, observeIntersectionForAnimation);
|
const isVisible = useIsIntersecting(ref, observeIntersectionForPlaying);
|
||||||
|
|
||||||
const shouldShowConfettiRef = useRef((() => {
|
const shouldShowConfettiRef = useRef((() => {
|
||||||
const isUnread = memoFirstUnreadIdRef?.current && message.id >= memoFirstUnreadIdRef.current;
|
const isUnread = memoFirstUnreadIdRef?.current && message.id >= memoFirstUnreadIdRef.current;
|
||||||
@ -128,7 +130,9 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
|||||||
targetUsers,
|
targetUsers,
|
||||||
targetMessage,
|
targetMessage,
|
||||||
targetChatId,
|
targetChatId,
|
||||||
{ asTextWithSpoilers: isEmbedded },
|
{ isEmbedded },
|
||||||
|
observeIntersectionForLoading,
|
||||||
|
observeIntersectionForPlaying,
|
||||||
);
|
);
|
||||||
const {
|
const {
|
||||||
isContextMenuOpen, contextMenuPosition,
|
isContextMenuOpen, contextMenuPosition,
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import type { ApiMessage } from '../../api/types';
|
|||||||
|
|
||||||
import { getPictogramDimensions } from '../common/helpers/mediaDimensions';
|
import { getPictogramDimensions } from '../common/helpers/mediaDimensions';
|
||||||
import { getMessageMediaHash, getMessageSingleInlineButton } from '../../global/helpers';
|
import { getMessageMediaHash, getMessageSingleInlineButton } from '../../global/helpers';
|
||||||
import { renderMessageSummary } from '../common/helpers/renderMessageText';
|
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
import { IS_TOUCH_ENV } from '../../util/environment';
|
import { IS_TOUCH_ENV } from '../../util/environment';
|
||||||
|
|
||||||
@ -19,6 +18,7 @@ import RippleEffect from '../ui/RippleEffect';
|
|||||||
import ConfirmDialog from '../ui/ConfirmDialog';
|
import ConfirmDialog from '../ui/ConfirmDialog';
|
||||||
import Button from '../ui/Button';
|
import Button from '../ui/Button';
|
||||||
import PinnedMessageNavigation from './PinnedMessageNavigation';
|
import PinnedMessageNavigation from './PinnedMessageNavigation';
|
||||||
|
import MessageSummary from '../common/MessageSummary';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
message: ApiMessage;
|
message: ApiMessage;
|
||||||
@ -39,7 +39,6 @@ const HeaderPinnedMessage: FC<OwnProps> = ({
|
|||||||
const mediaThumbnail = useThumbnail(message);
|
const mediaThumbnail = useThumbnail(message);
|
||||||
const mediaBlobUrl = useMedia(getMessageMediaHash(message, 'pictogram'));
|
const mediaBlobUrl = useMedia(getMessageMediaHash(message, 'pictogram'));
|
||||||
|
|
||||||
const text = renderMessageSummary(lang, message, Boolean(mediaThumbnail));
|
|
||||||
const [isUnpinDialogOpen, openUnpinDialog, closeUnpinDialog] = useFlag();
|
const [isUnpinDialogOpen, openUnpinDialog, closeUnpinDialog] = useFlag();
|
||||||
|
|
||||||
const handleUnpinMessage = useCallback(() => {
|
const handleUnpinMessage = useCallback(() => {
|
||||||
@ -107,7 +106,9 @@ const HeaderPinnedMessage: FC<OwnProps> = ({
|
|||||||
<div className="title" dir="auto">
|
<div className="title" dir="auto">
|
||||||
{customTitle || `${lang('PinnedMessage')} ${index > 0 ? `#${count - index}` : ''}`}
|
{customTitle || `${lang('PinnedMessage')} ${index > 0 ? `#${count - index}` : ''}`}
|
||||||
</div>
|
</div>
|
||||||
<p dir="auto">{text}</p>
|
<p dir="auto">
|
||||||
|
<MessageSummary lang={lang} message={message} noEmoji={Boolean(mediaThumbnail)} />
|
||||||
|
</p>
|
||||||
<RippleEffect />
|
<RippleEffect />
|
||||||
</div>
|
</div>
|
||||||
{inlineButton && (
|
{inlineButton && (
|
||||||
|
|||||||
@ -143,8 +143,9 @@ const MessageListContent: FC<OwnProps> = ({
|
|||||||
<ActionMessage
|
<ActionMessage
|
||||||
key={message.id}
|
key={message.id}
|
||||||
message={message}
|
message={message}
|
||||||
observeIntersection={observeIntersectionForReading}
|
observeIntersectionForReading={observeIntersectionForReading}
|
||||||
observeIntersectionForAnimation={observeIntersectionForAnimatedStickers}
|
observeIntersectionForLoading={observeIntersectionForMedia}
|
||||||
|
observeIntersectionForPlaying={observeIntersectionForAnimatedStickers}
|
||||||
memoFirstUnreadIdRef={memoFirstUnreadIdRef}
|
memoFirstUnreadIdRef={memoFirstUnreadIdRef}
|
||||||
appearanceOrder={messageCountToAnimate - ++appearanceIndex}
|
appearanceOrder={messageCountToAnimate - ++appearanceIndex}
|
||||||
isLastInList={isLastInList}
|
isLastInList={isLastInList}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import type { FC } from '../../../lib/teact/teact';
|
|||||||
import React, {
|
import React, {
|
||||||
useState, useEffect, memo, useRef, useMemo, useCallback,
|
useState, useEffect, memo, useRef, useMemo, useCallback,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
import { withGlobal } from '../../../global';
|
import { getGlobal, withGlobal } from '../../../global';
|
||||||
|
|
||||||
import type { ApiStickerSet, ApiSticker, ApiChat } from '../../../api/types';
|
import type { ApiStickerSet, ApiSticker, ApiChat } from '../../../api/types';
|
||||||
import type { StickerSetOrRecent } from '../../../types';
|
import type { StickerSetOrRecent } from '../../../types';
|
||||||
@ -12,7 +12,7 @@ import {
|
|||||||
FAVORITE_SYMBOL_SET_ID,
|
FAVORITE_SYMBOL_SET_ID,
|
||||||
PREMIUM_STICKER_SET_ID,
|
PREMIUM_STICKER_SET_ID,
|
||||||
RECENT_SYMBOL_SET_ID,
|
RECENT_SYMBOL_SET_ID,
|
||||||
SLIDE_TRANSITION_DURATION,
|
SLIDE_TRANSITION_DURATION, STICKER_PICKER_MAX_SHARED_COVERS,
|
||||||
STICKER_SIZE_PICKER_HEADER,
|
STICKER_SIZE_PICKER_HEADER,
|
||||||
} from '../../../config';
|
} from '../../../config';
|
||||||
import { IS_TOUCH_ENV } from '../../../util/environment';
|
import { IS_TOUCH_ENV } from '../../../util/environment';
|
||||||
@ -21,7 +21,11 @@ import fastSmoothScroll from '../../../util/fastSmoothScroll';
|
|||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import fastSmoothScrollHorizontal from '../../../util/fastSmoothScrollHorizontal';
|
import fastSmoothScrollHorizontal from '../../../util/fastSmoothScrollHorizontal';
|
||||||
import { pickTruthy } from '../../../util/iteratees';
|
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 useAsyncRendering from '../../right/hooks/useAsyncRendering';
|
||||||
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
|
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
|
||||||
@ -76,6 +80,11 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
|||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const headerRef = useRef<HTMLDivElement>(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 [activeSetIndex, setActiveSetIndex] = useState<number>(0);
|
||||||
|
|
||||||
const { observe: observeIntersection } = useIntersectionObserver({
|
const { observe: observeIntersection } = useIntersectionObserver({
|
||||||
@ -179,12 +188,16 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
|||||||
index === activeSetIndex && 'activated',
|
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
|
if (stickerSet.id === RECENT_SYMBOL_SET_ID
|
||||||
|| stickerSet.id === FAVORITE_SYMBOL_SET_ID
|
|| stickerSet.id === FAVORITE_SYMBOL_SET_ID
|
||||||
|| stickerSet.id === CHAT_STICKER_SET_ID
|
|| stickerSet.id === CHAT_STICKER_SET_ID
|
||||||
|| stickerSet.id === PREMIUM_STICKER_SET_ID
|
|| stickerSet.id === PREMIUM_STICKER_SET_ID
|
||||||
|| stickerSet.hasThumbnail
|
|| stickerSet.hasThumbnail
|
||||||
|| !firstSticker) {
|
|| !firstSticker
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
key={stickerSet.id}
|
key={stickerSet.id}
|
||||||
@ -203,6 +216,7 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
|||||||
stickerSet={stickerSet as ApiStickerSet}
|
stickerSet={stickerSet as ApiStickerSet}
|
||||||
noAnimate={!canAnimate || !loadAndPlay}
|
noAnimate={!canAnimate || !loadAndPlay}
|
||||||
observeIntersection={observeIntersectionForCovers}
|
observeIntersection={observeIntersectionForCovers}
|
||||||
|
sharedCanvasRef={withSharedCanvas ? (isHq ? sharedCanvasHqRef : sharedCanvasRef) : undefined}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
@ -219,6 +233,7 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
|||||||
observeIntersection={observeIntersectionForCovers}
|
observeIntersection={observeIntersectionForCovers}
|
||||||
noContextMenu
|
noContextMenu
|
||||||
isCurrentUserPremium
|
isCurrentUserPremium
|
||||||
|
sharedCanvasRef={withSharedCanvas ? (isHq ? sharedCanvasHqRef : sharedCanvasRef) : undefined}
|
||||||
onClick={selectStickerSet}
|
onClick={selectStickerSet}
|
||||||
clickArg={index}
|
clickArg={index}
|
||||||
/>
|
/>
|
||||||
@ -246,7 +261,11 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
|||||||
ref={headerRef}
|
ref={headerRef}
|
||||||
className="StickerPicker-header no-selection no-scrollbar"
|
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>
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
|
|||||||
@ -80,6 +80,15 @@
|
|||||||
background-color: var(--color-background-selected);
|
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 {
|
.symbol-set-container {
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import {
|
|||||||
FAVORITE_SYMBOL_SET_ID,
|
FAVORITE_SYMBOL_SET_ID,
|
||||||
PREMIUM_STICKER_SET_ID,
|
PREMIUM_STICKER_SET_ID,
|
||||||
RECENT_SYMBOL_SET_ID,
|
RECENT_SYMBOL_SET_ID,
|
||||||
SLIDE_TRANSITION_DURATION,
|
SLIDE_TRANSITION_DURATION, STICKER_PICKER_MAX_SHARED_COVERS,
|
||||||
STICKER_SIZE_PICKER_HEADER,
|
STICKER_SIZE_PICKER_HEADER,
|
||||||
} from '../../../config';
|
} from '../../../config';
|
||||||
import { IS_TOUCH_ENV } from '../../../util/environment';
|
import { IS_TOUCH_ENV } from '../../../util/environment';
|
||||||
@ -94,6 +94,9 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
|
|||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const headerRef = useRef<HTMLDivElement>(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 [activeSetIndex, setActiveSetIndex] = useState<number>(0);
|
||||||
|
|
||||||
const sendMessageAction = useSendMessageAction(chat!.id, threadId);
|
const sendMessageAction = useSendMessageAction(chat!.id, threadId);
|
||||||
@ -260,12 +263,15 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
|
|||||||
index === activeSetIndex && 'activated',
|
index === activeSetIndex && 'activated',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const withSharedCanvas = index < STICKER_PICKER_MAX_SHARED_COVERS;
|
||||||
|
|
||||||
if (stickerSet.id === RECENT_SYMBOL_SET_ID
|
if (stickerSet.id === RECENT_SYMBOL_SET_ID
|
||||||
|| stickerSet.id === FAVORITE_SYMBOL_SET_ID
|
|| stickerSet.id === FAVORITE_SYMBOL_SET_ID
|
||||||
|| stickerSet.id === CHAT_STICKER_SET_ID
|
|| stickerSet.id === CHAT_STICKER_SET_ID
|
||||||
|| stickerSet.id === PREMIUM_STICKER_SET_ID
|
|| stickerSet.id === PREMIUM_STICKER_SET_ID
|
||||||
|| stickerSet.hasThumbnail
|
|| stickerSet.hasThumbnail
|
||||||
|| !firstSticker) {
|
|| !firstSticker
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
key={stickerSet.id}
|
key={stickerSet.id}
|
||||||
@ -290,6 +296,7 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
|
|||||||
stickerSet={stickerSet as ApiStickerSet}
|
stickerSet={stickerSet as ApiStickerSet}
|
||||||
noAnimate={!canAnimate || !loadAndPlay}
|
noAnimate={!canAnimate || !loadAndPlay}
|
||||||
observeIntersection={observeIntersectionForCovers}
|
observeIntersection={observeIntersectionForCovers}
|
||||||
|
sharedCanvasRef={withSharedCanvas ? sharedCanvasRef : undefined}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
@ -306,6 +313,7 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
|
|||||||
observeIntersection={observeIntersectionForCovers}
|
observeIntersection={observeIntersectionForCovers}
|
||||||
noContextMenu
|
noContextMenu
|
||||||
isCurrentUserPremium
|
isCurrentUserPremium
|
||||||
|
sharedCanvasRef={withSharedCanvas ? sharedCanvasRef : undefined}
|
||||||
onClick={selectStickerSet}
|
onClick={selectStickerSet}
|
||||||
clickArg={index}
|
clickArg={index}
|
||||||
/>
|
/>
|
||||||
@ -335,7 +343,10 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
|
|||||||
ref={headerRef}
|
ref={headerRef}
|
||||||
className="StickerPicker-header no-selection no-scrollbar"
|
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>
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React, {
|
import React, {
|
||||||
memo, useCallback, useMemo, useRef,
|
memo, useCallback, useMemo, useRef,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
import { getActions } from '../../../global';
|
import { getActions, getGlobal } from '../../../global';
|
||||||
|
|
||||||
import type { FC } from '../../../lib/teact/teact';
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
import type { ApiSticker } from '../../../api/types';
|
import type { ApiSticker } from '../../../api/types';
|
||||||
@ -15,7 +15,7 @@ import {
|
|||||||
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
|
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
|
||||||
import windowSize from '../../../util/windowSize';
|
import windowSize from '../../../util/windowSize';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import { selectIsSetPremium } from '../../../global/selectors';
|
import { selectIsAlwaysHighPriorityEmoji, selectIsSetPremium } from '../../../global/selectors';
|
||||||
|
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
import useFlag from '../../../hooks/useFlag';
|
import useFlag from '../../../hooks/useFlag';
|
||||||
@ -68,10 +68,16 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
openPremiumModal,
|
openPremiumModal,
|
||||||
toggleStickerSet,
|
toggleStickerSet,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const ref = useRef<HTMLDivElement>(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 [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useFlag();
|
||||||
const [isExpanded, expand] = useFlag(!stickerSet.isEmoji);
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
useOnIntersect(ref, observeIntersection);
|
useOnIntersect(ref, observeIntersection);
|
||||||
@ -79,6 +85,7 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
const transitionClassNames = useMediaTransition(shouldRender);
|
const transitionClassNames = useMediaTransition(shouldRender);
|
||||||
|
|
||||||
const isRecent = stickerSet.id === RECENT_SYMBOL_SET_ID;
|
const isRecent = stickerSet.id === RECENT_SYMBOL_SET_ID;
|
||||||
|
const isFavorite = stickerSet.id === FAVORITE_SYMBOL_SET_ID;
|
||||||
const isEmoji = stickerSet.isEmoji;
|
const isEmoji = stickerSet.isEmoji;
|
||||||
const isPremiumSet = !isRecent && selectIsSetPremium(stickerSet);
|
const isPremiumSet = !isRecent && selectIsSetPremium(stickerSet);
|
||||||
|
|
||||||
@ -113,11 +120,11 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
? Math.floor((windowSize.get().width - MOBILE_CONTAINER_PADDING) / (itemSize + margin))
|
? Math.floor((windowSize.get().width - MOBILE_CONTAINER_PADDING) / (itemSize + margin))
|
||||||
: itemsPerRow;
|
: itemsPerRow;
|
||||||
|
|
||||||
const shouldCutSet = isEmoji && !isExpanded && !stickerSet.installedDate && stickerSet.id !== RECENT_SYMBOL_SET_ID;
|
const canCut = !stickerSet.installedDate && stickerSet.id !== RECENT_SYMBOL_SET_ID;
|
||||||
const itemsBeforeCutout = shouldCutSet ? stickersPerRow * 3 : Infinity;
|
const [isCut, , expand] = useFlag(canCut);
|
||||||
const height = Math.ceil((
|
const itemsBeforeCutout = stickersPerRow * 3 - 1;
|
||||||
!shouldCutSet ? stickerSet.count : Math.min(itemsBeforeCutout, stickerSet.count))
|
const heightWhenCut = Math.ceil(Math.min(itemsBeforeCutout, stickerSet.count) / stickersPerRow) * (itemSize + margin);
|
||||||
/ stickersPerRow) * (itemSize + margin);
|
const height = isCut ? heightWhenCut : Math.ceil(stickerSet.count / stickersPerRow) * (itemSize + margin);
|
||||||
|
|
||||||
const favoriteStickerIdsSet = useMemo(() => (
|
const favoriteStickerIdsSet = useMemo(() => (
|
||||||
favoriteStickers ? new Set(favoriteStickers.map(({ id }) => id)) : undefined
|
favoriteStickers ? new Set(favoriteStickers.map(({ id }) => id)) : undefined
|
||||||
@ -154,32 +161,46 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={buildClassName('symbol-set-container', transitionClassNames)}
|
className={buildClassName('symbol-set-container shared-canvas-container', transitionClassNames)}
|
||||||
style={`height: ${height}px;`}
|
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
|
{shouldRender && stickerSet.stickers && stickerSet.stickers
|
||||||
.slice(0, !isExpanded ? (itemsBeforeCutout - 1) : stickerSet.stickers.length)
|
.slice(0, isCut ? itemsBeforeCutout : stickerSet.stickers.length)
|
||||||
.map((sticker) => (
|
.map((sticker, i) => {
|
||||||
<StickerButton
|
const isHqEmoji = (isRecent || isFavorite)
|
||||||
key={sticker.id}
|
&& selectIsAlwaysHighPriorityEmoji(getGlobal(), sticker.stickerSetInfo);
|
||||||
sticker={sticker}
|
const canvasRef = (canCut && i >= itemsBeforeCutout) || isHqEmoji
|
||||||
size={itemSize}
|
? sharedCanvas2Ref
|
||||||
observeIntersection={observeIntersection}
|
: sharedCanvasRef;
|
||||||
noAnimate={!loadAndPlay}
|
|
||||||
isSavedMessages={isSavedMessages}
|
return (
|
||||||
canViewSet
|
<StickerButton
|
||||||
isCurrentUserPremium={isCurrentUserPremium}
|
key={sticker.id}
|
||||||
onClick={onStickerSelect}
|
sticker={sticker}
|
||||||
clickArg={sticker}
|
size={itemSize}
|
||||||
onUnfaveClick={stickerSet.id === FAVORITE_SYMBOL_SET_ID && favoriteStickerIdsSet?.has(sticker.id)
|
observeIntersection={observeIntersection}
|
||||||
? onStickerUnfave : undefined}
|
noAnimate={!loadAndPlay}
|
||||||
onFaveClick={!favoriteStickerIdsSet?.has(sticker.id) ? onStickerFave : undefined}
|
isSavedMessages={isSavedMessages}
|
||||||
onRemoveRecentClick={isRecent ? onStickerRemoveRecent : undefined}
|
canViewSet
|
||||||
/>
|
isCurrentUserPremium={isCurrentUserPremium}
|
||||||
))}
|
sharedCanvasRef={canvasRef}
|
||||||
{!isExpanded && stickerSet.count > itemsBeforeCutout && (
|
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}>
|
<Button className="StickerButton custom-emoji set-expand" round color="translucent" onClick={expand}>
|
||||||
+{stickerSet.count - itemsBeforeCutout + 1}
|
+{stickerSet.count - itemsBeforeCutout}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,16 +1,19 @@
|
|||||||
import type { FC } from '../../../lib/teact/teact';
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
import React, { memo, useRef } from '../../../lib/teact/teact';
|
import React, { memo, useRef } from '../../../lib/teact/teact';
|
||||||
|
import { getGlobal } from '../../../global';
|
||||||
|
|
||||||
import type { ApiStickerSet } from '../../../api/types';
|
import type { ApiStickerSet } from '../../../api/types';
|
||||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||||
|
|
||||||
import { STICKER_SIZE_PICKER_HEADER } from '../../../config';
|
import { STICKER_SIZE_PICKER_HEADER } from '../../../config';
|
||||||
|
import { selectIsAlwaysHighPriorityEmoji } from '../../../global/selectors';
|
||||||
import { IS_WEBM_SUPPORTED } from '../../../util/environment';
|
import { IS_WEBM_SUPPORTED } from '../../../util/environment';
|
||||||
import { getFirstLetters } from '../../../util/textFormat';
|
import { getFirstLetters } from '../../../util/textFormat';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
|
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
|
||||||
import useMedia from '../../../hooks/useMedia';
|
import useMedia from '../../../hooks/useMedia';
|
||||||
import useMediaTransition from '../../../hooks/useMediaTransition';
|
import useMediaTransition from '../../../hooks/useMediaTransition';
|
||||||
|
import useSharedCanvasCoords from '../../../hooks/useSharedCanvasCoords';
|
||||||
|
|
||||||
import AnimatedSticker from '../../common/AnimatedSticker';
|
import AnimatedSticker from '../../common/AnimatedSticker';
|
||||||
import OptimizedVideo from '../../ui/OptimizedVideo';
|
import OptimizedVideo from '../../ui/OptimizedVideo';
|
||||||
@ -22,6 +25,7 @@ type OwnProps = {
|
|||||||
size?: number;
|
size?: number;
|
||||||
noAnimate?: boolean;
|
noAnimate?: boolean;
|
||||||
observeIntersection: ObserveFn;
|
observeIntersection: ObserveFn;
|
||||||
|
sharedCanvasRef?: React.RefObject<HTMLCanvasElement>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StickerSetCover: FC<OwnProps> = ({
|
const StickerSetCover: FC<OwnProps> = ({
|
||||||
@ -29,20 +33,23 @@ const StickerSetCover: FC<OwnProps> = ({
|
|||||||
size = STICKER_SIZE_PICKER_HEADER,
|
size = STICKER_SIZE_PICKER_HEADER,
|
||||||
noAnimate,
|
noAnimate,
|
||||||
observeIntersection,
|
observeIntersection,
|
||||||
|
sharedCanvasRef,
|
||||||
}) => {
|
}) => {
|
||||||
// eslint-disable-next-line no-null/no-null
|
// 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 { 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 mediaData = useMedia((hasThumbnail || isLottie) && `stickerSet${stickerSet.id}`, !isIntersecting);
|
||||||
const isReady = mediaData && (!isVideo || IS_WEBM_SUPPORTED);
|
const isReady = mediaData && (!isVideo || IS_WEBM_SUPPORTED);
|
||||||
const transitionClassNames = useMediaTransition(isReady);
|
const transitionClassNames = useMediaTransition(isReady);
|
||||||
|
|
||||||
|
const sharedCanvasCoords = useSharedCanvasCoords(containerRef, sharedCanvasRef);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className="sticker-set-cover">
|
<div ref={containerRef} className="sticker-set-cover">
|
||||||
{isReady ? (
|
{isReady ? (
|
||||||
isLottie ? (
|
isLottie ? (
|
||||||
<AnimatedSticker
|
<AnimatedSticker
|
||||||
@ -50,6 +57,9 @@ const StickerSetCover: FC<OwnProps> = ({
|
|||||||
tgsUrl={mediaData}
|
tgsUrl={mediaData}
|
||||||
size={size}
|
size={size}
|
||||||
play={isIntersecting && !noAnimate}
|
play={isIntersecting && !noAnimate}
|
||||||
|
isLowPriority={!selectIsAlwaysHighPriorityEmoji(getGlobal(), stickerSet)}
|
||||||
|
sharedCanvas={sharedCanvasRef?.current || undefined}
|
||||||
|
sharedCanvasCoords={sharedCanvasCoords}
|
||||||
/>
|
/>
|
||||||
) : isVideo ? (
|
) : isVideo ? (
|
||||||
<OptimizedVideo
|
<OptimizedVideo
|
||||||
|
|||||||
@ -38,6 +38,5 @@ export function buildCustomEmojiHtmlFromEntity(rawText: string, entity: ApiMessa
|
|||||||
export function getCustomEmojiSize(maxEmojisInLine: number): number | undefined {
|
export function getCustomEmojiSize(maxEmojisInLine: number): number | undefined {
|
||||||
if (maxEmojisInLine > EMOJI_SIZES) return undefined;
|
if (maxEmojisInLine > EMOJI_SIZES) return undefined;
|
||||||
|
|
||||||
const size = (6 - (maxEmojisInLine * 0.625)) * REM; // Should be the same as in _message-content.scss
|
return (6 - (maxEmojisInLine * 0.625)) * REM; // Should be the same as in _message-content.scss
|
||||||
return size;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,6 +23,7 @@
|
|||||||
var(--meta-safe-area-base) + var(--meta-safe-author-width) + var(--meta-safe-area-extra-width)
|
var(--meta-safe-area-base) + var(--meta-safe-author-width) + var(--meta-safe-area-extra-width)
|
||||||
);
|
);
|
||||||
--color-voice-transcribe: var(--color-voice-transcribe-button);
|
--color-voice-transcribe: var(--color-voice-transcribe-button);
|
||||||
|
--thumbs-background: var(--color-background);
|
||||||
--deleting-translate-x: -50%;
|
--deleting-translate-x: -50%;
|
||||||
--select-message-scale: 0.9;
|
--select-message-scale: 0.9;
|
||||||
|
|
||||||
@ -201,6 +202,7 @@
|
|||||||
--deleting-translate-x: 50%;
|
--deleting-translate-x: 50%;
|
||||||
--color-text-green: var(--color-accent-own);
|
--color-text-green: var(--color-accent-own);
|
||||||
--color-voice-transcribe: var(--color-voice-transcribe-button-own);
|
--color-voice-transcribe: var(--color-voice-transcribe-button-own);
|
||||||
|
--thumbs-background: var(--color-background-own);
|
||||||
|
|
||||||
@media (min-width: 1921px) {
|
@media (min-width: 1921px) {
|
||||||
--max-width: 30vw;
|
--max-width: 30vw;
|
||||||
|
|||||||
@ -74,12 +74,11 @@ import {
|
|||||||
areReactionsEmpty,
|
areReactionsEmpty,
|
||||||
getMessageHtmlId,
|
getMessageHtmlId,
|
||||||
isGeoLiveExpired,
|
isGeoLiveExpired,
|
||||||
getMessageSingleCustomEmoji,
|
getMessageSingleCustomEmoji, hasMessageText,
|
||||||
} from '../../../global/helpers';
|
} from '../../../global/helpers';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import useEnsureMessage from '../../../hooks/useEnsureMessage';
|
import useEnsureMessage from '../../../hooks/useEnsureMessage';
|
||||||
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
|
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
|
||||||
import { renderMessageText } from '../../common/helpers/renderMessageText';
|
|
||||||
import { calculateDimensionsForMessageMedia, ROUND_VIDEO_DIMENSIONS_PX } from '../../common/helpers/mediaDimensions';
|
import { calculateDimensionsForMessageMedia, ROUND_VIDEO_DIMENSIONS_PX } from '../../common/helpers/mediaDimensions';
|
||||||
import { buildContentClassName } from './helpers/buildContentClassName';
|
import { buildContentClassName } from './helpers/buildContentClassName';
|
||||||
import { getMinMediaWidth, calculateMediaDimensions } from './helpers/mediaDimensions';
|
import { getMinMediaWidth, calculateMediaDimensions } from './helpers/mediaDimensions';
|
||||||
@ -96,7 +95,6 @@ import useOuterHandlers from './hooks/useOuterHandlers';
|
|||||||
import useInnerHandlers from './hooks/useInnerHandlers';
|
import useInnerHandlers from './hooks/useInnerHandlers';
|
||||||
import { getServerTime } from '../../../util/serverTime';
|
import { getServerTime } from '../../../util/serverTime';
|
||||||
import { isElementInViewport } from '../../../util/isElementInViewport';
|
import { isElementInViewport } from '../../../util/isElementInViewport';
|
||||||
import { getCustomEmojiSize } from '../composer/helpers/customEmoji';
|
|
||||||
|
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
import Avatar from '../../common/Avatar';
|
import Avatar from '../../common/Avatar';
|
||||||
@ -130,6 +128,8 @@ import PremiumIcon from '../../common/PremiumIcon';
|
|||||||
import FakeIcon from '../../common/FakeIcon';
|
import FakeIcon from '../../common/FakeIcon';
|
||||||
|
|
||||||
import './Message.scss';
|
import './Message.scss';
|
||||||
|
import MessageText from '../../common/MessageText';
|
||||||
|
import { getCustomEmojiSize } from '../composer/helpers/customEmoji';
|
||||||
|
|
||||||
type MessagePositionProperties = {
|
type MessagePositionProperties = {
|
||||||
isFirstInGroup: boolean;
|
isFirstInGroup: boolean;
|
||||||
@ -356,15 +356,15 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
const isScheduled = messageListType === 'scheduled' || message.isScheduled;
|
const isScheduled = messageListType === 'scheduled' || message.isScheduled;
|
||||||
const hasReply = isReplyMessage(message) && !shouldHideReply;
|
const hasReply = isReplyMessage(message) && !shouldHideReply;
|
||||||
const hasThread = Boolean(threadInfo) && messageListType === 'thread';
|
const hasThread = Boolean(threadInfo) && messageListType === 'thread';
|
||||||
const customShape = getMessageCustomShape(message);
|
const isCustomShape = getMessageCustomShape(message);
|
||||||
const hasAnimatedEmoji = customShape && (animatedEmoji || animatedCustomEmoji);
|
const hasAnimatedEmoji = isCustomShape && (animatedEmoji || animatedCustomEmoji);
|
||||||
const hasReactions = reactionMessage?.reactions && !areReactionsEmpty(reactionMessage.reactions);
|
const hasReactions = reactionMessage?.reactions && !areReactionsEmpty(reactionMessage.reactions);
|
||||||
const asForwarded = (
|
const asForwarded = (
|
||||||
forwardInfo
|
forwardInfo
|
||||||
&& (!isChatWithSelf || isScheduled)
|
&& (!isChatWithSelf || isScheduled)
|
||||||
&& !isRepliesChat
|
&& !isRepliesChat
|
||||||
&& !forwardInfo.isLinkedChannelPost
|
&& !forwardInfo.isLinkedChannelPost
|
||||||
&& !customShape
|
&& !isCustomShape
|
||||||
);
|
);
|
||||||
const isAlbum = Boolean(album) && album!.messages.length > 1
|
const isAlbum = Boolean(album) && album!.messages.length > 1
|
||||||
&& !album?.messages.some((msg) => Object.keys(msg.content).length === 0);
|
&& !album?.messages.some((msg) => Object.keys(msg.content).length === 0);
|
||||||
@ -509,7 +509,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
const contentClassName = buildContentClassName(message, {
|
const contentClassName = buildContentClassName(message, {
|
||||||
hasReply,
|
hasReply,
|
||||||
customShape,
|
isCustomShape,
|
||||||
isLastInGroup,
|
isLastInGroup,
|
||||||
asForwarded,
|
asForwarded,
|
||||||
hasThread,
|
hasThread,
|
||||||
@ -522,24 +522,15 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const withAppendix = contentClassName.includes('has-appendix');
|
const withAppendix = contentClassName.includes('has-appendix');
|
||||||
|
const hasText = hasMessageText(message);
|
||||||
const emojiSize = message.emojiOnlyCount && getCustomEmojiSize(message.emojiOnlyCount);
|
const emojiSize = message.emojiOnlyCount && getCustomEmojiSize(message.emojiOnlyCount);
|
||||||
const textParts = renderMessageText(
|
|
||||||
message,
|
|
||||||
highlight,
|
|
||||||
emojiSize,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
isProtected,
|
|
||||||
observeIntersectionForMedia,
|
|
||||||
observeIntersectionForAnimatedStickers,
|
|
||||||
);
|
|
||||||
|
|
||||||
let metaPosition!: MetaPosition;
|
let metaPosition!: MetaPosition;
|
||||||
if (phoneCall) {
|
if (phoneCall) {
|
||||||
metaPosition = 'none';
|
metaPosition = 'none';
|
||||||
} else if (isInDocumentGroupNotLast) {
|
} else if (isInDocumentGroupNotLast) {
|
||||||
metaPosition = 'none';
|
metaPosition = 'none';
|
||||||
} else if (textParts && !webPage && !hasAnimatedEmoji) {
|
} else if (hasText && !webPage && !hasAnimatedEmoji) {
|
||||||
metaPosition = 'in-text';
|
metaPosition = 'in-text';
|
||||||
} else {
|
} else {
|
||||||
metaPosition = 'standalone';
|
metaPosition = 'standalone';
|
||||||
@ -549,7 +540,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
if (areReactionsInMeta) {
|
if (areReactionsInMeta) {
|
||||||
reactionsPosition = 'in-meta';
|
reactionsPosition = 'in-meta';
|
||||||
} else if (hasReactions) {
|
} else if (hasReactions) {
|
||||||
if (customShape || ((photo || video) && !textParts)) {
|
if (isCustomShape || ((photo || video) && !hasText)) {
|
||||||
reactionsPosition = 'outside';
|
reactionsPosition = 'outside';
|
||||||
} else if (asForwarded) {
|
} else if (asForwarded) {
|
||||||
metaPosition = 'standalone';
|
metaPosition = 'standalone';
|
||||||
@ -686,7 +677,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
hasReply && 'reply-message',
|
hasReply && 'reply-message',
|
||||||
noMediaCorners && 'no-media-corners',
|
noMediaCorners && 'no-media-corners',
|
||||||
);
|
);
|
||||||
const hasCustomAppendix = isLastInGroup && !textParts && !asForwarded && !hasThread;
|
const hasCustomAppendix = isLastInGroup && !hasText && !asForwarded && !hasThread;
|
||||||
const textContentClass = buildClassName(
|
const textContentClass = buildClassName(
|
||||||
'text-content',
|
'text-content',
|
||||||
metaPosition === 'in-text' && 'with-meta',
|
metaPosition === 'in-text' && 'with-meta',
|
||||||
@ -702,7 +693,8 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
noUserColors={isOwn}
|
noUserColors={isOwn}
|
||||||
isProtected={isProtected}
|
isProtected={isProtected}
|
||||||
sender={replyMessageSender}
|
sender={replyMessageSender}
|
||||||
observeIntersection={observeIntersectionForMedia}
|
observeIntersectionForLoading={observeIntersectionForMedia}
|
||||||
|
observeIntersectionForPlaying={observeIntersectionForAnimatedStickers}
|
||||||
onClick={handleReplyClick}
|
onClick={handleReplyClick}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -877,9 +869,17 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!hasAnimatedEmoji && textParts && (
|
{!hasAnimatedEmoji && hasText && (
|
||||||
<div className={textContentClass} dir="auto">
|
<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()}
|
{metaPosition === 'in-text' && renderReactionsAndMeta()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -925,9 +925,9 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
function renderSenderName() {
|
function renderSenderName() {
|
||||||
const media = photo || video || location;
|
const media = photo || video || location;
|
||||||
const shouldRender = !(customShape && !viaBotId) && (
|
const shouldRender = !(isCustomShape && !viaBotId) && (
|
||||||
(withSenderName && !media) || asForwarded || viaBotId || forceSenderName
|
(withSenderName && !media) || asForwarded || viaBotId || forceSenderName
|
||||||
) && !isInDocumentGroupNotFirst && !(hasReply && customShape);
|
) && !isInDocumentGroupNotFirst && !(hasReply && isCustomShape);
|
||||||
|
|
||||||
if (!shouldRender) {
|
if (!shouldRender) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -935,7 +935,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
let senderTitle;
|
let senderTitle;
|
||||||
let senderColor;
|
let senderColor;
|
||||||
if (senderPeer && !(customShape && viaBotId)) {
|
if (senderPeer && !(isCustomShape && viaBotId)) {
|
||||||
senderTitle = getSenderTitle(lang, senderPeer);
|
senderTitle = getSenderTitle(lang, senderPeer);
|
||||||
|
|
||||||
if (!asForwarded) {
|
if (!asForwarded) {
|
||||||
|
|||||||
@ -7,7 +7,7 @@ export function buildContentClassName(
|
|||||||
message: ApiMessage,
|
message: ApiMessage,
|
||||||
{
|
{
|
||||||
hasReply,
|
hasReply,
|
||||||
customShape,
|
isCustomShape,
|
||||||
isLastInGroup,
|
isLastInGroup,
|
||||||
asForwarded,
|
asForwarded,
|
||||||
hasThread,
|
hasThread,
|
||||||
@ -19,7 +19,7 @@ export function buildContentClassName(
|
|||||||
withVoiceTranscription,
|
withVoiceTranscription,
|
||||||
}: {
|
}: {
|
||||||
hasReply?: boolean;
|
hasReply?: boolean;
|
||||||
customShape?: boolean | number;
|
isCustomShape?: boolean | number;
|
||||||
isLastInGroup?: boolean;
|
isLastInGroup?: boolean;
|
||||||
asForwarded?: boolean;
|
asForwarded?: boolean;
|
||||||
hasThread?: boolean;
|
hasThread?: boolean;
|
||||||
@ -54,7 +54,7 @@ export function buildContentClassName(
|
|||||||
classNames.push('has-action-button');
|
classNames.push('has-action-button');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (customShape) {
|
if (isCustomShape) {
|
||||||
classNames.push('custom-shape');
|
classNames.push('custom-shape');
|
||||||
if (video?.isRound) {
|
if (video?.isRound) {
|
||||||
classNames.push('round');
|
classNames.push('round');
|
||||||
@ -115,7 +115,7 @@ export function buildContentClassName(
|
|||||||
classNames.push('force-sender-name');
|
classNames.push('force-sender-name');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!customShape) {
|
if (!isCustomShape) {
|
||||||
classNames.push('has-shadow');
|
classNames.push('has-shadow');
|
||||||
|
|
||||||
if (isMedia && hasComments) {
|
if (isMedia && hasComments) {
|
||||||
|
|||||||
@ -74,7 +74,7 @@ export function getMessageCopyOptions(
|
|||||||
document.execCommand('copy');
|
document.execCommand('copy');
|
||||||
} else {
|
} else {
|
||||||
const clipboardText = renderMessageText(
|
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)!);
|
if (clipboardText) copyHtmlToClipboard(clipboardText.join(''), getMessageTextWithSpoilers(message)!);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import type { FC } from '../../lib/teact/teact';
|
import type { FC } from '../../lib/teact/teact';
|
||||||
import React, {
|
import React, {
|
||||||
useEffect, memo, useMemo, useCallback,
|
useEffect, memo, useMemo, useCallback, useRef,
|
||||||
} from '../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
import { getActions, withGlobal } from '../../global';
|
import { getActions, withGlobal } from '../../global';
|
||||||
|
|
||||||
@ -36,6 +36,9 @@ const StickerSetResult: FC<OwnProps & StateProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { loadStickers, toggleStickerSet, openStickerSet } = getActions();
|
const { loadStickers, toggleStickerSet, openStickerSet } = getActions();
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const sharedCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
const isAdded = set && Boolean(set.installedDate);
|
const isAdded = set && Boolean(set.installedDate);
|
||||||
const areStickersLoaded = Boolean(set?.stickers);
|
const areStickersLoaded = Boolean(set?.stickers);
|
||||||
@ -96,7 +99,8 @@ const StickerSetResult: FC<OwnProps & StateProps> = ({
|
|||||||
{lang(isAdded ? 'Stickers.Installed' : 'Stickers.Install')}
|
{lang(isAdded ? 'Stickers.Installed' : 'Stickers.Install')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="sticker-set-main">
|
<div className="sticker-set-main shared-canvas-container">
|
||||||
|
<canvas ref={sharedCanvasRef} className="shared-canvas" />
|
||||||
{!canRenderStickers && <Spinner />}
|
{!canRenderStickers && <Spinner />}
|
||||||
{canRenderStickers && displayedStickers.map((sticker) => (
|
{canRenderStickers && displayedStickers.map((sticker) => (
|
||||||
<StickerButton
|
<StickerButton
|
||||||
@ -108,6 +112,7 @@ const StickerSetResult: FC<OwnProps & StateProps> = ({
|
|||||||
onClick={handleStickerClick}
|
onClick={handleStickerClick}
|
||||||
noContextMenu
|
noContextMenu
|
||||||
isCurrentUserPremium={isCurrentUserPremium}
|
isCurrentUserPremium={isCurrentUserPremium}
|
||||||
|
sharedCanvasRef={sharedCanvasRef}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -142,6 +142,7 @@ export const EMOJI_SIZE_PICKER = 40;
|
|||||||
export const COMPOSER_EMOJI_SIZE_PICKER = 32;
|
export const COMPOSER_EMOJI_SIZE_PICKER = 32;
|
||||||
export const STICKER_SIZE_GENERAL_SETTINGS = 48;
|
export const STICKER_SIZE_GENERAL_SETTINGS = 48;
|
||||||
export const STICKER_SIZE_PICKER_HEADER = 32;
|
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_SEARCH = 64;
|
||||||
export const STICKER_SIZE_MODAL = 64;
|
export const STICKER_SIZE_MODAL = 64;
|
||||||
export const EMOJI_SIZE_MODAL = 40;
|
export const EMOJI_SIZE_MODAL = 40;
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
|
import type { TeactNode } from '../../lib/teact/teact';
|
||||||
import type { ApiMessage } from '../../api/types';
|
import type { ApiMessage } from '../../api/types';
|
||||||
import { ApiMessageEntityTypes } from '../../api/types';
|
import { ApiMessageEntityTypes } from '../../api/types';
|
||||||
import { CONTENT_NOT_SUPPORTED } from '../../config';
|
import { CONTENT_NOT_SUPPORTED } from '../../config';
|
||||||
|
|
||||||
import type { TextPart } from '../../types';
|
|
||||||
import type { LangFn } from '../../hooks/useLang';
|
import type { LangFn } from '../../hooks/useLang';
|
||||||
|
|
||||||
import trimText from '../../util/trimText';
|
import trimText from '../../util/trimText';
|
||||||
@ -108,7 +108,7 @@ export function getMessageSummaryEmoji(message: ApiMessage, noReactions = true)
|
|||||||
export function getMessageSummaryDescription(
|
export function getMessageSummaryDescription(
|
||||||
lang: LangFn,
|
lang: LangFn,
|
||||||
message: ApiMessage,
|
message: ApiMessage,
|
||||||
truncatedText?: string | TextPart[],
|
truncatedText?: string | TeactNode,
|
||||||
noReactions = true,
|
noReactions = true,
|
||||||
isExtended = false,
|
isExtended = false,
|
||||||
) {
|
) {
|
||||||
@ -127,7 +127,7 @@ export function getMessageSummaryDescription(
|
|||||||
game,
|
game,
|
||||||
} = message.content;
|
} = message.content;
|
||||||
|
|
||||||
let summary: string | TextPart[] | undefined;
|
let summary: string | TeactNode | undefined;
|
||||||
|
|
||||||
if (message.groupedId) {
|
if (message.groupedId) {
|
||||||
summary = truncatedText || lang('lng_in_dlg_album');
|
summary = truncatedText || lang('lng_in_dlg_album');
|
||||||
|
|||||||
@ -50,22 +50,20 @@ export function getMessageTranscription(message: ApiMessage) {
|
|||||||
return transcriptionId && global.transcriptions[transcriptionId]?.text;
|
return transcriptionId && global.transcriptions[transcriptionId]?.text;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMessageText(message: ApiMessage) {
|
export function hasMessageText(message: ApiMessage) {
|
||||||
const {
|
const {
|
||||||
text, sticker, photo, video, audio, voice, document, poll, webPage, contact, invoice, location,
|
text, sticker, photo, video, audio, voice, document, poll, webPage, contact, invoice, location,
|
||||||
game, action,
|
game, action,
|
||||||
} = message.content;
|
} = message.content;
|
||||||
|
|
||||||
if (text) {
|
return Boolean(text) || !(
|
||||||
return text.text;
|
sticker || photo || video || audio || voice || document || contact || poll || webPage || invoice || location
|
||||||
}
|
|| game || action?.phoneCall
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (sticker || photo || video || audio || voice || document
|
export function getMessageText(message: ApiMessage) {
|
||||||
|| contact || poll || webPage || invoice || location || game || action?.phoneCall) {
|
return hasMessageText(message) ? message.content.text?.text || CONTENT_NOT_SUPPORTED : undefined;
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return CONTENT_NOT_SUPPORTED;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMessageCustomShape(message: ApiMessage): boolean {
|
export function getMessageCustomShape(message: ApiMessage): boolean {
|
||||||
|
|||||||
@ -10,7 +10,7 @@ export function renderMessageSummaryHtml(
|
|||||||
const emoji = getMessageSummaryEmoji(message);
|
const emoji = getMessageSummaryEmoji(message);
|
||||||
const emojiWithSpace = emoji ? `${emoji} ` : '';
|
const emojiWithSpace = emoji ? `${emoji} ` : '';
|
||||||
const text = renderMessageText(
|
const text = renderMessageText(
|
||||||
message, undefined, undefined, undefined, undefined, undefined, undefined, undefined, true,
|
message, undefined, undefined, undefined, undefined, undefined, true,
|
||||||
)?.join('');
|
)?.join('');
|
||||||
const description = getMessageSummaryDescription(lang, message, text, true, true);
|
const description = getMessageSummaryDescription(lang, message, text, true, true);
|
||||||
|
|
||||||
|
|||||||
@ -139,10 +139,11 @@ export function selectLocalAnimatedEmojiEffectByName(name: string) {
|
|||||||
return name === 'Cumshot' ? '🍆' : undefined;
|
return name === 'Cumshot' ? '🍆' : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selectIsDefaultEmojiStatusPack(global: GlobalState, pack: ApiStickerSetInfo) {
|
export function selectIsDefaultEmojiStatusPack(global: GlobalState, stickerSet: ApiStickerSetInfo | ApiStickerSet) {
|
||||||
return 'id' in pack && pack.id === global.appConfig?.defaultEmojiStatusesStickerSetId;
|
return 'id' in stickerSet && stickerSet.id === global.appConfig?.defaultEmojiStatusesStickerSetId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selectIsAlwaysHighPriorityEmoji(global: GlobalState, pack: ApiStickerSetInfo) {
|
export function selectIsAlwaysHighPriorityEmoji(global: GlobalState, stickerSet: ApiStickerSetInfo | ApiStickerSet) {
|
||||||
return selectIsDefaultEmojiStatusPack(global, pack) || ('id' in pack && pack.id === RESTRICTED_EMOJI_SET_ID);
|
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;
|
size?: number;
|
||||||
quality?: number;
|
quality?: number;
|
||||||
isLowPriority?: boolean;
|
isLowPriority?: boolean;
|
||||||
|
coords?: { x: number; y: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
type Frames = ArrayBuffer[];
|
type Frames = ArrayBuffer[];
|
||||||
@ -36,11 +37,13 @@ let lastWorkerIndex = -1;
|
|||||||
class RLottie {
|
class RLottie {
|
||||||
// Config
|
// Config
|
||||||
|
|
||||||
private containers = new Map<HTMLDivElement, {
|
private containers = new Map<string, {
|
||||||
canvas: HTMLCanvasElement;
|
canvas: HTMLCanvasElement;
|
||||||
ctx: CanvasRenderingContext2D;
|
ctx: CanvasRenderingContext2D;
|
||||||
isLoaded?: boolean;
|
isLoaded?: boolean;
|
||||||
isPaused?: boolean;
|
isPaused?: boolean;
|
||||||
|
isSharedCanvas?: boolean;
|
||||||
|
coords?: Params['coords'];
|
||||||
onLoad?: NoneToVoidFunction;
|
onLoad?: NoneToVoidFunction;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
@ -87,7 +90,7 @@ class RLottie {
|
|||||||
private lastRenderAt?: number;
|
private lastRenderAt?: number;
|
||||||
|
|
||||||
static init(...args: ConstructorParameters<typeof RLottie>) {
|
static init(...args: ConstructorParameters<typeof RLottie>) {
|
||||||
const [container, onLoad, id] = args;
|
const [container, canvas, onLoad, id, , params] = args;
|
||||||
let instance = instancesById.get(id);
|
let instance = instancesById.get(id);
|
||||||
|
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
@ -95,14 +98,15 @@ class RLottie {
|
|||||||
instance = new RLottie(...args);
|
instance = new RLottie(...args);
|
||||||
instancesById.set(id, instance);
|
instancesById.set(id, instance);
|
||||||
} else {
|
} else {
|
||||||
instance.addContainer(container, onLoad);
|
instance.addContainer(container, canvas, onLoad, params?.coords);
|
||||||
}
|
}
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
container: HTMLDivElement,
|
containerId: string,
|
||||||
|
container: HTMLDivElement | HTMLCanvasElement,
|
||||||
onLoad: NoneToVoidFunction | undefined,
|
onLoad: NoneToVoidFunction | undefined,
|
||||||
private id: string,
|
private id: string,
|
||||||
private tgsUrl: string,
|
private tgsUrl: string,
|
||||||
@ -111,14 +115,18 @@ class RLottie {
|
|||||||
private onEnded?: (isDestroyed?: boolean) => void,
|
private onEnded?: (isDestroyed?: boolean) => void,
|
||||||
private onLoop?: () => void,
|
private onLoop?: () => void,
|
||||||
) {
|
) {
|
||||||
this.addContainer(container, onLoad);
|
this.addContainer(containerId, container, onLoad, params.coords);
|
||||||
this.initConfig();
|
this.initConfig();
|
||||||
this.initRenderer();
|
this.initRenderer();
|
||||||
}
|
}
|
||||||
|
|
||||||
public removeContainer(container: HTMLDivElement) {
|
public removeContainer(containerId: string) {
|
||||||
this.containers.get(container)!.canvas.remove();
|
const containerData = this.containers.get(containerId)!;
|
||||||
this.containers.delete(container);
|
if (!containerData.isSharedCanvas) {
|
||||||
|
this.containers.get(containerId)!.canvas.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.containers.delete(containerId);
|
||||||
|
|
||||||
if (!this.containers.size) {
|
if (!this.containers.size) {
|
||||||
this.destroy();
|
this.destroy();
|
||||||
@ -129,9 +137,9 @@ class RLottie {
|
|||||||
return this.isAnimating || this.isWaiting;
|
return this.isAnimating || this.isWaiting;
|
||||||
}
|
}
|
||||||
|
|
||||||
play(forceRestart = false, container?: HTMLDivElement) {
|
play(forceRestart = false, containerId?: string) {
|
||||||
if (container) {
|
if (containerId) {
|
||||||
this.containers.get(container)!.isPaused = false;
|
this.containers.get(containerId)!.isPaused = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isEnded && forceRestart) {
|
if (this.isEnded && forceRestart) {
|
||||||
@ -143,9 +151,9 @@ class RLottie {
|
|||||||
this.doPlay();
|
this.doPlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
pause(container?: HTMLDivElement) {
|
pause(containerId?: string) {
|
||||||
if (container) {
|
if (containerId) {
|
||||||
this.containers.get(container)!.isPaused = true;
|
this.containers.get(containerId)!.isPaused = true;
|
||||||
|
|
||||||
const areAllContainersPaused = Array.from(this.containers.values()).every(({ isPaused }) => isPaused);
|
const areAllContainersPaused = Array.from(this.containers.values()).every(({ isPaused }) => isPaused);
|
||||||
if (!areAllContainersPaused) {
|
if (!areAllContainersPaused) {
|
||||||
@ -178,47 +186,85 @@ class RLottie {
|
|||||||
this.params.noLoop = noLoop;
|
this.params.noLoop = noLoop;
|
||||||
}
|
}
|
||||||
|
|
||||||
private addContainer(container: HTMLDivElement, onLoad?: NoneToVoidFunction) {
|
private addContainer(
|
||||||
if (!(container.parentNode instanceof HTMLElement)) {
|
containerId: string,
|
||||||
throw new Error('[RLottie] Container is not mounted');
|
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) {
|
let { size } = this.params;
|
||||||
size = (
|
|
||||||
container.offsetWidth
|
|
||||||
|| parseInt(container.style.width, 10)
|
|
||||||
|| container.parentNode.offsetWidth
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!size) {
|
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) {
|
if (!this.imgSize) {
|
||||||
this.imgSize = imgSize;
|
this.imgSize = imgSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.containers.set(container, { canvas, ctx, onLoad });
|
|
||||||
|
|
||||||
if (this.isRendererInited) {
|
if (this.isRendererInited) {
|
||||||
this.doPlay();
|
this.doPlay();
|
||||||
}
|
}
|
||||||
@ -371,15 +417,16 @@ class RLottie {
|
|||||||
/* eslint-enable prefer-destructuring */
|
/* eslint-enable prefer-destructuring */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const imageData = new ImageData(arr, this.imgSize, this.imgSize);
|
const imageData = new ImageData(arr, this.imgSize, this.imgSize);
|
||||||
|
|
||||||
this.containers.forEach((containerData) => {
|
this.containers.forEach((containerData) => {
|
||||||
const {
|
const {
|
||||||
ctx, isLoaded, isPaused, onLoad,
|
ctx, isLoaded, isPaused, coords: { x, y } = {}, onLoad,
|
||||||
} = containerData;
|
} = containerData;
|
||||||
|
|
||||||
if (!isLoaded || !isPaused) {
|
if (!isLoaded || !isPaused) {
|
||||||
ctx.putImageData(imageData, 0, 0);
|
ctx.putImageData(imageData, x || 0, y || 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isLoaded) {
|
if (!isLoaded) {
|
||||||
|
|||||||
@ -286,6 +286,20 @@ div[role="button"] {
|
|||||||
left: 0;
|
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 {
|
@keyframes grow-icon {
|
||||||
0% {
|
0% {
|
||||||
transform: scale(0.5);
|
transform: scale(0.5);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user