Custom Emoji: Support adaptive static and video packs (#3118)
This commit is contained in:
parent
cebbda28cf
commit
a71a3bc53b
@ -128,6 +128,7 @@ export function buildStickerSet(set: GramJs.StickerSet): ApiStickerSet {
|
||||
accessHash: String(accessHash),
|
||||
title,
|
||||
hasThumbnail: Boolean(thumbs?.length || thumbDocumentId),
|
||||
thumbCustomEmojiId: thumbDocumentId?.toString(),
|
||||
count,
|
||||
shortName,
|
||||
};
|
||||
|
||||
@ -57,6 +57,7 @@ export interface ApiStickerSet {
|
||||
accessHash: string;
|
||||
title: string;
|
||||
hasThumbnail?: boolean;
|
||||
thumbCustomEmojiId?: string;
|
||||
count: number;
|
||||
stickers?: ApiSticker[];
|
||||
packs?: Record<string, ApiSticker[]>;
|
||||
|
||||
@ -51,11 +51,11 @@ const OutlinedMicrophoneIcon: FC<OwnProps> = ({
|
||||
// eslint-disable-next-line
|
||||
}, [isMuted, shouldRaiseHand, isRaiseHand]);
|
||||
|
||||
const microphoneColor: [number, number, number] | undefined = useMemo(() => {
|
||||
return noColor ? [0xff, 0xff, 0xff] : (
|
||||
isRaiseHand ? [0x4d, 0xa6, 0xe0]
|
||||
: (shouldRaiseHand || isMutedByMe ? [0xFF, 0x70, 0x6F] : (
|
||||
isSpeaking ? [0x57, 0xBC, 0x6C] : [0x84, 0x8D, 0x94]
|
||||
const microphoneColor: string | undefined = useMemo(() => {
|
||||
return noColor ? '#ffffff' : (
|
||||
isRaiseHand ? '#4da6e0'
|
||||
: (shouldRaiseHand || isMutedByMe ? '#ff706f' : (
|
||||
isSpeaking ? '#57bc6c' : '#848d94'
|
||||
))
|
||||
);
|
||||
}, [noColor, isRaiseHand, shouldRaiseHand, isMutedByMe, isSpeaking]);
|
||||
|
||||
@ -11,6 +11,7 @@ import React, {
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import buildStyle from '../../util/buildStyle';
|
||||
import generateIdFor from '../../util/generateIdFor';
|
||||
import { hexToRgb } from '../../util/switchTheme';
|
||||
|
||||
import useHeavyAnimationCheck, { isHeavyAnimating } from '../../hooks/useHeavyAnimationCheck';
|
||||
import usePriorityPlaybackCheck, { isPriorityPlaybackActive } from '../../hooks/usePriorityPlaybackCheck';
|
||||
@ -19,6 +20,8 @@ import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps';
|
||||
import { useStateRef } from '../../hooks/useStateRef';
|
||||
import useSharedIntersectionObserver from '../../hooks/useSharedIntersectionObserver';
|
||||
import useThrottledCallback from '../../hooks/useThrottledCallback';
|
||||
import useColorFilter from '../../hooks/stickers/useColorFilter';
|
||||
import useSyncEffect from '../../hooks/useSyncEffect';
|
||||
|
||||
export type OwnProps = {
|
||||
ref?: RefObject<HTMLDivElement>;
|
||||
@ -32,7 +35,7 @@ export type OwnProps = {
|
||||
noLoop?: boolean;
|
||||
size: number;
|
||||
quality?: number;
|
||||
color?: [number, number, number];
|
||||
color?: string;
|
||||
isLowPriority?: boolean;
|
||||
forceOnHeavyAnimation?: boolean;
|
||||
sharedCanvas?: HTMLCanvasElement;
|
||||
@ -80,10 +83,24 @@ const AnimatedSticker: FC<OwnProps> = ({
|
||||
const animationRef = useRef<RLottieInstance>();
|
||||
const isFirstRender = useRef(true);
|
||||
|
||||
const shouldUseColorFilter = !sharedCanvas && color;
|
||||
const colorFilter = useColorFilter(shouldUseColorFilter ? color : undefined);
|
||||
|
||||
const playKey = play || playSegment;
|
||||
const playRef = useStateRef(play);
|
||||
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);
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@ -118,7 +135,7 @@ const AnimatedSticker: FC<OwnProps> = ({
|
||||
coords: sharedCanvasCoords,
|
||||
},
|
||||
viewId,
|
||||
color,
|
||||
rgbColor.current,
|
||||
onLoad,
|
||||
onEnded,
|
||||
onLoop,
|
||||
@ -131,7 +148,7 @@ const AnimatedSticker: FC<OwnProps> = ({
|
||||
setAnimation(newAnimation);
|
||||
animationRef.current = newAnimation;
|
||||
}, [
|
||||
color, isLowPriority, noLoop, onEnded, onLoad, onLoop, quality,
|
||||
isLowPriority, noLoop, onEnded, onLoad, onLoop, quality,
|
||||
renderId, sharedCanvas, sharedCanvasCoords, size, speed, tgsUrl, viewId,
|
||||
]);
|
||||
|
||||
@ -149,7 +166,7 @@ const AnimatedSticker: FC<OwnProps> = ({
|
||||
useEffect(() => {
|
||||
if (!animation) return;
|
||||
|
||||
animation.setColor(color);
|
||||
animation.setColor(rgbColor.current);
|
||||
}, [color, animation]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -239,6 +256,7 @@ const AnimatedSticker: FC<OwnProps> = ({
|
||||
style={buildStyle(
|
||||
size !== undefined && `width: ${size}px; height: ${size}px;`,
|
||||
onClick && 'cursor: pointer',
|
||||
colorFilter,
|
||||
style,
|
||||
)}
|
||||
onClick={onClick}
|
||||
|
||||
@ -12,7 +12,7 @@ import safePlay from '../../util/safePlay';
|
||||
import { selectIsAlwaysHighPriorityEmoji } from '../../global/selectors';
|
||||
|
||||
import useCustomEmoji from './hooks/useCustomEmoji';
|
||||
import useDynamicColorListener from '../../hooks/useDynamicColorListener';
|
||||
import useDynamicColorListener from '../../hooks/stickers/useDynamicColorListener';
|
||||
|
||||
import StickerView from './StickerView';
|
||||
|
||||
@ -77,7 +77,7 @@ const CustomEmoji: FC<OwnProps> = ({
|
||||
const [shouldLoop, setShouldLoop] = useState(true);
|
||||
|
||||
const hasCustomColor = customEmoji?.shouldUseTextColor;
|
||||
const { rgbColor: customColor } = useDynamicColorListener(containerRef, !hasCustomColor);
|
||||
const customColor = useDynamicColorListener(containerRef, !hasCustomColor);
|
||||
|
||||
const handleVideoEnded = useCallback((e) => {
|
||||
if (!loopLimit) return;
|
||||
@ -132,7 +132,6 @@ const CustomEmoji: FC<OwnProps> = ({
|
||||
isSmall={!isBig}
|
||||
size={size}
|
||||
noPlay={noPlay || !canPlay}
|
||||
customColor={customColor}
|
||||
thumbClassName={styles.thumb}
|
||||
fullMediaClassName={styles.media}
|
||||
shouldLoop={shouldLoop}
|
||||
@ -146,6 +145,7 @@ const CustomEmoji: FC<OwnProps> = ({
|
||||
withTranslucentThumb={withTranslucentThumb}
|
||||
onVideoEnded={handleVideoEnded}
|
||||
onAnimatedStickerLoop={handleStickerLoop}
|
||||
customColor={customColor}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -6,4 +6,8 @@
|
||||
> h3 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:global(.custom-emoji) {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@ import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
|
||||
import useMenuPosition from '../../hooks/useMenuPosition';
|
||||
import useDynamicColorListener from '../../hooks/useDynamicColorListener';
|
||||
import useDynamicColorListener from '../../hooks/stickers/useDynamicColorListener';
|
||||
|
||||
import StickerView from './StickerView';
|
||||
import Button from '../ui/Button';
|
||||
@ -93,7 +93,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const lang = useLang();
|
||||
const hasCustomColor = sticker.shouldUseTextColor;
|
||||
const { rgbColor: customColor } = useDynamicColorListener(ref, !hasCustomColor);
|
||||
const customColor = useDynamicColorListener(ref, !hasCustomColor);
|
||||
|
||||
const {
|
||||
id, isCustomEmoji, hasEffect: isPremium, stickerSetInfo,
|
||||
|
||||
@ -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 type { FC } from '../../lib/teact/teact';
|
||||
@ -19,6 +21,7 @@ import useMediaTransition from '../../hooks/useMediaTransition';
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
import useCoordsInSharedCanvas from '../../hooks/useCoordsInSharedCanvas';
|
||||
import useHeavyAnimationCheck, { isHeavyAnimating } from '../../hooks/useHeavyAnimationCheck';
|
||||
import useColorFilter from '../../hooks/stickers/useColorFilter';
|
||||
|
||||
import AnimatedSticker from './AnimatedSticker';
|
||||
import OptimizedVideo from '../ui/OptimizedVideo';
|
||||
@ -33,7 +36,7 @@ type OwnProps = {
|
||||
fullMediaClassName?: string;
|
||||
isSmall?: boolean;
|
||||
size?: number;
|
||||
customColor?: [number, number, number];
|
||||
customColor?: string;
|
||||
loopLimit?: number;
|
||||
shouldLoop?: boolean;
|
||||
shouldPreloadPreview?: boolean;
|
||||
@ -86,6 +89,8 @@ const StickerView: FC<OwnProps> = ({
|
||||
const isStatic = !isLottie && !isVideo;
|
||||
const previewMediaHash = getStickerPreviewHash(sticker.id);
|
||||
|
||||
const filterStyle = useColorFilter(customColor);
|
||||
|
||||
const isIntersectingForLoading = useIsIntersecting(containerRef, observeIntersectionForLoading);
|
||||
const shouldLoad = isIntersectingForLoading && !noLoad;
|
||||
const isIntersectingForPlaying = (
|
||||
@ -128,7 +133,10 @@ const StickerView: FC<OwnProps> = ({
|
||||
|
||||
const randomIdPrefix = useMemo(() => generateIdFor(ID_STORE, true), []);
|
||||
const renderId = [
|
||||
(withSharedAnimation ? SHARED_PREFIX : randomIdPrefix), id, size, customColor?.join(','),
|
||||
(withSharedAnimation ? SHARED_PREFIX : randomIdPrefix),
|
||||
id,
|
||||
size,
|
||||
(withSharedAnimation ? customColor : undefined),
|
||||
].filter(Boolean).join('_');
|
||||
|
||||
return (
|
||||
@ -159,7 +167,6 @@ const StickerView: FC<OwnProps> = ({
|
||||
)}
|
||||
tgsUrl={fullMediaData}
|
||||
play={shouldPlay}
|
||||
color={customColor}
|
||||
noLoop={!shouldLoop}
|
||||
forceOnHeavyAnimation={forceOnHeavyAnimation}
|
||||
isLowPriority={isSmall && !selectIsAlwaysHighPriorityEmoji(getGlobal(), stickerSetInfo)}
|
||||
@ -168,6 +175,7 @@ const StickerView: FC<OwnProps> = ({
|
||||
onLoad={markPlayerReady}
|
||||
onLoop={onAnimatedStickerLoop}
|
||||
onEnded={onAnimatedStickerLoop}
|
||||
color={customColor}
|
||||
/>
|
||||
) : isVideo ? (
|
||||
<OptimizedVideo
|
||||
@ -180,12 +188,14 @@ const StickerView: FC<OwnProps> = ({
|
||||
disablePictureInPicture
|
||||
onReady={markPlayerReady}
|
||||
onEnded={onVideoEnded}
|
||||
style={filterStyle}
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
className={buildClassName(styles.media, fullMediaClassName, fullMediaClassNames, 'sticker-media')}
|
||||
src={fullMediaData}
|
||||
alt={emoji}
|
||||
style={filterStyle}
|
||||
draggable={false}
|
||||
/>
|
||||
))}
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
width: calc(100vw - 1rem);
|
||||
}
|
||||
|
||||
:global(.custom-emoji) {
|
||||
:global(.sticker-set-cover), :global(.custom-emoji) {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
@ -462,6 +462,10 @@
|
||||
.custom-emoji {
|
||||
margin: 0;
|
||||
vertical-align: text-top;
|
||||
|
||||
&.colorable {
|
||||
filter: var(--input-custom-emoji-filter);
|
||||
}
|
||||
}
|
||||
|
||||
// Workaround to preserve correct input height
|
||||
|
||||
@ -16,6 +16,9 @@ import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
|
||||
import useMedia from '../../../hooks/useMedia';
|
||||
import useMediaTransition from '../../../hooks/useMediaTransition';
|
||||
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 OptimizedVideo from '../../ui/OptimizedVideo';
|
||||
@ -41,7 +44,14 @@ const StickerSetCover: FC<OwnProps> = ({
|
||||
// eslint-disable-next-line no-null/no-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 shouldPlay = isIntersecting && !noPlay;
|
||||
@ -86,12 +96,14 @@ const StickerSetCover: FC<OwnProps> = ({
|
||||
className={buildClassName(styles.video, transitionClassNames)}
|
||||
src={mediaData}
|
||||
canPlay={shouldPlay}
|
||||
style={colorFilter}
|
||||
loop
|
||||
disablePictureInPicture
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={mediaData || staticMediaData}
|
||||
style={colorFilter}
|
||||
className={buildClassName(styles.image, transitionClassNames)}
|
||||
alt=""
|
||||
/>
|
||||
|
||||
@ -161,7 +161,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.StickerButton.custom-emoji {
|
||||
.StickerButton.custom-emoji, .sticker-set-cover {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
|
||||
@ -5,14 +5,19 @@ import { getGlobal } from '../../../../global';
|
||||
import { EMOJI_SIZES } from '../../../../config';
|
||||
import { REM } from '../../../common/helpers/mediaDimensions';
|
||||
import { getInputCustomEmojiParams } from '../../../../util/customEmojiManager';
|
||||
import buildClassName from '../../../../util/buildClassName';
|
||||
|
||||
export const INPUT_CUSTOM_EMOJI_SELECTOR = 'img[data-document-id]';
|
||||
|
||||
export function buildCustomEmojiHtml(emoji: ApiSticker) {
|
||||
const [isPlaceholder, src, uniqueId] = getInputCustomEmojiParams(emoji);
|
||||
|
||||
const className = buildClassName(
|
||||
'custom-emoji', 'emoji', 'emoji-small', isPlaceholder && 'placeholder', emoji.shouldUseTextColor && 'colorable',
|
||||
);
|
||||
|
||||
return `<img
|
||||
class="custom-emoji emoji emoji-small ${isPlaceholder ? 'placeholder' : ''}"
|
||||
class="${className}"
|
||||
draggable="false"
|
||||
alt="${emoji.emoji}"
|
||||
data-document-id="${emoji.id}"
|
||||
@ -25,8 +30,17 @@ export function buildCustomEmojiHtml(emoji: ApiSticker) {
|
||||
export function buildCustomEmojiHtmlFromEntity(rawText: string, entity: ApiMessageEntityCustomEmoji) {
|
||||
const customEmoji = getGlobal().customEmojis.byId[entity.documentId];
|
||||
const [isPlaceholder, src, uniqueId] = getInputCustomEmojiParams(customEmoji);
|
||||
|
||||
const className = buildClassName(
|
||||
'custom-emoji',
|
||||
'emoji',
|
||||
'emoji-small',
|
||||
isPlaceholder && 'placeholder',
|
||||
customEmoji?.shouldUseTextColor && 'colorable',
|
||||
);
|
||||
|
||||
return `<img
|
||||
class="custom-emoji emoji emoji-small ${isPlaceholder ? 'placeholder' : ''}"
|
||||
class="${className}"
|
||||
draggable="false"
|
||||
alt="${rawText}"
|
||||
data-document-id="${entity.documentId}"
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import {
|
||||
useCallback, useEffect, useRef,
|
||||
} 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 { Signal } from '../../../../util/signals';
|
||||
@ -16,13 +17,14 @@ import {
|
||||
import { round } from '../../../../util/math';
|
||||
import AbsoluteVideo from '../../../../util/AbsoluteVideo';
|
||||
import { REM } from '../../../common/helpers/mediaDimensions';
|
||||
import { hexToRgb } from '../../../../util/switchTheme';
|
||||
import useColorFilter from '../../../../hooks/stickers/useColorFilter';
|
||||
|
||||
import useResizeObserver from '../../../../hooks/useResizeObserver';
|
||||
import useBackgroundMode from '../../../../hooks/useBackgroundMode';
|
||||
import useThrottledCallback from '../../../../hooks/useThrottledCallback';
|
||||
import useDynamicColorListener from '../../../../hooks/useDynamicColorListener';
|
||||
import useDynamicColorListener from '../../../../hooks/stickers/useDynamicColorListener';
|
||||
import useEffectWithPrevDeps from '../../../../hooks/useEffectWithPrevDeps';
|
||||
import { ensureRLottie } from '../../../../lib/rlottie/RLottie.async';
|
||||
|
||||
const SIZE = 1.25 * REM;
|
||||
const THROTTLE_MS = 300;
|
||||
@ -44,7 +46,8 @@ export default function useInputCustomEmojis(
|
||||
canPlayAnimatedEmojis: 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 clearPlayers = useCallback((ids: string[]) => {
|
||||
@ -59,6 +62,11 @@ export default function useInputCustomEmojis(
|
||||
|
||||
const synchronizeElements = useCallback(() => {
|
||||
if (!inputRef.current || !sharedCanvasRef.current || !sharedCanvasHqRef.current) return;
|
||||
|
||||
requestMutation(() => {
|
||||
document.documentElement.style.setProperty('--input-custom-emoji-filter', colorFilter || 'none');
|
||||
});
|
||||
|
||||
const global = getGlobal();
|
||||
const playerIdsToClear = new Set(playersById.current.keys());
|
||||
const customEmojis = Array.from(inputRef.current.querySelectorAll<HTMLElement>('.custom-emoji'));
|
||||
@ -67,7 +75,7 @@ export default function useInputCustomEmojis(
|
||||
if (!element.dataset.uniqueId) {
|
||||
return;
|
||||
}
|
||||
const playerId = `${prefixId}${element.dataset.uniqueId}${textColor?.join(',') || ''}`;
|
||||
const playerId = `${prefixId}${element.dataset.uniqueId}${customColor || ''}`;
|
||||
const documentId = element.dataset.documentId!;
|
||||
|
||||
playerIdsToClear.delete(playerId);
|
||||
@ -77,50 +85,53 @@ export default function useInputCustomEmojis(
|
||||
return;
|
||||
}
|
||||
|
||||
const canvasBounds = sharedCanvasRef.current!.getBoundingClientRect();
|
||||
const elementBounds = element.getBoundingClientRect();
|
||||
const x = round((elementBounds.left - canvasBounds.left) / canvasBounds.width, 4);
|
||||
const y = round((elementBounds.top - canvasBounds.top) / canvasBounds.height, 4);
|
||||
requestMeasure(() => {
|
||||
const canvasBounds = sharedCanvasRef.current!.getBoundingClientRect();
|
||||
const elementBounds = element.getBoundingClientRect();
|
||||
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)) {
|
||||
const player = playersById.current.get(playerId)!;
|
||||
player.updatePosition(x, y);
|
||||
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();
|
||||
if (playersById.current.has(playerId)) {
|
||||
const player = playersById.current.get(playerId)!;
|
||||
player.updatePosition(x, y);
|
||||
return;
|
||||
}
|
||||
|
||||
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));
|
||||
}, [
|
||||
inputRef, sharedCanvasRef, sharedCanvasHqRef, clearPlayers, prefixId, textColor, absoluteContainerRef,
|
||||
canPlayAnimatedEmojis,
|
||||
inputRef, sharedCanvasRef, sharedCanvasHqRef, clearPlayers, prefixId, customColor, absoluteContainerRef,
|
||||
canPlayAnimatedEmojis, colorFilter,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -143,11 +154,11 @@ export default function useInputCustomEmojis(
|
||||
});
|
||||
}, [getHtml, synchronizeElements, inputRef, clearPlayers, sharedCanvasRef, isActive]);
|
||||
|
||||
useEffectWithPrevDeps(([prevTextColor]) => {
|
||||
if (textColor !== prevTextColor) {
|
||||
useEffectWithPrevDeps(([prevCustomColor]) => {
|
||||
if (customColor !== prevCustomColor) {
|
||||
synchronizeElements();
|
||||
}
|
||||
}, [textColor, synchronizeElements]);
|
||||
}, [customColor, synchronizeElements]);
|
||||
|
||||
const throttledSynchronizeElements = useThrottledCallback(
|
||||
synchronizeElements,
|
||||
@ -194,6 +205,7 @@ async function createPlayer({
|
||||
position,
|
||||
isHq,
|
||||
textColor,
|
||||
colorFilter,
|
||||
}: {
|
||||
customEmoji: ApiSticker;
|
||||
sharedCanvasRef: React.RefObject<HTMLCanvasElement>;
|
||||
@ -204,9 +216,11 @@ async function createPlayer({
|
||||
mediaUrl: string;
|
||||
position: { x: number; y: number };
|
||||
isHq?: boolean;
|
||||
textColor?: [number, number, number];
|
||||
textColor?: string;
|
||||
colorFilter?: string;
|
||||
}): Promise<CustomEmojiPlayer> {
|
||||
if (customEmoji.isLottie) {
|
||||
const color = customEmoji.shouldUseTextColor && textColor ? hexToRgb(textColor) : undefined;
|
||||
const RLottie = await ensureRLottie();
|
||||
const lottie = RLottie.init(
|
||||
mediaUrl,
|
||||
@ -218,7 +232,7 @@ async function createPlayer({
|
||||
isLowPriority: !isHq,
|
||||
},
|
||||
viewId,
|
||||
customEmoji.shouldUseTextColor ? textColor : undefined,
|
||||
color ? [color.r, color.g, color.b] : undefined,
|
||||
);
|
||||
|
||||
return {
|
||||
@ -232,7 +246,12 @@ async function createPlayer({
|
||||
}
|
||||
|
||||
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 {
|
||||
play: () => absoluteVideo.play(),
|
||||
pause: () => absoluteVideo.pause(),
|
||||
|
||||
@ -147,7 +147,7 @@ const RightSearch: FC<OwnProps & StateProps> = ({
|
||||
/>
|
||||
<div className="info">
|
||||
<div className="search-result-message-top">
|
||||
<FullNameTitle peer={(senderUser || senderChat)!} />
|
||||
<FullNameTitle peer={(senderUser || senderChat)!} withEmojiStatus />
|
||||
<LastMessageMeta message={message} />
|
||||
</div>
|
||||
<div className="subtitle" dir="auto">
|
||||
|
||||
94
src/hooks/stickers/useColorFilter.ts
Normal file
94
src/hooks/stickers/useColorFilter.ts
Normal 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});`;
|
||||
}
|
||||
@ -1,20 +1,17 @@
|
||||
import {
|
||||
useCallback, useEffect, useLayoutEffect, useRef, useState,
|
||||
} from '../lib/teact/teact';
|
||||
import { hexToRgb } from '../util/switchTheme';
|
||||
import { getPropertyHexColor } from '../util/themeStyle';
|
||||
import useResizeObserver from './useResizeObserver';
|
||||
import useSyncEffect from './useSyncEffect';
|
||||
useCallback, useEffect, useLayoutEffect, useState,
|
||||
} from '../../lib/teact/teact';
|
||||
import { getPropertyHexColor } from '../../util/themeStyle';
|
||||
import useResizeObserver from '../useResizeObserver';
|
||||
|
||||
// Transition required to detect `color` property change.
|
||||
// Duration parameter describes a delay between color change and color state update.
|
||||
// Small values may cause large amount of re-renders.
|
||||
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) {
|
||||
const [hexColor, setHexColor] = useState<string | undefined>();
|
||||
const rgbColorRef = useRef<[number, number, number] | undefined>();
|
||||
|
||||
const updateColor = useCallback(() => {
|
||||
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.
|
||||
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(() => {
|
||||
const el = ref.current;
|
||||
if (!el || isDisabled) {
|
||||
@ -78,8 +64,5 @@ export default function useDynamicColorListener(ref: React.RefObject<HTMLElement
|
||||
};
|
||||
}, [isDisabled, ref, updateColor]);
|
||||
|
||||
return {
|
||||
hexColor,
|
||||
rgbColor: rgbColorRef.current,
|
||||
};
|
||||
return hexColor;
|
||||
}
|
||||
@ -236,9 +236,7 @@ class RLottie {
|
||||
const frame = this.getFrame(this.prevFrameIndex) || this.getFrame(Math.round(this.approxFrameIndex));
|
||||
|
||||
if (frame && frame !== WAITING) {
|
||||
requestMutation(() => {
|
||||
ctx.drawImage(frame, containerInfo.coords!.x, containerInfo.coords!.y);
|
||||
});
|
||||
ctx.drawImage(frame, containerInfo.coords!.x, containerInfo.coords!.y);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import { requestMeasure, requestMutation } from '../lib/fasterdom/fasterdom';
|
||||
import safePlay from './safePlay';
|
||||
|
||||
type AbsoluteVideoOptions = {
|
||||
position: { x: number; y: number };
|
||||
noLoop?: boolean;
|
||||
size: number;
|
||||
style?: string;
|
||||
};
|
||||
|
||||
export default class AbsoluteVideo {
|
||||
@ -20,6 +22,9 @@ export default class AbsoluteVideo {
|
||||
this.video.src = videoUrl;
|
||||
this.video.disablePictureInPicture = true;
|
||||
this.video.muted = true;
|
||||
if (options.style) {
|
||||
this.video.setAttribute('style', options.style);
|
||||
}
|
||||
this.video.style.position = 'absolute';
|
||||
this.video.load();
|
||||
|
||||
@ -27,8 +32,11 @@ export default class AbsoluteVideo {
|
||||
this.video.loop = true;
|
||||
}
|
||||
|
||||
this.container.appendChild(this.video);
|
||||
this.recalculatePositionAndSize();
|
||||
requestMutation(() => {
|
||||
this.container.appendChild(this.video!);
|
||||
|
||||
this.recalculatePositionAndSize();
|
||||
});
|
||||
}
|
||||
|
||||
public play() {
|
||||
@ -60,12 +68,17 @@ export default class AbsoluteVideo {
|
||||
}
|
||||
|
||||
private recalculatePositionAndSize() {
|
||||
if (!this.video) return;
|
||||
const { size, position: { x, y } } = this.options;
|
||||
const { width, height } = this.container.getBoundingClientRect();
|
||||
this.video.style.left = `${Math.round(x * width)}px`;
|
||||
this.video.style.top = `${Math.round(y * height)}px`;
|
||||
this.video.style.width = `${size}px`;
|
||||
this.video.style.height = `${size}px`;
|
||||
requestMeasure(() => {
|
||||
if (!this.video) return;
|
||||
const video = this.video;
|
||||
const { width, height } = this.container.getBoundingClientRect();
|
||||
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`;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { addCallback } from '../lib/teact/teactn';
|
||||
import { requestMutation } from '../lib/fasterdom/fasterdom';
|
||||
import { getGlobal } from '../global';
|
||||
|
||||
import { ApiMediaFormat } from '../api/types';
|
||||
@ -76,12 +77,20 @@ function processDomForCustomEmoji() {
|
||||
}
|
||||
const [isPlaceholder, src, uniqueId] = getInputCustomEmojiParams(customEmoji);
|
||||
|
||||
if (!isPlaceholder) {
|
||||
emoji.src = src;
|
||||
emoji.classList.remove('placeholder');
|
||||
if (uniqueId) emoji.dataset.uniqueId = uniqueId;
|
||||
if (customEmoji.shouldUseTextColor && !emoji.classList.contains('colorable')) {
|
||||
requestMutation(() => {
|
||||
emoji.classList.add('colorable');
|
||||
});
|
||||
}
|
||||
|
||||
callInputRenderHandlers(customEmoji.id);
|
||||
if (!isPlaceholder) {
|
||||
requestMutation(() => {
|
||||
emoji.src = src;
|
||||
emoji.classList.remove('placeholder');
|
||||
if (uniqueId) emoji.dataset.uniqueId = uniqueId;
|
||||
|
||||
callInputRenderHandlers(customEmoji.id);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user