diff --git a/fluxer_app/src/features/messaging/components/markdown/renderers/EmojiRenderer.tsx b/fluxer_app/src/features/messaging/components/markdown/renderers/EmojiRenderer.tsx index 00d07e260..71c35b0ca 100644 --- a/fluxer_app/src/features/messaging/components/markdown/renderers/EmojiRenderer.tsx +++ b/fluxer_app/src/features/messaging/components/markdown/renderers/EmojiRenderer.tsx @@ -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(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) => { - 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) => { + 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 ? ( {emojiData.name} ) : ( diff --git a/fluxer_app/src/features/messaging/utils/markdown/EmojiDetector.ts b/fluxer_app/src/features/messaging/utils/markdown/EmojiDetector.ts index 39443091d..958d66fb6 100644 --- a/fluxer_app/src/features/messaging/utils/markdown/EmojiDetector.ts +++ b/fluxer_app/src/features/messaging/utils/markdown/EmojiDetector.ts @@ -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, + }); +}