diff --git a/src/api/gramjs/apiBuilders/symbols.ts b/src/api/gramjs/apiBuilders/symbols.ts index 30d315b0a..7b6eb0320 100644 --- a/src/api/gramjs/apiBuilders/symbols.ts +++ b/src/api/gramjs/apiBuilders/symbols.ts @@ -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, }; diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index 65007cf8e..bb1972f66 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -57,6 +57,7 @@ export interface ApiStickerSet { accessHash: string; title: string; hasThumbnail?: boolean; + thumbCustomEmojiId?: string; count: number; stickers?: ApiSticker[]; packs?: Record; diff --git a/src/components/calls/group/OutlinedMicrophoneIcon.tsx b/src/components/calls/group/OutlinedMicrophoneIcon.tsx index ac54ed3e3..e44a8d6f2 100644 --- a/src/components/calls/group/OutlinedMicrophoneIcon.tsx +++ b/src/components/calls/group/OutlinedMicrophoneIcon.tsx @@ -51,11 +51,11 @@ const OutlinedMicrophoneIcon: FC = ({ // 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]); diff --git a/src/components/common/AnimatedSticker.tsx b/src/components/common/AnimatedSticker.tsx index 161046761..85944d793 100644 --- a/src/components/common/AnimatedSticker.tsx +++ b/src/components/common/AnimatedSticker.tsx @@ -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; @@ -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 = ({ const animationRef = useRef(); 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 = ({ coords: sharedCanvasCoords, }, viewId, - color, + rgbColor.current, onLoad, onEnded, onLoop, @@ -131,7 +148,7 @@ const AnimatedSticker: FC = ({ 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 = ({ useEffect(() => { if (!animation) return; - animation.setColor(color); + animation.setColor(rgbColor.current); }, [color, animation]); useEffect(() => { @@ -239,6 +256,7 @@ const AnimatedSticker: FC = ({ style={buildStyle( size !== undefined && `width: ${size}px; height: ${size}px;`, onClick && 'cursor: pointer', + colorFilter, style, )} onClick={onClick} diff --git a/src/components/common/CustomEmoji.tsx b/src/components/common/CustomEmoji.tsx index 9ec7e7b7d..957d0a598 100644 --- a/src/components/common/CustomEmoji.tsx +++ b/src/components/common/CustomEmoji.tsx @@ -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 = ({ 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 = ({ isSmall={!isBig} size={size} noPlay={noPlay || !canPlay} - customColor={customColor} thumbClassName={styles.thumb} fullMediaClassName={styles.media} shouldLoop={shouldLoop} @@ -146,6 +145,7 @@ const CustomEmoji: FC = ({ withTranslucentThumb={withTranslucentThumb} onVideoEnded={handleVideoEnded} onAnimatedStickerLoop={handleStickerLoop} + customColor={customColor} /> )} diff --git a/src/components/common/FullNameTitle.module.scss b/src/components/common/FullNameTitle.module.scss index 957253340..904000823 100644 --- a/src/components/common/FullNameTitle.module.scss +++ b/src/components/common/FullNameTitle.module.scss @@ -6,4 +6,8 @@ > h3 { margin-bottom: 0; } + + :global(.custom-emoji) { + color: var(--color-primary); + } } diff --git a/src/components/common/StickerButton.tsx b/src/components/common/StickerButton.tsx index 24a944aed..110e08493 100644 --- a/src/components/common/StickerButton.tsx +++ b/src/components/common/StickerButton.tsx @@ -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 = (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, diff --git a/src/components/common/StickerView.tsx b/src/components/common/StickerView.tsx index 30a8c33d2..776a8e30d 100644 --- a/src/components/common/StickerView.tsx +++ b/src/components/common/StickerView.tsx @@ -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 = ({ 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 = ({ 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 = ({ )} tgsUrl={fullMediaData} play={shouldPlay} - color={customColor} noLoop={!shouldLoop} forceOnHeavyAnimation={forceOnHeavyAnimation} isLowPriority={isSmall && !selectIsAlwaysHighPriorityEmoji(getGlobal(), stickerSetInfo)} @@ -168,6 +175,7 @@ const StickerView: FC = ({ onLoad={markPlayerReady} onLoop={onAnimatedStickerLoop} onEnded={onAnimatedStickerLoop} + color={customColor} /> ) : isVideo ? ( = ({ disablePictureInPicture onReady={markPlayerReady} onEnded={onVideoEnded} + style={filterStyle} /> ) : ( {emoji} ))} diff --git a/src/components/left/main/StatusPickerMenu.module.scss b/src/components/left/main/StatusPickerMenu.module.scss index e005f4fc4..8c0ad1245 100644 --- a/src/components/left/main/StatusPickerMenu.module.scss +++ b/src/components/left/main/StatusPickerMenu.module.scss @@ -26,7 +26,7 @@ width: calc(100vw - 1rem); } - :global(.custom-emoji) { + :global(.sticker-set-cover), :global(.custom-emoji) { color: var(--color-primary); } } diff --git a/src/components/middle/composer/Composer.scss b/src/components/middle/composer/Composer.scss index eb4d9485e..fadd6c91e 100644 --- a/src/components/middle/composer/Composer.scss +++ b/src/components/middle/composer/Composer.scss @@ -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 diff --git a/src/components/middle/composer/StickerSetCover.tsx b/src/components/middle/composer/StickerSetCover.tsx index 9e9847691..fef4a4732 100644 --- a/src/components/middle/composer/StickerSetCover.tsx +++ b/src/components/middle/composer/StickerSetCover.tsx @@ -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 = ({ // eslint-disable-next-line no-null/no-null const containerRef = useRef(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 = ({ className={buildClassName(styles.video, transitionClassNames)} src={mediaData} canPlay={shouldPlay} + style={colorFilter} loop disablePictureInPicture /> ) : ( diff --git a/src/components/middle/composer/SymbolMenu.scss b/src/components/middle/composer/SymbolMenu.scss index 232710325..a4ff3deac 100644 --- a/src/components/middle/composer/SymbolMenu.scss +++ b/src/components/middle/composer/SymbolMenu.scss @@ -161,7 +161,7 @@ } } - .StickerButton.custom-emoji { + .StickerButton.custom-emoji, .sticker-set-cover { color: var(--color-text); } diff --git a/src/components/middle/composer/helpers/customEmoji.ts b/src/components/middle/composer/helpers/customEmoji.ts index 919adfe84..ecbb65096 100644 --- a/src/components/middle/composer/helpers/customEmoji.ts +++ b/src/components/middle/composer/helpers/customEmoji.ts @@ -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 `${emoji.emoji}>(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('.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; @@ -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 { 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(), diff --git a/src/components/right/RightSearch.tsx b/src/components/right/RightSearch.tsx index edfbc80f9..9d453a6e5 100644 --- a/src/components/right/RightSearch.tsx +++ b/src/components/right/RightSearch.tsx @@ -147,7 +147,7 @@ const RightSearch: FC = ({ />
- +
diff --git a/src/hooks/stickers/useColorFilter.ts b/src/hooks/stickers/useColorFilter.ts new file mode 100644 index 000000000..ac1072fb2 --- /dev/null +++ b/src/hooks/stickers/useColorFilter.ts @@ -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(); + +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});`; +} diff --git a/src/hooks/useDynamicColorListener.ts b/src/hooks/stickers/useDynamicColorListener.ts similarity index 69% rename from src/hooks/useDynamicColorListener.ts rename to src/hooks/stickers/useDynamicColorListener.ts index beeb2d4bb..212642a24 100644 --- a/src/hooks/useDynamicColorListener.ts +++ b/src/hooks/stickers/useDynamicColorListener.ts @@ -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, isDisabled?: boolean) { const [hexColor, setHexColor] = useState(); - 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 { - 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 { - ctx.drawImage(frame, containerInfo.coords!.x, containerInfo.coords!.y); - }); + ctx.drawImage(frame, containerInfo.coords!.x, containerInfo.coords!.y); } } diff --git a/src/util/AbsoluteVideo.ts b/src/util/AbsoluteVideo.ts index cabbfd528..74ff54030 100644 --- a/src/util/AbsoluteVideo.ts +++ b/src/util/AbsoluteVideo.ts @@ -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`; + }); + }); } } diff --git a/src/util/customEmojiManager.ts b/src/util/customEmojiManager.ts index 2da7eec75..4ad091d02 100644 --- a/src/util/customEmojiManager.ts +++ b/src/util/customEmojiManager.ts @@ -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); + }); } }); }