Custom Emoji: Dynamically update color (#2681)

This commit is contained in:
Alexander Zinchuk 2023-03-03 14:30:19 +01:00
parent 12810e8200
commit 2b6e3b3f8c
12 changed files with 119 additions and 62 deletions

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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,

View File

@ -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;

View File

@ -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));

View File

@ -102,7 +102,7 @@
.emoji-status {
--custom-emoji-size: 1.5rem;
--color-text: var(--color-primary);
color: var(--color-primary);
}
.PremiumIcon {

View File

@ -14,4 +14,8 @@
right: 0.5rem !important;
width: calc(100vw - 1rem);
}
:global(.custom-emoji) {
color: var(--color-primary);
}
}

View File

@ -320,7 +320,7 @@
}
.custom-emoji {
--color-text: var(--color-primary);
color: var(--color-primary);
}
}

View File

@ -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(),

View File

@ -4,7 +4,7 @@
}
.particle {
--color-text: var(--color-primary);
color: var(--color-primary);
position: absolute;
width: 1rem;

View File

@ -281,7 +281,7 @@
color: var(--color-user-#{$i});
.custom-emoji {
--color-text: var(--color-user-#{$i});
color: var(--color-user-#{$i});
}
.PremiumIcon {

View 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,
};
}