mirror of
https://github.com/mastodon/mastodon.git
synced 2025-10-05 16:42:47 +00:00
add new render and context components
Some checks are pending
Chromatic / Run Chromatic (push) Waiting to run
Some checks are pending
Chromatic / Run Chromatic (push) Waiting to run
This commit is contained in:
parent
42b5b4247b
commit
e4566d4f5a
77
app/javascript/mastodon/components/emoji/context.tsx
Normal file
77
app/javascript/mastodon/components/emoji/context.tsx
Normal 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>
|
||||
);
|
||||
};
|
85
app/javascript/mastodon/components/emoji/index.tsx
Normal file
85
app/javascript/mastodon/components/emoji/index.tsx
Normal 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'
|
||||
/>
|
||||
);
|
||||
};
|
Loading…
Reference in New Issue
Block a user