Custom Emoji: Dynamically update color (#2681)
This commit is contained in:
parent
12810e8200
commit
2b6e3b3f8c
@ -1,5 +1,5 @@
|
||||
import React, {
|
||||
memo, useCallback, useEffect, useRef, useState,
|
||||
memo, useCallback, useRef, useState,
|
||||
} from '../../lib/teact/teact';
|
||||
import { getGlobal } from '../../global';
|
||||
|
||||
@ -7,13 +7,12 @@ import type { FC, TeactNode } from '../../lib/teact/teact';
|
||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||
import { ApiMessageEntityTypes } from '../../api/types';
|
||||
|
||||
import { getPropertyHexColor } from '../../util/themeStyle';
|
||||
import { hexToRgb } from '../../util/switchTheme';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import safePlay from '../../util/safePlay';
|
||||
import { selectIsAlwaysHighPriorityEmoji } from '../../global/selectors';
|
||||
|
||||
import useCustomEmoji from './hooks/useCustomEmoji';
|
||||
import useDynamicColorListener from '../../hooks/useDynamicColorListener';
|
||||
|
||||
import StickerView from './StickerView';
|
||||
|
||||
@ -75,22 +74,8 @@ const CustomEmoji: FC<OwnProps> = ({
|
||||
const loopCountRef = useRef(0);
|
||||
const [shouldLoop, setShouldLoop] = useState(true);
|
||||
|
||||
const [customColor, setCustomColor] = useState<[number, number, number] | undefined>();
|
||||
const hasCustomColor = customEmoji?.shouldUseTextColor;
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasCustomColor) {
|
||||
setCustomColor(undefined);
|
||||
return;
|
||||
}
|
||||
const hexColor = getPropertyHexColor(getComputedStyle(containerRef.current!), '--color-text');
|
||||
if (!hexColor) {
|
||||
setCustomColor(undefined);
|
||||
return;
|
||||
}
|
||||
const customColorRgb = hexToRgb(hexColor);
|
||||
setCustomColor([customColorRgb.r, customColorRgb.g, customColorRgb.b]);
|
||||
}, [hasCustomColor]);
|
||||
const { rgbColor: customColor } = useDynamicColorListener(containerRef, !hasCustomColor);
|
||||
|
||||
const handleVideoEnded = useCallback((e) => {
|
||||
if (!loopLimit) return;
|
||||
|
||||
@ -28,9 +28,10 @@
|
||||
|
||||
.VerifiedIcon,
|
||||
.PremiumIcon {
|
||||
z-index: 2;
|
||||
--color-fill: var(--color-white);
|
||||
--color-checkmark: var(--color-primary);
|
||||
|
||||
z-index: 2;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
@ -41,10 +42,10 @@
|
||||
}
|
||||
|
||||
.custom-emoji {
|
||||
--custom-emoji-size: 1.5rem;
|
||||
|
||||
color: var(--color-white);
|
||||
pointer-events: auto;
|
||||
cursor: pointer;
|
||||
|
||||
--custom-emoji-size: 1.5rem;
|
||||
--color-text: var(--color-white);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { MouseEvent as ReactMouseEvent, ReactNode } from 'react';
|
||||
import React, {
|
||||
memo, useCallback, useEffect, useMemo, useRef, useState,
|
||||
memo, useCallback, useEffect, useMemo, useRef,
|
||||
} from '../../lib/teact/teact';
|
||||
import { getActions } from '../../global';
|
||||
|
||||
@ -9,8 +9,6 @@ import type { ApiBotInlineMediaResult, ApiSticker } from '../../api/types';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { preventMessageInputBlurWithBubbling } from '../middle/helpers/preventMessageInputBlur';
|
||||
import { IS_TOUCH_ENV } from '../../util/environment';
|
||||
import { getPropertyHexColor } from '../../util/themeStyle';
|
||||
import { hexToRgb } from '../../util/switchTheme';
|
||||
import { getServerTimeOffset } from '../../util/serverTime';
|
||||
|
||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||
@ -18,6 +16,7 @@ import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
|
||||
import useContextMenuPosition from '../../hooks/useContextMenuPosition';
|
||||
import useDynamicColorListener from '../../hooks/useDynamicColorListener';
|
||||
|
||||
import StickerView from './StickerView';
|
||||
import Button from '../ui/Button';
|
||||
@ -87,22 +86,8 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const lang = useLang();
|
||||
const [customColor, setCustomColor] = useState<[number, number, number] | undefined>();
|
||||
const hasCustomColor = sticker.shouldUseTextColor;
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasCustomColor) {
|
||||
setCustomColor(undefined);
|
||||
return;
|
||||
}
|
||||
const hexColor = getPropertyHexColor(getComputedStyle(ref.current!), '--color-text');
|
||||
if (!hexColor) {
|
||||
setCustomColor(undefined);
|
||||
return;
|
||||
}
|
||||
const customColorRgb = hexToRgb(hexColor);
|
||||
setCustomColor([customColorRgb.r, customColorRgb.g, customColorRgb.b]);
|
||||
}, [hasCustomColor]);
|
||||
const { rgbColor: customColor } = useDynamicColorListener(ref, !hasCustomColor);
|
||||
|
||||
const {
|
||||
id, isCustomEmoji, hasEffect: isPremium, stickerSetInfo,
|
||||
|
||||
@ -73,8 +73,8 @@
|
||||
&.selected:not(.forum):hover {
|
||||
--background-color: var(--color-chat-active) !important;
|
||||
|
||||
.custom-emoji {
|
||||
--color-text: var(--color-white);
|
||||
.title .custom-emoji {
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.VerifiedIcon, .PremiumIcon {
|
||||
@ -192,10 +192,6 @@
|
||||
background: var(--background-color);
|
||||
}
|
||||
|
||||
.custom-emoji {
|
||||
--color-text: var(--color-primary);
|
||||
}
|
||||
|
||||
.avatar-badge-wrapper {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
@ -216,6 +212,10 @@
|
||||
.info {
|
||||
transition: opacity 300ms ease, transform var(--layer-transition);
|
||||
|
||||
.title .custom-emoji {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.icon-muted {
|
||||
font-size: 1.25rem;
|
||||
margin-top: -0.0625rem;
|
||||
|
||||
@ -75,7 +75,6 @@ type StateProps = {
|
||||
isMuted?: boolean;
|
||||
user?: ApiUser;
|
||||
userStatus?: ApiUserStatus;
|
||||
isEmojiStatusColored?: boolean;
|
||||
actionTargetUserIds?: string[];
|
||||
actionTargetMessage?: ApiMessage;
|
||||
actionTargetChatId?: string;
|
||||
@ -103,7 +102,6 @@ const Chat: FC<OwnProps & StateProps> = ({
|
||||
isMuted,
|
||||
user,
|
||||
userStatus,
|
||||
isEmojiStatusColored,
|
||||
actionTargetUserIds,
|
||||
lastMessageSender,
|
||||
lastMessageOutgoingStatus,
|
||||
@ -256,7 +254,6 @@ const Chat: FC<OwnProps & StateProps> = ({
|
||||
withEmojiStatus
|
||||
isSavedMessages={chatId === user?.id && user?.isSelf}
|
||||
observeIntersection={observeIntersection}
|
||||
key={!isMobile && isEmojiStatusColored ? `${isSelected}` : undefined}
|
||||
/>
|
||||
{isMuted && <i className="icon-muted" />}
|
||||
<div className="separator" />
|
||||
@ -331,8 +328,6 @@ export default memo(withGlobal<OwnProps>(
|
||||
|
||||
const typingStatus = selectThreadParam(global, chatId, MAIN_THREAD_ID, 'typingStatus');
|
||||
|
||||
const statusEmoji = user?.emojiStatus && global.customEmojis.byId[user.emojiStatus.documentId];
|
||||
|
||||
return {
|
||||
chat,
|
||||
isMuted: selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global)),
|
||||
@ -354,7 +349,6 @@ export default memo(withGlobal<OwnProps>(
|
||||
userStatus,
|
||||
lastMessageTopic,
|
||||
typingStatus,
|
||||
isEmojiStatusColored: statusEmoji?.shouldUseTextColor,
|
||||
};
|
||||
},
|
||||
)(Chat));
|
||||
|
||||
@ -102,7 +102,7 @@
|
||||
|
||||
.emoji-status {
|
||||
--custom-emoji-size: 1.5rem;
|
||||
--color-text: var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.PremiumIcon {
|
||||
|
||||
@ -14,4 +14,8 @@
|
||||
right: 0.5rem !important;
|
||||
width: calc(100vw - 1rem);
|
||||
}
|
||||
|
||||
:global(.custom-emoji) {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
@ -320,7 +320,7 @@
|
||||
}
|
||||
|
||||
.custom-emoji {
|
||||
--color-text: var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -21,6 +21,8 @@ import { REM } from '../../../common/helpers/mediaDimensions';
|
||||
import useResizeObserver from '../../../../hooks/useResizeObserver';
|
||||
import useBackgroundMode from '../../../../hooks/useBackgroundMode';
|
||||
import useThrottledCallback from '../../../../hooks/useThrottledCallback';
|
||||
import useDynamicColorListener from '../../../../hooks/useDynamicColorListener';
|
||||
import useEffectWithPrevDeps from '../../../../hooks/useEffectWithPrevDeps';
|
||||
|
||||
const SIZE = 1.25 * REM;
|
||||
const THROTTLE_MS = 300;
|
||||
@ -41,6 +43,7 @@ export default function useInputCustomEmojis(
|
||||
prefixId?: string,
|
||||
isActive?: boolean,
|
||||
) {
|
||||
const { rgbColor: textColor } = useDynamicColorListener(inputRef);
|
||||
const mapRef = useRef<Map<string, CustomEmojiPlayer>>(new Map());
|
||||
|
||||
const removeContainers = useCallback((ids: string[]) => {
|
||||
@ -60,11 +63,12 @@ export default function useInputCustomEmojis(
|
||||
const customEmojies = Array.from(inputRef.current.querySelectorAll<HTMLElement>('.custom-emoji'));
|
||||
|
||||
customEmojies.forEach((element) => {
|
||||
const id = `${prefixId || ''}${element.dataset.uniqueId!}`;
|
||||
const documentId = element.dataset.documentId!;
|
||||
if (!id) {
|
||||
if (!element.dataset.uniqueId) {
|
||||
return;
|
||||
}
|
||||
const id = `${prefixId || ''}${element.dataset.uniqueId}${textColor?.join(',') || ''}`;
|
||||
const documentId = element.dataset.documentId!;
|
||||
|
||||
removedContainers.delete(id);
|
||||
|
||||
const mediaUrl = getCustomEmojiMediaDataForInput(documentId);
|
||||
@ -95,9 +99,11 @@ export default function useInputCustomEmojis(
|
||||
sharedCanvasHqRef,
|
||||
absoluteContainerRef,
|
||||
uniqueId: id,
|
||||
containerId: prefixId || id,
|
||||
mediaUrl,
|
||||
isHq,
|
||||
position: { x, y },
|
||||
textColor,
|
||||
});
|
||||
animation.play();
|
||||
|
||||
@ -105,7 +111,7 @@ export default function useInputCustomEmojis(
|
||||
});
|
||||
|
||||
removeContainers(Array.from(removedContainers));
|
||||
}, [absoluteContainerRef, inputRef, prefixId, removeContainers, sharedCanvasHqRef, sharedCanvasRef]);
|
||||
}, [absoluteContainerRef, textColor, inputRef, prefixId, removeContainers, sharedCanvasHqRef, sharedCanvasRef]);
|
||||
|
||||
useEffect(() => {
|
||||
addCustomEmojiInputRenderCallback(synchronizeElements);
|
||||
@ -127,6 +133,12 @@ export default function useInputCustomEmojis(
|
||||
});
|
||||
}, [getHtml, synchronizeElements, inputRef, removeContainers, sharedCanvasRef, isActive]);
|
||||
|
||||
useEffectWithPrevDeps(([prevTextColor]) => {
|
||||
if (textColor !== prevTextColor) {
|
||||
synchronizeElements();
|
||||
}
|
||||
}, [textColor, synchronizeElements]);
|
||||
|
||||
const throttledSynchronizeElements = useThrottledCallback(
|
||||
synchronizeElements,
|
||||
[synchronizeElements],
|
||||
@ -163,32 +175,38 @@ function createPlayer({
|
||||
sharedCanvasHqRef,
|
||||
absoluteContainerRef,
|
||||
uniqueId,
|
||||
containerId,
|
||||
mediaUrl,
|
||||
position,
|
||||
isHq,
|
||||
textColor,
|
||||
}: {
|
||||
customEmoji: ApiSticker;
|
||||
sharedCanvasRef: React.RefObject<HTMLCanvasElement>;
|
||||
sharedCanvasHqRef: React.RefObject<HTMLCanvasElement>;
|
||||
absoluteContainerRef: React.RefObject<HTMLElement>;
|
||||
uniqueId: string;
|
||||
containerId: string;
|
||||
mediaUrl: string;
|
||||
position: { x: number; y: number };
|
||||
isHq?: boolean;
|
||||
textColor?: [number, number, number];
|
||||
}): CustomEmojiPlayer {
|
||||
if (customEmoji.isLottie) {
|
||||
const lottie = RLottie.init(
|
||||
uniqueId,
|
||||
containerId,
|
||||
isHq ? sharedCanvasHqRef.current! : sharedCanvasRef.current!,
|
||||
undefined,
|
||||
customEmoji.id,
|
||||
uniqueId,
|
||||
mediaUrl,
|
||||
{
|
||||
size: SIZE,
|
||||
coords: position,
|
||||
isLowPriority: !isHq,
|
||||
},
|
||||
customEmoji.shouldUseTextColor ? textColor : undefined,
|
||||
);
|
||||
|
||||
return {
|
||||
play: () => lottie.play(),
|
||||
pause: () => lottie.pause(),
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
}
|
||||
|
||||
.particle {
|
||||
--color-text: var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
|
||||
position: absolute;
|
||||
width: 1rem;
|
||||
|
||||
@ -281,7 +281,7 @@
|
||||
color: var(--color-user-#{$i});
|
||||
|
||||
.custom-emoji {
|
||||
--color-text: var(--color-user-#{$i});
|
||||
color: var(--color-user-#{$i});
|
||||
}
|
||||
|
||||
.PremiumIcon {
|
||||
|
||||
70
src/hooks/useDynamicColorListener.ts
Normal file
70
src/hooks/useDynamicColorListener.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import {
|
||||
useCallback, useEffect, useRef, useState,
|
||||
} from '../lib/teact/teact';
|
||||
import { hexToRgb } from '../util/switchTheme';
|
||||
import { getPropertyHexColor } from '../util/themeStyle';
|
||||
import useResizeObserver from './useResizeObserver';
|
||||
import useSyncEffect from './useSyncEffect';
|
||||
|
||||
// Delay required to prevent constant re-rendering on theme change
|
||||
const TRANSITION_STYLE = '0.1s color linear 50ms';
|
||||
|
||||
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) {
|
||||
setHexColor(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentHexColor = getPropertyHexColor(getComputedStyle(ref.current), 'color');
|
||||
setHexColor(currentHexColor);
|
||||
}, [isDisabled, ref]);
|
||||
|
||||
// Element does not receive `transitionend` event if parent has `display: none`.
|
||||
// We will receive `resize` event when parent is shown again.
|
||||
useResizeObserver(!isDisabled ? ref : undefined, updateColor);
|
||||
|
||||
// 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]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref?.current) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
updateColor();
|
||||
|
||||
if (isDisabled) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function handleTransitionEnd(e: TransitionEvent) {
|
||||
if (e.propertyName !== 'color') return;
|
||||
updateColor();
|
||||
}
|
||||
|
||||
const el = ref.current;
|
||||
el.addEventListener('transitionend', handleTransitionEnd);
|
||||
el.style.setProperty('transition', TRANSITION_STYLE, 'important');
|
||||
return () => {
|
||||
el.removeEventListener('transitionend', handleTransitionEnd);
|
||||
el.style.removeProperty('transition');
|
||||
};
|
||||
}, [isDisabled, ref, updateColor]);
|
||||
|
||||
return {
|
||||
hexColor,
|
||||
rgbColor: rgbColorRef.current,
|
||||
};
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user