From e4566d4f5a442d74c63d99d775673829db5b8e23 Mon Sep 17 00:00:00 2001 From: ChaosExAnima Date: Tue, 23 Sep 2025 12:49:52 +0200 Subject: [PATCH] add new render and context components --- .../mastodon/components/emoji/context.tsx | 77 +++++++++++++++++ .../mastodon/components/emoji/index.tsx | 85 +++++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 app/javascript/mastodon/components/emoji/context.tsx create mode 100644 app/javascript/mastodon/components/emoji/index.tsx diff --git a/app/javascript/mastodon/components/emoji/context.tsx b/app/javascript/mastodon/components/emoji/context.tsx new file mode 100644 index 00000000000..ad34a8a8860 --- /dev/null +++ b/app/javascript/mastodon/components/emoji/context.tsx @@ -0,0 +1,77 @@ +import type { + ComponentPropsWithoutRef, + ElementType, + PropsWithChildren, +} from 'react'; +import { createContext, useCallback, useState } from 'react'; + +import type { List as ImmutableList } from 'immutable'; + +import type { ApiCustomEmojiJSON } from '@/mastodon/api_types/custom_emoji'; +import { autoPlayGif } from '@/mastodon/initial_state'; +import { useAppSelector } from '@/mastodon/store'; +import type { ExtraCustomEmojiMap } from 'mastodon/features/emoji/types'; + +// Animation context +export const AnimateEmojiContext = createContext(autoPlayGif ?? false); + +// Polymorphic provider component +type AnimateEmojiProviderProps = + ComponentPropsWithoutRef & { as: Element } & PropsWithChildren; + +export const AnimateEmojiProvider = ({ + children, + as: Wrapper = 'div', + ...props +}: AnimateEmojiProviderProps) => { + const [animate, setAnimate] = useState(autoPlayGif ?? false); + + const handleEnter = useCallback(() => { + if (!autoPlayGif) { + setAnimate(true); + } + }, []); + const handleLeave = useCallback(() => { + if (!autoPlayGif) { + setAnimate(false); + } + }, []); + + return ( + + + {children} + + + ); +}; + +// Handle custom emoji +export const CustomEmojiContext = createContext({}); + +export const CustomEmojiProvider = ({ + children, + emoji, +}: PropsWithChildren<{ emoji: ExtraCustomEmojiMap }>) => { + return ( + + {children} + + ); +}; + +export const RootCustomEmojiProvider = ({ children }: PropsWithChildren) => { + const emoji = useAppSelector((state) => + (state.custom_emojis as ImmutableList) + .toJS() + .reduce( + (acc, emoji) => ({ ...acc, [emoji.shortcode]: emoji }), + {}, + ), + ); + return ( + + {children} + + ); +}; diff --git a/app/javascript/mastodon/components/emoji/index.tsx b/app/javascript/mastodon/components/emoji/index.tsx new file mode 100644 index 00000000000..ea41f8e45ce --- /dev/null +++ b/app/javascript/mastodon/components/emoji/index.tsx @@ -0,0 +1,85 @@ +import type { FC } from 'react'; +import { useContext, useMemo } from 'react'; + +import { + EMOJI_TYPE_UNICODE, + EMOJI_TYPE_CUSTOM, +} from '@/mastodon/features/emoji/constants'; +import { useEmojiAppState } from '@/mastodon/features/emoji/hooks'; +import { emojiToUnicodeHex } from '@/mastodon/features/emoji/normalize'; +import { shouldRenderUnicodeImage } from '@/mastodon/features/emoji/render'; +import type { + EmojiStateCustom, + EmojiStateUnicode, +} from '@/mastodon/features/emoji/types'; +import { anyEmojiRegex } from '@/mastodon/features/emoji/utils'; + +import { AnimateEmojiContext, CustomEmojiContext } from './context'; + +export const Emoji: FC<{ code: string; noFallback?: boolean }> = ({ + code: rawCode, + noFallback, +}) => { + const customEmoji = useContext(CustomEmojiContext); + const appState = useEmojiAppState(); + const animate = useContext(AnimateEmojiContext); + + const state: null | Required | EmojiStateUnicode = + useMemo(() => { + let code = rawCode; + if (!anyEmojiRegex().test(code)) { + return null; + } + if (code.startsWith(':') && code.endsWith(':')) { + code = code.slice(1, -1); // Remove the colons + const data = customEmoji[code]; + if (!data) { + return null; + } + return { + type: EMOJI_TYPE_CUSTOM, + code, + data, + }; + } + + // If it's not custom, check if we should render this based on mode. + if (!shouldRenderUnicodeImage(code, appState.mode)) { + return null; + } + + // If we are rendering it, convert it to a hex code. + code = emojiToUnicodeHex(code); + return { + type: EMOJI_TYPE_UNICODE, + code, + }; + }, [appState.mode, customEmoji, rawCode]); + + if (!state) { + return noFallback ? null : rawCode; + } + + if (state.type === EMOJI_TYPE_CUSTOM) { + return ( + {state.code} + ); + } + + // TODO: Load data + if (!state.data) { + return null; + } + + return ( + {state.data.label} + ); +};