Custom Emoji: Support adaptive static and video packs (#3118)

This commit is contained in:
Alexander Zinchuk 2023-05-15 10:57:14 +02:00
parent cebbda28cf
commit a71a3bc53b
20 changed files with 290 additions and 110 deletions

View File

@ -128,6 +128,7 @@ export function buildStickerSet(set: GramJs.StickerSet): ApiStickerSet {
accessHash: String(accessHash), accessHash: String(accessHash),
title, title,
hasThumbnail: Boolean(thumbs?.length || thumbDocumentId), hasThumbnail: Boolean(thumbs?.length || thumbDocumentId),
thumbCustomEmojiId: thumbDocumentId?.toString(),
count, count,
shortName, shortName,
}; };

View File

@ -57,6 +57,7 @@ export interface ApiStickerSet {
accessHash: string; accessHash: string;
title: string; title: string;
hasThumbnail?: boolean; hasThumbnail?: boolean;
thumbCustomEmojiId?: string;
count: number; count: number;
stickers?: ApiSticker[]; stickers?: ApiSticker[];
packs?: Record<string, ApiSticker[]>; packs?: Record<string, ApiSticker[]>;

View File

@ -51,11 +51,11 @@ const OutlinedMicrophoneIcon: FC<OwnProps> = ({
// eslint-disable-next-line // eslint-disable-next-line
}, [isMuted, shouldRaiseHand, isRaiseHand]); }, [isMuted, shouldRaiseHand, isRaiseHand]);
const microphoneColor: [number, number, number] | undefined = useMemo(() => { const microphoneColor: string | undefined = useMemo(() => {
return noColor ? [0xff, 0xff, 0xff] : ( return noColor ? '#ffffff' : (
isRaiseHand ? [0x4d, 0xa6, 0xe0] isRaiseHand ? '#4da6e0'
: (shouldRaiseHand || isMutedByMe ? [0xFF, 0x70, 0x6F] : ( : (shouldRaiseHand || isMutedByMe ? '#ff706f' : (
isSpeaking ? [0x57, 0xBC, 0x6C] : [0x84, 0x8D, 0x94] isSpeaking ? '#57bc6c' : '#848d94'
)) ))
); );
}, [noColor, isRaiseHand, shouldRaiseHand, isMutedByMe, isSpeaking]); }, [noColor, isRaiseHand, shouldRaiseHand, isMutedByMe, isSpeaking]);

View File

