add new render and context components
Some checks are pending
Chromatic / Run Chromatic (push) Waiting to run

This commit is contained in:
ChaosExAnima 2025-09-23 12:49:52 +02:00
parent 42b5b4247b
commit e4566d4f5a
No known key found for this signature in database
GPG Key ID: 8F2B333100FB6117
2 changed files with 162 additions and 0 deletions

View File

@ -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<Element extends ElementType = 'div'> =
ComponentPropsWithoutRef<Element> & { as: Element } & PropsWithChildren;
export const AnimateEmojiProvider = ({
children,
as: Wrapper = 'div',
...props
}: AnimateEmojiProviderProps<ElementType>) => {
const [animate, setAnimate] = useState(autoPlayGif ?? false);
const handleEnter = useCallback(() => {
if (!autoPlayGif) {
setAnimate(true);
}
}, []);
const handleLeave = useCallback(() => {
if (!autoPlayGif) {
setAnimate(false);
}
}, []);
return (
<Wrapper {...props} onMouseEnter={handleEnter} onMouseLeave={handleLeave}>
<AnimateEmojiContext.Provider value={animate}>
{children}
</AnimateEmojiContext.Provider>
</Wrapper>
);
};
// Handle custom emoji
export const CustomEmojiContext = createContext<ExtraCustomEmojiMap>({});
export const CustomEmojiProvider = ({
children,
emoji,
}: PropsWithChildren<{ emoji: ExtraCustomEmojiMap }>) => {
return (
<CustomEmojiContext.Provider value={emoji}>
{children}
</CustomEmojiContext.Provider>
);
};
export const RootCustomEmojiProvider = ({ children }: PropsWithChildren) => {
const emoji = useAppSelector((state) =>
(state.custom_emojis as ImmutableList<ApiCustomEmojiJSON>)
.toJS()
.reduce<ExtraCustomEmojiMap>(
(acc, emoji) => ({ ...acc, [emoji.shortcode]: emoji }),
{},
),
);
return (
<CustomEmojiContext.Provider value={emoji}>
{children}
</CustomEmojiContext.Provider>
);
};

View File

@ -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<EmojiStateCustom> | 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 (
<img
src={animate ? state.data.url : state.data.static_url}
alt={state.code}
className='emoji'
/>
);
}
// TODO: Load data
if (!state.data) {
return null;
}
return (
<img
src={`/emoji/${state.code}.svg`}
alt={state.data.label}
className='emoji'
/>
);
};