mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
fix(app): animate reaction emoji only while hovering them (#1883)
This commit is contained in:
@@ -78,14 +78,10 @@ const MessageReactionItem = observer(
|
||||
const {i18n} = useLingui();
|
||||
const [hoverRef, isHovering] = useHover();
|
||||
const [prevCount, setPrevCount] = useState(reaction.count);
|
||||
const [animationSyncKey, setAnimationSyncKey] = useState(0);
|
||||
const [emojiInfoOpen, setEmojiInfoOpen] = useState(false);
|
||||
const [selectedEmoji, setSelectedEmoji] = useState<EmojiInfoData | null>(null);
|
||||
const [tooltipHovering, setTooltipHovering] = useState(false);
|
||||
const isMobile = MobileLayout.isMobileLayout();
|
||||
const handleTooltipAnimationSync = useCallback(() => {
|
||||
setAnimationSyncKey((prev) => prev + 1);
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (prevCount !== reaction.count) {
|
||||
setPrevCount(reaction.count);
|
||||
@@ -186,6 +182,7 @@ const MessageReactionItem = observer(
|
||||
<ReactionImage
|
||||
src={emojiUrl}
|
||||
alt={emojiName}
|
||||
aria-hidden={true}
|
||||
draggable={false}
|
||||
className={clsx('emoji', styles.emoji, reactionShouldBlur && matureStyles.matureBlurred)}
|
||||
data-flx="channel.message-reactions.message-reaction-item.emoji"
|
||||
@@ -251,8 +248,6 @@ const MessageReactionItem = observer(
|
||||
message={message}
|
||||
reaction={reaction}
|
||||
hoveredEmojiUrl={emojiUrl}
|
||||
animationSyncKey={animationSyncKey}
|
||||
onRequestAnimationSync={handleTooltipAnimationSync}
|
||||
onTooltipHoverChange={setTooltipHovering}
|
||||
data-flx="channel.message-reactions.message-reaction-item.reaction-tooltip"
|
||||
>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {useExpressionImagePreload} from '@app/features/expressions/utils/ExpressionImageCache';
|
||||
import type React from 'react';
|
||||
|
||||
type ReactionImageProps = Omit<React.ImgHTMLAttributes<HTMLImageElement>, 'alt' | 'src'> & {
|
||||
@@ -8,15 +7,6 @@ type ReactionImageProps = Omit<React.ImgHTMLAttributes<HTMLImageElement>, 'alt'
|
||||
alt: string;
|
||||
};
|
||||
|
||||
export const ReactionImage: React.FC<ReactionImageProps> = ({
|
||||
src,
|
||||
alt,
|
||||
decoding = 'async',
|
||||
loading = 'eager',
|
||||
...props
|
||||
}) => {
|
||||
useExpressionImagePreload(src);
|
||||
return (
|
||||
<img src={src} alt={alt} decoding={decoding} loading={loading} data-flx="messaging.reaction-image.img" {...props} />
|
||||
);
|
||||
export const ReactionImage: React.FC<ReactionImageProps> = ({src, alt, loading = 'eager', ...props}) => {
|
||||
return <img src={src} alt={alt} loading={loading} data-flx="messaging.reaction-image.img" {...props} />;
|
||||
};
|
||||
|
||||
@@ -28,40 +28,24 @@ export const ReactionTooltip = observer(
|
||||
reaction,
|
||||
children,
|
||||
hoveredEmojiUrl,
|
||||
animationSyncKey,
|
||||
onRequestAnimationSync,
|
||||
onTooltipHoverChange,
|
||||
}: {
|
||||
message: Message;
|
||||
reaction: MessageReaction;
|
||||
children: React.ReactElement<Record<string, unknown> & {ref?: React.Ref<HTMLElement>}>;
|
||||
hoveredEmojiUrl?: string | null;
|
||||
animationSyncKey?: number;
|
||||
onRequestAnimationSync?: () => void;
|
||||
onTooltipHoverChange?: (hovering: boolean) => void;
|
||||
}) => {
|
||||
const {i18n} = useLingui();
|
||||
const tooltip = useHoverFloatingTooltip(500);
|
||||
const prevIsOpenRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (tooltip.state.isOpen && !prevIsOpenRef.current) {
|
||||
onRequestAnimationSync?.();
|
||||
}
|
||||
prevIsOpenRef.current = tooltip.state.isOpen;
|
||||
}, [tooltip.state.isOpen, onRequestAnimationSync]);
|
||||
useEffect(() => {
|
||||
onTooltipHoverChange?.(tooltip.state.isOpen);
|
||||
}, [tooltip.state.isOpen, onTooltipHoverChange]);
|
||||
const {fetchStatus} = useReactionUsers(message.id, reaction.emoji);
|
||||
const isLoading = fetchStatus === 'pending';
|
||||
const tooltipText = getReactionTooltip(message, reaction.emoji);
|
||||
const emojiIdentifier = reaction.emoji.id ?? reaction.emoji.name;
|
||||
const tooltipEmojiKey = `${emojiIdentifier}-${animationSyncKey ?? 0}`;
|
||||
const fallbackEmojiUrl = useEmojiURL({
|
||||
emoji: reaction.emoji,
|
||||
isHovering: tooltip.state.isOpen,
|
||||
forceAnimate: tooltip.state.isOpen,
|
||||
});
|
||||
const tooltipEmojiKey = reaction.emoji.id ?? reaction.emoji.name;
|
||||
const fallbackEmojiUrl = useEmojiURL({emoji: reaction.emoji});
|
||||
const emojiUrl = hoveredEmojiUrl ?? fallbackEmojiUrl;
|
||||
const errorRetryRef = useRef(false);
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import i18n from '@app/app/I18n';
|
||||
import {useShouldAnimate} from '@app/features/app/hooks/useShouldAnimate';
|
||||
import Channels from '@app/features/channel/state/Channels';
|
||||
import type {UnicodeEmoji} from '@app/features/emoji/types/EmojiTypes';
|
||||
import EmojiCatalog from '@app/features/expressions/utils/EmojiCatalog';
|
||||
import * as EmojiImageUtils from '@app/features/expressions/utils/EmojiImageUtils';
|
||||
import {getSkinTonedSurrogate} from '@app/features/expressions/utils/SkinToneUtils';
|
||||
import type {Message} from '@app/features/messaging/models/MessagingMessage';
|
||||
import MessageReactions from '@app/features/messaging/state/MessageReactions';
|
||||
import * as ImageCacheUtils from '@app/features/messaging/utils/ImageCacheUtils';
|
||||
import {setUrlQueryParams} from '@app/features/messaging/utils/MessagingUrlUtils';
|
||||
import {getReactionKey, type ReactionEmoji} from '@app/features/messaging/utils/ReactionEmoji';
|
||||
import * as AvatarSourceUtils from '@app/features/user/utils/AvatarSourceUtils';
|
||||
import * as ReactionUtils from '@app/features/messaging/utils/ReactionUtils';
|
||||
import * as DisplayNameUtils from '@app/features/user/utils/DisplayNameUtils';
|
||||
import {getCurrentLocale} from '@app/features/user/utils/LocaleUtils';
|
||||
import {msg, plural} from '@lingui/core/macro';
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
const REACTED_BY_DESCRIPTOR = msg({
|
||||
message: '{emojiName} reacted by {reactors}',
|
||||
@@ -97,10 +92,6 @@ export function getEmojiNameWithColons(emoji: ReactionEmoji): string {
|
||||
return name ? `:${name}:` : surrogate;
|
||||
}
|
||||
|
||||
function getCustomEmojiURL(id: string, animated: boolean, size: number): string {
|
||||
return setUrlQueryParams(AvatarSourceUtils.getEmojiURL({id, animated}), {size, quality: 'lossless'});
|
||||
}
|
||||
|
||||
export interface EmojiURLParams {
|
||||
readonly emoji: ReactionEmoji;
|
||||
readonly isHovering?: boolean;
|
||||
@@ -116,26 +107,6 @@ export function useEmojiURL({
|
||||
forceAnimate = false,
|
||||
enabled = true,
|
||||
}: EmojiURLParams): string | null {
|
||||
const shouldAnimate = useShouldAnimate({
|
||||
kind: 'emoji',
|
||||
isHovering: isHovering || forceAnimate,
|
||||
});
|
||||
const staticUrl =
|
||||
emoji.id == null
|
||||
? EmojiImageUtils.getEmojiURL(EmojiCatalog.normalizeShortcodeToSurrogate(emoji.name))
|
||||
: getCustomEmojiURL(emoji.id, false, size);
|
||||
const animatedUrl = emoji.id != null && emoji.animated ? getCustomEmojiURL(emoji.id, true, size) : null;
|
||||
const [loadedAnimatedUrl, setLoadedAnimatedUrl] = useState<string | null>(() =>
|
||||
ImageCacheUtils.hasImage(animatedUrl) ? animatedUrl : null,
|
||||
);
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
ImageCacheUtils.pinImage(staticUrl);
|
||||
}, [enabled, staticUrl]);
|
||||
useEffect(() => {
|
||||
if (!enabled || !shouldAnimate || !animatedUrl) return;
|
||||
return ImageCacheUtils.loadImage(animatedUrl, () => setLoadedAnimatedUrl(animatedUrl));
|
||||
}, [animatedUrl, enabled, shouldAnimate]);
|
||||
if (!enabled) return null;
|
||||
return shouldAnimate && animatedUrl === loadedAnimatedUrl ? animatedUrl : staticUrl;
|
||||
const url = ReactionUtils.useEmojiURL({emoji, isHovering, size, forceAnimate});
|
||||
return enabled ? url : null;
|
||||
}
|
||||
|
||||
@@ -4,13 +4,12 @@ import i18n from '@app/app/I18n';
|
||||
import {useShouldAnimate} from '@app/features/app/hooks/useShouldAnimate';
|
||||
import Channels from '@app/features/channel/state/Channels';
|
||||
import type {UnicodeEmoji} from '@app/features/emoji/types/EmojiTypes';
|
||||
import {buildCustomEmojiURL} from '@app/features/expressions/utils/CustomEmojiImageUrl';
|
||||
import * as EmojiUtils from '@app/features/expressions/utils/EmojiUtils';
|
||||
import {getSkinTonedSurrogate} from '@app/features/expressions/utils/SkinToneUtils';
|
||||
import UnicodeEmojis from '@app/features/expressions/utils/UnicodeEmojis';
|
||||
import type {Message} from '@app/features/messaging/models/MessagingMessage';
|
||||
import MessageReactions from '@app/features/messaging/state/MessageReactions';
|
||||
import {setUrlQueryParams} from '@app/features/messaging/utils/MessagingUrlUtils';
|
||||
import * as AvatarUtils from '@app/features/user/utils/AvatarUtils';
|
||||
import {getCurrentLocale} from '@app/features/user/utils/LocaleUtils';
|
||||
import * as NicknameUtils from '@app/features/user/utils/NicknameUtils';
|
||||
import {msg, plural} from '@lingui/core/macro';
|
||||
@@ -101,7 +100,7 @@ export function getEmojiNameWithColons(emoji: ReactionEmoji): string {
|
||||
export function useEmojiURL({
|
||||
emoji,
|
||||
isHovering = false,
|
||||
size = 128,
|
||||
size,
|
||||
forceAnimate = false,
|
||||
}: {
|
||||
emoji: ReactionEmoji;
|
||||
@@ -111,11 +110,11 @@ export function useEmojiURL({
|
||||
}): string | null {
|
||||
const shouldAnimate = useShouldAnimate({
|
||||
kind: 'emoji',
|
||||
isAnimated: emoji.animated === true,
|
||||
isHovering: isHovering || forceAnimate,
|
||||
});
|
||||
if (emoji.id == null) {
|
||||
return EmojiUtils.getEmojiURL(UnicodeEmojis.normalizeEmojiNameToSurrogate(emoji.name));
|
||||
}
|
||||
const url = AvatarUtils.getEmojiURL({id: emoji.id, animated: shouldAnimate});
|
||||
return setUrlQueryParams(url, {size, quality: 'lossless'});
|
||||
return buildCustomEmojiURL({id: emoji.id, animated: emoji.animated === true && shouldAnimate, size});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user