@ -11,6 +11,7 @@ import React, {
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import buildStyle from '../../util/buildStyle'; import buildStyle from '../../util/buildStyle';
import generateIdFor from '../../util/generateIdFor'; import generateIdFor from '../../util/generateIdFor';
import { hexToRgb } from '../../util/switchTheme';
import useHeavyAnimationCheck, { isHeavyAnimating } from '../../hooks/useHeavyAnimationCheck'; import useHeavyAnimationCheck, { isHeavyAnimating } from '../../hooks/useHeavyAnimationCheck';
import usePriorityPlaybackCheck, { isPriorityPlaybackActive } from '../../hooks/usePriorityPlaybackCheck'; import usePriorityPlaybackCheck, { isPriorityPlaybackActive } from '../../hooks/usePriorityPlaybackCheck';
@ -19,6 +20,8 @@ import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps';
import { useStateRef } from '../../hooks/useStateRef'; import { useStateRef } from '../../hooks/useStateRef';
import useSharedIntersectionObserver from '../../hooks/useSharedIntersectionObserver'; import useSharedIntersectionObserver from '../../hooks/useSharedIntersectionObserver';
import useThrottledCallback from '../../hooks/useThrottledCallback'; import useThrottledCallback from '../../hooks/useThrottledCallback';
import useColorFilter from '../../hooks/stickers/useColorFilter';
import useSyncEffect from '../../hooks/useSyncEffect';
export type OwnProps = { export type OwnProps = {
ref?: RefObject<HTMLDivElement>; ref?: RefObject<HTMLDivElement>;
@ -32,7 +35,7 @@ export type OwnProps = {
noLoop?: boolean; noLoop?: boolean;
size: number; size: number;
quality?: number; quality?: number;
color?: [number, number, number]; color?: string;
isLowPriority?: boolean; isLowPriority?: boolean;
forceOnHeavyAnimation?: boolean; forceOnHeavyAnimation?: boolean;
sharedCanvas?: HTMLCanvasElement; sharedCanvas?: HTMLCanvasElement;
@ -80,10 +83,24 @@ const AnimatedSticker: FC<OwnProps> = ({
const animationRef = useRef<RLottieInstance>(); const animationRef = useRef<RLottieInstance>();
const isFirstRender = useRef(true); const isFirstRender = useRef(true);
const shouldUseColorFilter = !sharedCanvas && color;
const colorFilter = useColorFilter(shouldUseColorFilter ? color : undefined);
const playKey = play || playSegment; const playKey = play || playSegment;
const playRef = useStateRef(play); const playRef = useStateRef(play);
const playSegmentRef = useStateRef(playSegment); const playSegmentRef = useStateRef(playSegment);
const rgbColor = useRef<[number, number, number] | undefined>();
useSyncEffect(() => {
if (color && !shouldUseColorFilter) {
const { r, g, b } = hexToRgb(color);
rgbColor.current = [r, g, b];
} else {
rgbColor.current = undefined;
}
}, [color, shouldUseColorFilter]);
const isUnmountedRef = useRef(false); const isUnmountedRef = useRef(false);
useEffect(() => { useEffect(() => {
return () => { return () => {
@ -118,7 +135,7 @@ const AnimatedSticker: FC<OwnProps> = ({
coords: sharedCanvasCoords, coords: sharedCanvasCoords,
}, },
viewId, viewId,
color, rgbColor.current,
onLoad, onLoad,
onEnded, onEnded,
onLoop, onLoop,
@ -131,7 +148,7 @@ const AnimatedSticker: FC<OwnProps> = ({
setAnimation(newAnimation); setAnimation(newAnimation);
animationRef.current = newAnimation; animationRef.current = newAnimation;
}, [ }, [
color, isLowPriority, noLoop, onEnded, onLoad, onLoop, quality, isLowPriority, noLoop, onEnded, onLoad, onLoop, quality,
renderId, sharedCanvas, sharedCanvasCoords, size, speed, tgsUrl, viewId, renderId, sharedCanvas, sharedCanvasCoords, size, speed, tgsUrl, viewId,
]); ]);
@ -149,7 +166,7 @@ const AnimatedSticker: FC<OwnProps> = ({
useEffect(() => { useEffect(() => {
if (!animation) return; if (!animation) return;
animation.setColor(color); animation.setColor(rgbColor.current);
}, [color, animation]); }, [color, animation]);
useEffect(() => { useEffect(() => {
@ -239,6 +256,7 @@ const AnimatedSticker: FC<OwnProps> = ({
style={buildStyle( style={buildStyle(
size !== undefined && `width: ${size}px; height: ${size}px;`, size !== undefined && `width: ${size}px; height: ${size}px;`,
onClick && 'cursor: pointer', onClick && 'cursor: pointer',
colorFilter,
style, style,
)} )}
onClick={onClick} onClick={onClick}

View File

@ -12,7 +12,7 @@ import safePlay from '../../util/safePlay';
import { selectIsAlwaysHighPriorityEmoji } from '../../global/selectors'; import { selectIsAlwaysHighPriorityEmoji } from '../../global/selectors';
import useCustomEmoji from './hooks/useCustomEmoji'; import useCustomEmoji from './hooks/useCustomEmoji';
import useDynamicColorListener from '../../hooks/useDynamicColorListener'; import useDynamicColorListener from '../../hooks/stickers/useDynamicColorListener';
import StickerView from './StickerView'; import StickerView from './StickerView';
@ -77,7 +77,7 @@ const CustomEmoji: FC<OwnProps> = ({
const [shouldLoop, setShouldLoop] = useState(true); const [shouldLoop, setShouldLoop] = useState(true);
const hasCustomColor = customEmoji?.shouldUseTextColor; const hasCustomColor = customEmoji?.shouldUseTextColor;
const { rgbColor: customColor } = useDynamicColorListener(containerRef, !hasCustomColor); const customColor = useDynamicColorListener(containerRef, !hasCustomColor);
const handleVideoEnded = useCallback((e) => { const handleVideoEnded = useCallback((e) => {
if (!loopLimit) return; if (!loopLimit) return;
@ -132,7 +132,6 @@ const CustomEmoji: FC<OwnProps> = ({
isSmall={!isBig} isSmall={!isBig}
size={size} size={size}
noPlay={noPlay || !canPlay} noPlay={noPlay || !canPlay}
customColor={customColor}
thumbClassName={styles.thumb} thumbClassName={styles.thumb}
fullMediaClassName={styles.media} fullMediaClassName={styles.media}
shouldLoop={shouldLoop} shouldLoop={shouldLoop}
@ -146,6 +145,7 @@ const CustomEmoji: FC<OwnProps> = ({
withTranslucentThumb={withTranslucentThumb} withTranslucentThumb={withTranslucentThumb}
onVideoEnded={handleVideoEnded} onVideoEnded={handleVideoEnded}
onAnimatedStickerLoop={handleStickerLoop} onAnimatedStickerLoop={handleStickerLoop}
customColor={customColor}
/> />
)} )}
</div> </div>

View File

@ -6,4 +6,8 @@
> h3 { > h3 {
margin-bottom: 0; margin-bottom: 0;
} }
:global(.custom-emoji) {
color: var(--color-primary);
}
} }

View File

@ -16,7 +16,7 @@ import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers'; import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
import useMenuPosition from '../../hooks/useMenuPosition'; import useMenuPosition from '../../hooks/useMenuPosition';
import useDynamicColorListener from '../../hooks/useDynamicColorListener'; import useDynamicColorListener from '../../hooks/stickers/useDynamicColorListener';
import StickerView from './StickerView'; import StickerView from './StickerView';
import Button from '../ui/Button'; import Button from '../ui/Button';
@ -93,7 +93,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
const menuRef = useRef<HTMLDivElement>(null); const menuRef = useRef<HTMLDivElement>(null);
const lang = useLang(); const lang = useLang();
const hasCustomColor = sticker.shouldUseTextColor; const hasCustomColor = sticker.shouldUseTextColor;
const { rgbColor: customColor } = useDynamicColorListener(ref, !hasCustomColor); const customColor = useDynamicColorListener(ref, !hasCustomColor);
const { const {
id, isCustomEmoji, hasEffect: isPremium, stickerSetInfo, id, isCustomEmoji, hasEffect: isPremium, stickerSetInfo,

View File

@ -1,4 +1,6 @@
import React, { memo, useMemo } from '../../lib/teact/teact'; import React, {
memo, useMemo,
} 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';
@ -19,6 +21,7 @@ import useMediaTransition from '../../hooks/useMediaTransition';
import useFlag from '../../hooks/useFlag'; import useFlag from '../../hooks/useFlag';
import useCoordsInSharedCanvas from '../../hooks/useCoordsInSharedCanvas'; import useCoordsInSharedCanvas from '../../hooks/useCoordsInSharedCanvas';
import useHeavyAnimationCheck, { isHeavyAnimating } from '../../hooks/useHeavyAnimationCheck'; import useHeavyAnimationCheck, { isHeavyAnimating } from '../../hooks/useHeavyAnimationCheck';
import useColorFilter from '../../hooks/stickers/useColorFilter';
import AnimatedSticker from './AnimatedSticker'; import AnimatedSticker from './AnimatedSticker';
import OptimizedVideo from '../ui/OptimizedVideo'; import OptimizedVideo from '../ui/OptimizedVideo';
@ -33,7 +36,7 @@ type OwnProps = {
fullMediaClassName?: string; fullMediaClassName?: string;
isSmall?: boolean; isSmall?: boolean;
size?: number; size?: number;
customColor?: [number, number, number]; customColor?: string;
loopLimit?: number; loopLimit?: number;
shouldLoop?: boolean; shouldLoop?: boolean;
shouldPreloadPreview?: boolean; shouldPreloadPreview?: boolean;
@ -86,6 +89,8 @@ const StickerView: FC<OwnProps> = ({
const isStatic = !isLottie && !isVideo; const isStatic = !isLottie && !isVideo;
const previewMediaHash = getStickerPreviewHash(sticker.id); const previewMediaHash = getStickerPreviewHash(sticker.id);
const filterStyle = useColorFilter(customColor);
const isIntersectingForLoading = useIsIntersecting(containerRef, observeIntersectionForLoading); const isIntersectingForLoading = useIsIntersecting(containerRef, observeIntersectionForLoading);
const shouldLoad = isIntersectingForLoading && !noLoad; const shouldLoad = isIntersectingForLoading && !noLoad;
const isIntersectingForPlaying = ( const isIntersectingForPlaying = (
@ -128,7 +133,10 @@ const StickerView: FC<OwnProps> = ({
const randomIdPrefix = useMemo(() => generateIdFor(ID_STORE, true), []); const randomIdPrefix = useMemo(() => generateIdFor(ID_STORE, true), []);
const renderId = [ const renderId = [
(withSharedAnimation ? SHARED_PREFIX : randomIdPrefix), id, size, customColor?.join(','), (withSharedAnimation ? SHARED_PREFIX : randomIdPrefix),
id,
size,
(withSharedAnimation ? customColor : undefined),
].filter(Boolean).join('_'); ].filter(Boolean).join('_');
return ( return (
@ -159,7 +167,6 @@ const StickerView: FC<OwnProps> = ({
)} )}
tgsUrl={fullMediaData} tgsUrl={fullMediaData}
play={shouldPlay} play={shouldPlay}
color={customColor}
noLoop={!shouldLoop} noLoop={!shouldLoop}
forceOnHeavyAnimation={forceOnHeavyAnimation} forceOnHeavyAnimation={forceOnHeavyAnimation}
isLowPriority={isSmall && !selectIsAlwaysHighPriorityEmoji(getGlobal(), stickerSetInfo)} isLowPriority={isSmall && !selectIsAlwaysHighPriorityEmoji(getGlobal(), stickerSetInfo)}
@ -168,6 +175,7 @@ const StickerView: FC<OwnProps> = ({
onLoad={markPlayerReady} onLoad={markPlayerReady}
onLoop={onAnimatedStickerLoop} onLoop={onAnimatedStickerLoop}
onEnded={onAnimatedStickerLoop} onEnded={onAnimatedStickerLoop}
color={customColor}
/> />
) : isVideo ? ( ) : isVideo ? (
<OptimizedVideo <OptimizedVideo
@ -180,12 +188,14 @@ const StickerView: FC<OwnProps> = ({
disablePictureInPicture disablePictureInPicture
onReady={markPlayerReady} onReady={markPlayerReady}
onEnded={onVideoEnded} onEnded={onVideoEnded}
style={filterStyle}
/> />
) : ( ) : (
<img <img
className={buildClassName(styles.media, fullMediaClassName, fullMediaClassNames, 'sticker-media')} className={buildClassName(styles.media, fullMediaClassName, fullMediaClassNames, 'sticker-media')}
src={fullMediaData} src={fullMediaData}
alt={emoji} alt={emoji}
style={filterStyle}
draggable={false} draggable={false}
/> />
))} ))}

View File

@ -26,7 +26,7 @@
width: calc(100vw - 1rem); width: calc(100vw - 1rem);
} }
:global(.custom-emoji) { :global(.sticker-set-cover), :global(.custom-emoji) {
color: var(--color-primary); color: var(--color-primary);
} }
} }

View File

@ -462,6 +462,10 @@
.custom-emoji { .custom-emoji {
margin: 0; margin: 0;
vertical-align: text-top; vertical-align: text-top;
&.colorable {
filter: var(--input-custom-emoji-filter);
}
} }
// Workaround to preserve correct input height // Workaround to preserve correct input height

View File

@ -16,6 +16,9 @@ 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 useCoordsInSharedCanvas from '../../../hooks/useCoordsInSharedCanvas'; import useCoordsInSharedCanvas from '../../../hooks/useCoordsInSharedCanvas';
import useCustomEmoji from '../../common/hooks/useCustomEmoji';
import useDynamicColorListener from '../../../hooks/stickers/useDynamicColorListener';
import useColorFilter from '../../../hooks/stickers/useColorFilter';
import AnimatedSticker from '../../common/AnimatedSticker'; import AnimatedSticker from '../../common/AnimatedSticker';
import OptimizedVideo from '../../ui/OptimizedVideo'; import OptimizedVideo from '../../ui/OptimizedVideo';
@ -41,7 +44,14 @@ const StickerSetCover: FC<OwnProps> = ({
// 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);
const { hasThumbnail, isLottie, isVideos: isVideo } = stickerSet; const {
hasThumbnail, thumbCustomEmojiId, isLottie, isVideos: isVideo,
} = stickerSet;
const { customEmoji } = useCustomEmoji(thumbCustomEmojiId);
const hasCustomColor = customEmoji?.shouldUseTextColor;
const customColor = useDynamicColorListener(containerRef, !hasCustomColor);
const colorFilter = useColorFilter(customColor);
const isIntersecting = useIsIntersecting(containerRef, observeIntersection); const isIntersecting = useIsIntersecting(containerRef, observeIntersection);
const shouldPlay = isIntersecting && !noPlay; const shouldPlay = isIntersecting && !noPlay;
@ -86,12 +96,14 @@ const StickerSetCover: FC<OwnProps> = ({
className={buildClassName(styles.video, transitionClassNames)} className={buildClassName(styles.video, transitionClassNames)}
src={mediaData} src={mediaData}
canPlay={shouldPlay} canPlay={shouldPlay}
style={colorFilter}
loop loop
disablePictureInPicture disablePictureInPicture
/> />
) : ( ) : (
<img <img
src={mediaData || staticMediaData} src={mediaData || staticMediaData}
style={colorFilter}
className={buildClassName(styles.image, transitionClassNames)} className={buildClassName(styles.image, transitionClassNames)}
alt="" alt=""
/> />

View File

@ -161,7 +161,7 @@
} }
} }
.StickerButton.custom-emoji { .StickerButton.custom-emoji, .sticker-set-cover {
color: var(--color-text); color: var(--color-text);
} }

View File

@ -5,14 +5,19 @@ import { getGlobal } from '../../../../global';
import { EMOJI_SIZES } from '../../../../config'; import { EMOJI_SIZES } from '../../../../config';
import { REM } from '../../../common/helpers/mediaDimensions'; import { REM } from '../../../common/helpers/mediaDimensions';
import { getInputCustomEmojiParams } from '../../../../util/customEmojiManager'; import { getInputCustomEmojiParams } from '../../../../util/customEmojiManager';
import buildClassName from '../../../../util/buildClassName';
export const INPUT_CUSTOM_EMOJI_SELECTOR = 'img[data-document-id]'; export const INPUT_CUSTOM_EMOJI_SELECTOR = 'img[data-document-id]';
export function buildCustomEmojiHtml(emoji: ApiSticker) { export function buildCustomEmojiHtml(emoji: ApiSticker) {
const [isPlaceholder, src, uniqueId] = getInputCustomEmojiParams(emoji); const [isPlaceholder, src, uniqueId] = getInputCustomEmojiParams(emoji);
const className = buildClassName(
'custom-emoji', 'emoji', 'emoji-small', isPlaceholder && 'placeholder', emoji.shouldUseTextColor && 'colorable',
);
return `<img return `<img
class="custom-emoji emoji emoji-small ${isPlaceholder ? 'placeholder' : ''}" class="${className}"
draggable="false" draggable="false"
alt="${emoji.emoji}" alt="${emoji.emoji}"
data-document-id="${emoji.id}" data-document-id="${emoji.id}"
@ -25,8 +30,17 @@ export function buildCustomEmojiHtml(emoji: ApiSticker) {
export function buildCustomEmojiHtmlFromEntity(rawText: string, entity: ApiMessageEntityCustomEmoji) { export function buildCustomEmojiHtmlFromEntity(rawText: string, entity: ApiMessageEntityCustomEmoji) {
const customEmoji = getGlobal().customEmojis.byId[entity.documentId]; const customEmoji = getGlobal().customEmojis.byId[entity.documentId];
const [isPlaceholder, src, uniqueId] = getInputCustomEmojiParams(customEmoji); const [isPlaceholder, src, uniqueId] = getInputCustomEmojiParams(customEmoji);
const className = buildClassName(
'custom-emoji',
'emoji',
'emoji-small',
isPlaceholder && 'placeholder',
customEmoji?.shouldUseTextColor && 'colorable',
);
return `<img return `<img
class="custom-emoji emoji emoji-small ${isPlaceholder ? 'placeholder' : ''}" class="${className}"
draggable="false" draggable="false"
alt="${rawText}" alt="${rawText}"
data-document-id="${entity.documentId}" data-document-id="${entity.documentId}"

View File

@ -1,7 +1,8 @@
import { import {
useCallback, useEffect, useRef, useCallback, useEffect, useRef,
} from '../../../../lib/teact/teact'; } from '../../../../lib/teact/teact';
import { requestMeasure } from '../../../../lib/fasterdom/fasterdom'; import { requestMeasure, requestMutation } from '../../../../lib/fasterdom/fasterdom';
import { ensureRLottie } from '../../../../lib/rlottie/RLottie.async';
import type { ApiSticker } from '../../../../api/types'; import type { ApiSticker } from '../../../../api/types';
import type { Signal } from '../../../../util/signals'; import type { Signal } from '../../../../util/signals';
@ -16,13 +17,14 @@ import {
import { round } from '../../../../util/math'; import { round } from '../../../../util/math';
import AbsoluteVideo from '../../../../util/AbsoluteVideo'; import AbsoluteVideo from '../../../../util/AbsoluteVideo';
import { REM } from '../../../common/helpers/mediaDimensions'; import { REM } from '../../../common/helpers/mediaDimensions';
import { hexToRgb } from '../../../../util/switchTheme';
import useColorFilter from '../../../../hooks/stickers/useColorFilter';
import useResizeObserver from '../../../../hooks/useResizeObserver'; import useResizeObserver from '../../../../hooks/useResizeObserver';
import useBackgroundMode from '../../../../hooks/useBackgroundMode'; import useBackgroundMode from '../../../../hooks/useBackgroundMode';
import useThrottledCallback from '../../../../hooks/useThrottledCallback'; import useThrottledCallback from '../../../../hooks/useThrottledCallback';
import useDynamicColorListener from '../../../../hooks/useDynamicColorListener'; import useDynamicColorListener from '../../../../hooks/stickers/useDynamicColorListener';
import useEffectWithPrevDeps from '../../../../hooks/useEffectWithPrevDeps'; import useEffectWithPrevDeps from '../../../../hooks/useEffectWithPrevDeps';
import { ensureRLottie } from '../../../../lib/rlottie/RLottie.async';
const SIZE = 1.25 * REM; const SIZE = 1.25 * REM;
const THROTTLE_MS = 300; const THROTTLE_MS = 300;
@ -44,7 +46,8 @@ export default function useInputCustomEmojis(
canPlayAnimatedEmojis: boolean, canPlayAnimatedEmojis: boolean,
isActive?: boolean, isActive?: boolean,
) { ) {
const { rgbColor: textColor } = useDynamicColorListener(inputRef); const customColor = useDynamicColorListener(inputRef);
const colorFilter = useColorFilter(customColor, true);
const playersById = useRef<Map<string, CustomEmojiPlayer>>(new Map()); const playersById = useRef<Map<string, CustomEmojiPlayer>>(new Map());
const clearPlayers = useCallback((ids: string[]) => { const clearPlayers = useCallback((ids: string[]) => {
@ -59,6 +62,11 @@ export default function useInputCustomEmojis(
const synchronizeElements = useCallback(() => { const synchronizeElements = useCallback(() => {
if (!inputRef.current || !sharedCanvasRef.current || !sharedCanvasHqRef.current) return; if (!inputRef.current || !sharedCanvasRef.current || !sharedCanvasHqRef.current) return;
requestMutation(() => {
document.documentElement.style.setProperty('--input-custom-emoji-filter', colorFilter || 'none');
});
const global = getGlobal(); const global = getGlobal();
const playerIdsToClear = new Set(playersById.current.keys()); const playerIdsToClear = new Set(playersById.current.keys());
const customEmojis = Array.from(inputRef.current.querySelectorAll<HTMLElement>('.custom-emoji')); const customEmojis = Array.from(inputRef.current.querySelectorAll<HTMLElement>('.custom-emoji'));
@ -67,7 +75,7 @@ export default function useInputCustomEmojis(
if (!element.dataset.uniqueId) { if (!element.dataset.uniqueId) {
return; return;
} }
const playerId = `${prefixId}${element.dataset.uniqueId}${textColor?.join(',') || ''}`; const playerId = `${prefixId}${element.dataset.uniqueId}${customColor || ''}`;
const documentId = element.dataset.documentId!; const documentId = element.dataset.documentId!;
playerIdsToClear.delete(playerId); playerIdsToClear.delete(playerId);
@ -77,50 +85,53 @@ export default function useInputCustomEmojis(
return; return;
} }
const canvasBounds = sharedCanvasRef.current!.getBoundingClientRect(); requestMeasure(() => {
const elementBounds = element.getBoundingClientRect(); const canvasBounds = sharedCanvasRef.current!.getBoundingClientRect();
const x = round((elementBounds.left - canvasBounds.left) / canvasBounds.width, 4); const elementBounds = element.getBoundingClientRect();
const y = round((elementBounds.top - canvasBounds.top) / canvasBounds.height, 4); const x = round((elementBounds.left - canvasBounds.left) / canvasBounds.width, 4);
const y = round((elementBounds.top - canvasBounds.top) / canvasBounds.height, 4);
if (playersById.current.has(playerId)) { if (playersById.current.has(playerId)) {
const player = playersById.current.get(playerId)!; const player = playersById.current.get(playerId)!;
player.updatePosition(x, y); player.updatePosition(x, y);
return; return;
}
const customEmoji = global.customEmojis.byId[documentId];
if (!customEmoji) {
return;
}
const isHq = customEmoji?.stickerSetInfo && selectIsAlwaysHighPriorityEmoji(global, customEmoji.stickerSetInfo);
const renderId = [
prefixId, documentId, textColor?.join(','),
].filter(Boolean).join('_');
createPlayer({
customEmoji,
sharedCanvasRef,
sharedCanvasHqRef,
absoluteContainerRef,
renderId,
viewId: playerId,
mediaUrl,
isHq,
position: { x, y },
textColor,
}).then((animation) => {
if (canPlayAnimatedEmojis) {
animation.play();
} }
playersById.current.set(playerId, animation); const customEmoji = global.customEmojis.byId[documentId];
if (!customEmoji) {
return;
}
const isHq = customEmoji?.stickerSetInfo && selectIsAlwaysHighPriorityEmoji(global, customEmoji.stickerSetInfo);
const renderId = [
prefixId, documentId, customColor,
].filter(Boolean).join('_');
createPlayer({
customEmoji,
sharedCanvasRef,
sharedCanvasHqRef,
absoluteContainerRef,
renderId,
viewId: playerId,
mediaUrl,
isHq,
position: { x, y },
textColor: customColor,
colorFilter,
}).then((animation) => {
if (canPlayAnimatedEmojis) {
animation.play();
}
playersById.current.set(playerId, animation);
});
}); });
}); });
clearPlayers(Array.from(playerIdsToClear)); clearPlayers(Array.from(playerIdsToClear));
}, [ }, [
inputRef, sharedCanvasRef, sharedCanvasHqRef, clearPlayers, prefixId, textColor, absoluteContainerRef, inputRef, sharedCanvasRef, sharedCanvasHqRef, clearPlayers, prefixId, customColor, absoluteContainerRef,
canPlayAnimatedEmojis, canPlayAnimatedEmojis, colorFilter,
]); ]);
useEffect(() => { useEffect(() => {
@ -143,11 +154,11 @@ export default function useInputCustomEmojis(
}); });
}, [getHtml, synchronizeElements, inputRef, clearPlayers, sharedCanvasRef, isActive]); }, [getHtml, synchronizeElements, inputRef, clearPlayers, sharedCanvasRef, isActive]);
useEffectWithPrevDeps(([prevTextColor]) => { useEffectWithPrevDeps(([prevCustomColor]) => {
if (textColor !== prevTextColor) { if (customColor !== prevCustomColor) {
synchronizeElements(); synchronizeElements();
} }
}, [textColor, synchronizeElements]); }, [customColor, synchronizeElements]);
const throttledSynchronizeElements = useThrottledCallback( const throttledSynchronizeElements = useThrottledCallback(
synchronizeElements, synchronizeElements,
@ -194,6 +205,7 @@ async function createPlayer({
position, position,
isHq, isHq,
textColor, textColor,
colorFilter,
}: { }: {
customEmoji: ApiSticker; customEmoji: ApiSticker;
sharedCanvasRef: React.RefObject<HTMLCanvasElement>; sharedCanvasRef: React.RefObject<HTMLCanvasElement>;
@ -204,9 +216,11 @@ async function createPlayer({
mediaUrl: string; mediaUrl: string;
position: { x: number; y: number }; position: { x: number; y: number };
isHq?: boolean; isHq?: boolean;
textColor?: [number, number, number]; textColor?: string;
colorFilter?: string;
}): Promise<CustomEmojiPlayer> { }): Promise<CustomEmojiPlayer> {
if (customEmoji.isLottie) { if (customEmoji.isLottie) {
const color = customEmoji.shouldUseTextColor && textColor ? hexToRgb(textColor) : undefined;
const RLottie = await ensureRLottie(); const RLottie = await ensureRLottie();
const lottie = RLottie.init( const lottie = RLottie.init(
mediaUrl, mediaUrl,
@ -218,7 +232,7 @@ async function createPlayer({
isLowPriority: !isHq, isLowPriority: !isHq,
}, },
viewId, viewId,
customEmoji.shouldUseTextColor ? textColor : undefined, color ? [color.r, color.g, color.b] : undefined,
); );
return { return {
@ -232,7 +246,12 @@ async function createPlayer({
} }
if (customEmoji.isVideo) { if (customEmoji.isVideo) {
const absoluteVideo = new AbsoluteVideo(mediaUrl, absoluteContainerRef.current!, { size: SIZE, position }); const style = colorFilter ? `filter: ${colorFilter};` : undefined;
const absoluteVideo = new AbsoluteVideo(
mediaUrl,
absoluteContainerRef.current!,
{ size: SIZE, position, style },
);
return { return {
play: () => absoluteVideo.play(), play: () => absoluteVideo.play(),
pause: () => absoluteVideo.pause(), pause: () => absoluteVideo.pause(),

View File

@ -147,7 +147,7 @@ const RightSearch: FC<OwnProps & StateProps> = ({
/> />
<div className="info"> <div className="info">
<div className="search-result-message-top"> <div className="search-result-message-top">
<FullNameTitle peer={(senderUser || senderChat)!} /> <FullNameTitle peer={(senderUser || senderChat)!} withEmojiStatus />
<LastMessageMeta message={message} /> <LastMessageMeta message={message} />
</div> </div>
<div className="subtitle" dir="auto"> <div className="subtitle" dir="auto">

View File

@ -0,0 +1,94 @@
import { useEffect } from '../../lib/teact/teact';
import { hexToRgb } from '../../util/switchTheme';
const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
const SVG_MAP = new Map<string, SvgColorFilter>();
class SvgColorFilter {
public filterId: string;
public element: SVGSVGElement;
private referenceCount = 0;
constructor(public color: string) {
this.filterId = `color-filter-${color.slice(1)}`;
this.element = document.createElementNS(SVG_NAMESPACE, 'svg');
this.element.width.baseVal.valueAsString = '0px';
this.element.height.baseVal.valueAsString = '0px';
const defs = document.createElementNS(SVG_NAMESPACE, 'defs');
this.element.appendChild(defs);
const filter = document.createElementNS(SVG_NAMESPACE, 'filter');
filter.id = this.filterId;
filter.setAttribute('color-interpolation-filters', 'sRGB');
defs.appendChild(filter);
const feColorMatrix = document.createElementNS(SVG_NAMESPACE, 'feColorMatrix');
feColorMatrix.setAttribute('type', 'matrix');
const rgbColor = hexToRgb(color);
feColorMatrix.setAttribute(
'values',
`0 0 0 0 ${rgbColor.r / 255} 0 0 0 0 ${rgbColor.g / 255} 0 0 0 0 ${rgbColor.b / 255} 0 0 0 1 0`,
);
filter.appendChild(feColorMatrix);
document.body.appendChild(this.element);
}
public getFilterId() {
this.referenceCount += 1;
return this.filterId;
}
public removeReference() {
this.referenceCount -= 1;
if (this.referenceCount === 0) {
this.element.remove();
}
}
public isUsed() {
return this.referenceCount > 0;
}
}
export default function useColorFilter(color?: string, asValue?: boolean) {
useEffect(() => {
if (!color) return undefined;
return () => {
const colorFilter = SVG_MAP.get(color);
if (colorFilter) {
colorFilter.removeReference();
if (!colorFilter.isUsed()) {
SVG_MAP.delete(colorFilter.color);
}
}
};
}, [color]);
if (!color) return undefined;
if (SVG_MAP.has(color)) {
const svg = SVG_MAP.get(color)!;
return prepareStyle(svg.getFilterId(), asValue);
}
const svg = new SvgColorFilter(color);
SVG_MAP.set(color, svg);
return prepareStyle(svg.getFilterId(), asValue);
}
function prepareStyle(filterId: string, asValue?: boolean) {
if (asValue) {
return `url(#${filterId})`;
}
return `filter: url(#${filterId});`;
}

View File

@ -1,20 +1,17 @@
import { import {
useCallback, useEffect, useLayoutEffect, useRef, useState, useCallback, useEffect, useLayoutEffect, useState,
} from '../lib/teact/teact'; } from '../../lib/teact/teact';
import { hexToRgb } from '../util/switchTheme'; import { getPropertyHexColor } from '../../util/themeStyle';
import { getPropertyHexColor } from '../util/themeStyle'; import useResizeObserver from '../useResizeObserver';
import useResizeObserver from './useResizeObserver';
import useSyncEffect from './useSyncEffect';
// Transition required to detect `color` property change. // Transition required to detect `color` property change.
// Duration parameter describes a delay between color change and color state update. // Duration parameter describes a delay between color change and color state update.
// Small values may cause large amount of re-renders. // Small values may cause large amount of re-renders.
const TRANSITION_PROPERTY = 'color'; const TRANSITION_PROPERTY = 'color';
const TRANSITION_STYLE = `60ms ${TRANSITION_PROPERTY} linear`; const TRANSITION_STYLE = `50ms ${TRANSITION_PROPERTY} linear`;
export default function useDynamicColorListener(ref: React.RefObject<HTMLElement>, isDisabled?: boolean) { export default function useDynamicColorListener(ref: React.RefObject<HTMLElement>, isDisabled?: boolean) {
const [hexColor, setHexColor] = useState<string | undefined>(); const [hexColor, setHexColor] = useState<string | undefined>();
const rgbColorRef = useRef<[number, number, number] | undefined>();
const updateColor = useCallback(() => { const updateColor = useCallback(() => {
if (!ref.current || isDisabled) { if (!ref.current || isDisabled) {
@ -30,17 +27,6 @@ export default function useDynamicColorListener(ref: React.RefObject<HTMLElement
// We will receive `resize` event when parent is shown again. // We will receive `resize` event when parent is shown again.
useResizeObserver(ref, updateColor, isDisabled); useResizeObserver(ref, updateColor, isDisabled);
// Update RGB color only when hex color changes
useSyncEffect(() => {
if (!hexColor) {
rgbColorRef.current = undefined;
return;
}
const { r, g, b } = hexToRgb(hexColor);
rgbColorRef.current = [r, g, b];
}, [hexColor]);
useLayoutEffect(() => { useLayoutEffect(() => {
const el = ref.current; const el = ref.current;
if (!el || isDisabled) { if (!el || isDisabled) {
@ -78,8 +64,5 @@ export default function useDynamicColorListener(ref: React.RefObject<HTMLElement
}; };
}, [isDisabled, ref, updateColor]); }, [isDisabled, ref, updateColor]);
return { return hexColor;
hexColor,
rgbColor: rgbColorRef.current,
};
} }

View File

@ -236,9 +236,7 @@ class RLottie {
const frame = this.getFrame(this.prevFrameIndex) || this.getFrame(Math.round(this.approxFrameIndex)); const frame = this.getFrame(this.prevFrameIndex) || this.getFrame(Math.round(this.approxFrameIndex));
if (frame && frame !== WAITING) { if (frame && frame !== WAITING) {
requestMutation(() => { ctx.drawImage(frame, containerInfo.coords!.x, containerInfo.coords!.y);
ctx.drawImage(frame, containerInfo.coords!.x, containerInfo.coords!.y);
});
} }
} }

View File

@ -1,9 +1,11 @@
import { requestMeasure, requestMutation } from '../lib/fasterdom/fasterdom';
import safePlay from './safePlay'; import safePlay from './safePlay';
type AbsoluteVideoOptions = { type AbsoluteVideoOptions = {
position: { x: number; y: number }; position: { x: number; y: number };
noLoop?: boolean; noLoop?: boolean;
size: number; size: number;
style?: string;
}; };
export default class AbsoluteVideo { export default class AbsoluteVideo {
@ -20,6 +22,9 @@ export default class AbsoluteVideo {
this.video.src = videoUrl; this.video.src = videoUrl;
this.video.disablePictureInPicture = true; this.video.disablePictureInPicture = true;
this.video.muted = true; this.video.muted = true;
if (options.style) {
this.video.setAttribute('style', options.style);
}
this.video.style.position = 'absolute'; this.video.style.position = 'absolute';
this.video.load(); this.video.load();
@ -27,8 +32,11 @@ export default class AbsoluteVideo {
this.video.loop = true; this.video.loop = true;
} }
this.container.appendChild(this.video); requestMutation(() => {
this.recalculatePositionAndSize(); this.container.appendChild(this.video!);
this.recalculatePositionAndSize();
});
} }
public play() { public play() {
@ -60,12 +68,17 @@ export default class AbsoluteVideo {
} }
private recalculatePositionAndSize() { private recalculatePositionAndSize() {
if (!this.video) return;
const { size, position: { x, y } } = this.options; const { size, position: { x, y } } = this.options;
const { width, height } = this.container.getBoundingClientRect(); requestMeasure(() => {
this.video.style.left = `${Math.round(x * width)}px`; if (!this.video) return;
this.video.style.top = `${Math.round(y * height)}px`; const video = this.video;
this.video.style.width = `${size}px`; const { width, height } = this.container.getBoundingClientRect();
this.video.style.height = `${size}px`; requestMutation(() => {
video.style.left = `${Math.round(x * width)}px`;
video.style.top = `${Math.round(y * height)}px`;
video.style.width = `${size}px`;
video.style.height = `${size}px`;
});
});
} }
} }

View File

@ -1,4 +1,5 @@
import { addCallback } from '../lib/teact/teactn'; import { addCallback } from '../lib/teact/teactn';
import { requestMutation } from '../lib/fasterdom/fasterdom';
import { getGlobal } from '../global'; import { getGlobal } from '../global';
import { ApiMediaFormat } from '../api/types'; import { ApiMediaFormat } from '../api/types';
@ -76,12 +77,20 @@ function processDomForCustomEmoji() {
} }
const [isPlaceholder, src, uniqueId] = getInputCustomEmojiParams(customEmoji); const [isPlaceholder, src, uniqueId] = getInputCustomEmojiParams(customEmoji);
if (!isPlaceholder) { if (customEmoji.shouldUseTextColor && !emoji.classList.contains('colorable')) {
emoji.src = src; requestMutation(() => {
emoji.classList.remove('placeholder'); emoji.classList.add('colorable');
if (uniqueId) emoji.dataset.uniqueId = uniqueId; });
}
callInputRenderHandlers(customEmoji.id); if (!isPlaceholder) {
requestMutation(() => {
emoji.src = src;
emoji.classList.remove('placeholder');
if (uniqueId) emoji.dataset.uniqueId = uniqueId;
callInputRenderHandlers(customEmoji.id);
});
} }
}); });
} }