mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-13 05:58:29 +09:00
fix(app): animate message emoji only while hovering the message (#1869)
This commit is contained in:
1 parent
5b8a46d087
commit
a59f3d0d0c
2 files changed
+74
-39
No files matched your search
@@ -1,15 +1,16 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {MatureEmojiWrapper} from '@app/features/app/components/shared/MatureEmojiWrapper';
|
||||
import {useShouldAnimate} from '@app/features/app/hooks/useShouldAnimate';
|
||||
import {requestDeleteMessage} from '@app/features/channel/components/MessageActionUtils';
|
||||
import {useMaybeMessageViewContext} from '@app/features/channel/components/MessageViewContext';
|
||||
import {EmojiInfoBottomSheet} from '@app/features/emoji/components/bottomsheets/EmojiInfoBottomSheet';
|
||||
import {EmojiInfoContent} from '@app/features/emoji/components/emojis/EmojiInfoContent';
|
||||
import type {FlatEmoji} from '@app/features/emoji/types/EmojiTypes';
|
||||
import {isKeyboardActivationKey} from '@app/features/input/utils/KeyboardUtils';
|
||||
import type {RendererProps} from '@app/features/messaging/components/markdown/renderers/RendererTypes';
|
||||
import Messages from '@app/features/messaging/state/MessagingMessages';
|
||||
import {setUrlQueryParams} from '@app/features/messaging/utils/MessagingUrlUtils';
|
||||
import {getEmojiRenderData} from '@app/features/messaging/utils/markdown/EmojiDetector';
|
||||
import {getEmojiRenderData, getEmojiRenderUrl} from '@app/features/messaging/utils/markdown/EmojiDetector';
|
||||
import {EmojiKind} from '@app/features/messaging/utils/markdown/parser/Enums';
|
||||
import type {EmojiNode} from '@app/features/messaging/utils/markdown/parser/Nodes';
|
||||
import {EmojiContextMenuItems, EmojiInlineMenuItems} from '@app/features/ui/action_menu/items/EmojiContextMenuItems';
|
||||
@@ -31,6 +32,8 @@ const FAILED_TO_LOAD_DESCRIPTOR = msg({
|
||||
comment: 'Error message in the messaging emoji renderer.',
|
||||
});
|
||||
|
||||
const FAILED_EMOJI_STYLE: React.CSSProperties = {opacity: 0.5};
|
||||
|
||||
interface EmojiBottomSheetState {
|
||||
isOpen: boolean;
|
||||
emoji: {id?: string; name: string; animated?: boolean} | null;
|
||||
@@ -88,19 +91,25 @@ const EmojiRendererInner = observer(function EmojiRendererInner({
|
||||
isOpen: false,
|
||||
emoji: null,
|
||||
});
|
||||
const [failedUrl, setFailedUrl] = useState<string | null>(null);
|
||||
const messageView = useMaybeMessageViewContext();
|
||||
const isAnimatable = emojiData.isAnimatable;
|
||||
const shouldAnimate = useShouldAnimate({
|
||||
kind: 'emoji',
|
||||
isHovering: isAnimatable && messageView?.isHovering === true,
|
||||
});
|
||||
const animated = isAnimatable && shouldAnimate;
|
||||
const className = clsx('emoji', shouldJumboEmojis && 'jumboable');
|
||||
const size = shouldJumboEmojis ? 240 : 96;
|
||||
const renderedEmojiUrl = useMemo(
|
||||
const emojiUrl = useMemo(
|
||||
() =>
|
||||
emojiData.id && emojiData.url ? setUrlQueryParams(emojiData.url, {size, quality: 'lossless'}) : emojiData.url,
|
||||
[emojiData.id, emojiData.url, size],
|
||||
);
|
||||
const tooltipEmojiUrl = useMemo(
|
||||
() =>
|
||||
emojiData.id && emojiData.url
|
||||
? setUrlQueryParams(emojiData.url, {size: 240, quality: 'lossless'})
|
||||
: emojiData.url,
|
||||
[emojiData.id, emojiData.url],
|
||||
getEmojiRenderUrl({
|
||||
id: emojiData.id,
|
||||
surrogateUrl: emojiData.surrogateUrl,
|
||||
isAnimatable: emojiData.isAnimatable,
|
||||
animated,
|
||||
jumbo: shouldJumboEmojis,
|
||||
}),
|
||||
[emojiData.id, emojiData.surrogateUrl, emojiData.isAnimatable, animated, shouldJumboEmojis],
|
||||
);
|
||||
const isCustomEmoji = node.kind.kind === EmojiKind.Custom;
|
||||
const standardEmojiSurrogate = node.kind.kind === EmojiKind.Standard ? node.kind.raw : undefined;
|
||||
@@ -147,11 +156,11 @@ const EmojiRendererInner = observer(function EmojiRendererInner({
|
||||
allNamesString: `:${node.kind.name}:`,
|
||||
uniqueName: node.kind.name,
|
||||
surrogates: standardEmojiSurrogate,
|
||||
url: standardEmojiSurrogate ? (emojiData.url ?? undefined) : undefined,
|
||||
url: standardEmojiSurrogate ? (emojiData.surrogateUrl ?? undefined) : undefined,
|
||||
};
|
||||
}, [
|
||||
emojiData.id,
|
||||
emojiData.url,
|
||||
emojiData.surrogateUrl,
|
||||
emojiRecord,
|
||||
fallbackAnimated,
|
||||
fallbackGuildId,
|
||||
@@ -160,13 +169,11 @@ const EmojiRendererInner = observer(function EmojiRendererInner({
|
||||
]);
|
||||
const getTooltipData = useCallback(() => {
|
||||
const emojiForSubtext = buildEmojiForSubtext();
|
||||
return {emojiUrl: tooltipEmojiUrl, emojiForSubtext};
|
||||
}, [buildEmojiForSubtext, tooltipEmojiUrl]);
|
||||
const handleImageError = (e: React.SyntheticEvent<HTMLImageElement>) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.style.opacity = '0.5';
|
||||
target.alt = `${emojiData.name} ${i18n._(FAILED_TO_LOAD_DESCRIPTOR)}`;
|
||||
};
|
||||
return {emojiUrl, emojiForSubtext};
|
||||
}, [buildEmojiForSubtext, emojiUrl]);
|
||||
const handleImageError = useCallback((e: React.SyntheticEvent<HTMLImageElement>) => {
|
||||
setFailedUrl((e.target as HTMLImageElement).getAttribute('src'));
|
||||
}, []);
|
||||
const handleContextMenu = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
if (!isCustomEmoji || !emojiData.id) return;
|
||||
@@ -220,20 +227,22 @@ const EmojiRendererInner = observer(function EmojiRendererInner({
|
||||
) : (
|
||||
element
|
||||
);
|
||||
const hasFailed = emojiUrl != null && failedUrl === emojiUrl;
|
||||
const renderEmojiElement = (contextMenu: boolean, dataFlx: string) =>
|
||||
renderedEmojiUrl ? (
|
||||
emojiUrl ? (
|
||||
<img
|
||||
draggable={false}
|
||||
className={className}
|
||||
alt={emojiData.name}
|
||||
src={renderedEmojiUrl}
|
||||
alt={hasFailed ? `${emojiData.name} ${i18n._(FAILED_TO_LOAD_DESCRIPTOR)}` : emojiData.name}
|
||||
src={emojiUrl}
|
||||
style={hasFailed ? FAILED_EMOJI_STYLE : undefined}
|
||||
data-message-id={messageId}
|
||||
data-message-emoji="true"
|
||||
data-emoji-id={emojiData.id}
|
||||
data-animated={emojiData.isAnimated}
|
||||
onError={handleImageError}
|
||||
onContextMenu={contextMenu ? handleContextMenu : undefined}
|
||||
loading="lazy"
|
||||
loading="eager"
|
||||
data-flx={dataFlx}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -1,43 +1,69 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {getAnimatedMediaPlaybackAllowed} from '@app/features/app/hooks/useAnimatedMediaPlayback';
|
||||
import Emoji from '@app/features/emoji/state/Emoji';
|
||||
import type {FlatEmoji} from '@app/features/emoji/types/EmojiTypes';
|
||||
import {
|
||||
buildCustomEmojiURL,
|
||||
CUSTOM_EMOJI_ENLARGED_IMAGE_RUNG,
|
||||
CUSTOM_EMOJI_IMAGE_RUNG,
|
||||
} from '@app/features/expressions/utils/CustomEmojiImageUrl';
|
||||
import * as EmojiUtils from '@app/features/expressions/utils/EmojiUtils';
|
||||
import {EmojiKind} from '@app/features/messaging/utils/markdown/parser/Enums';
|
||||
import type {EmojiNode} from '@app/features/messaging/utils/markdown/parser/Nodes';
|
||||
import UserSettings from '@app/features/user/state/UserSettings';
|
||||
import * as AvatarUtils from '@app/features/user/utils/AvatarUtils';
|
||||
|
||||
interface EmojiRenderData {
|
||||
url: string | null;
|
||||
export interface EmojiRenderData {
|
||||
name: string;
|
||||
isAnimated: boolean;
|
||||
isAnimatable: boolean;
|
||||
surrogateUrl: string | null;
|
||||
id?: string;
|
||||
emoji?: FlatEmoji;
|
||||
}
|
||||
|
||||
export function getEmojiRenderData(emojiNode: EmojiNode, disableAnimatedEmoji = false): EmojiRenderData {
|
||||
const {kind} = emojiNode;
|
||||
const emojiName = `:${kind.name}:`;
|
||||
if (kind.kind === EmojiKind.Standard) {
|
||||
return {
|
||||
url: EmojiUtils.getTwemojiURL(kind.codepoints),
|
||||
name: emojiName,
|
||||
name: `:${kind.name}:`,
|
||||
isAnimated: false,
|
||||
isAnimatable: false,
|
||||
surrogateUrl: EmojiUtils.getTwemojiURL(kind.codepoints),
|
||||
};
|
||||
}
|
||||
const {id} = kind;
|
||||
const emoji = Emoji.getEmojiById(id);
|
||||
const isAnimated = emoji?.animated ?? kind.animated;
|
||||
const shouldAnimate =
|
||||
isAnimated && !disableAnimatedEmoji && UserSettings.getAnimateEmoji() && getAnimatedMediaPlaybackAllowed();
|
||||
const finalEmojiName = `:${emoji?.name || kind.name}:`;
|
||||
return {
|
||||
url: AvatarUtils.getEmojiURL({id, animated: shouldAnimate}),
|
||||
name: finalEmojiName,
|
||||
name: `:${emoji?.name || kind.name}:`,
|
||||
isAnimated,
|
||||
isAnimatable: isAnimated && !disableAnimatedEmoji,
|
||||
surrogateUrl: null,
|
||||
id,
|
||||
emoji,
|
||||
};
|
||||
}
|
||||
|
||||
interface EmojiRenderUrlOptions {
|
||||
id?: string;
|
||||
surrogateUrl: string | null;
|
||||
isAnimatable: boolean;
|
||||
animated: boolean;
|
||||
jumbo: boolean;
|
||||
}
|
||||
|
||||
export function getEmojiRenderUrl({
|
||||
id,
|
||||
surrogateUrl,
|
||||
isAnimatable,
|
||||
animated,
|
||||
jumbo,
|
||||
}: EmojiRenderUrlOptions): string | null {
|
||||
if (id == null) {
|
||||
return surrogateUrl;
|
||||
}
|
||||
return buildCustomEmojiURL({
|
||||
id,
|
||||
animated: animated && isAnimatable,
|
||||
size: jumbo ? CUSTOM_EMOJI_ENLARGED_IMAGE_RUNG : CUSTOM_EMOJI_IMAGE_RUNG,
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user