mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-15 08:48:53 +00:00
[Glitch] Emoji Component
Port c12b8f51c1 to glitch-soc
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
@@ -1,11 +1,15 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { useLinks } from 'flavours/glitch/hooks/useLinks';
|
||||
|
||||
import { EmojiHTML } from '../features/emoji/emoji_html';
|
||||
import { useAppSelector } from '../store';
|
||||
import { isModernEmojiEnabled } from '../utils/environment';
|
||||
|
||||
import { AnimateEmojiProvider } from './emoji/context';
|
||||
import { EmojiHTML } from './emoji/html';
|
||||
|
||||
interface AccountBioProps {
|
||||
className: string;
|
||||
accountId: string;
|
||||
@@ -44,13 +48,13 @@ export const AccountBio: React.FC<AccountBioProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${className} translate`}
|
||||
<AnimateEmojiProvider
|
||||
className={classNames(className, 'translate')}
|
||||
onClickCapture={handleClick}
|
||||
ref={handleNodeChange}
|
||||
>
|
||||
<EmojiHTML htmlString={note} extraEmojis={extraEmojis} />
|
||||
</div>
|
||||
</AnimateEmojiProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,9 +2,10 @@ import type { ComponentPropsWithoutRef, FC } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { EmojiHTML } from '@/flavours/glitch/features/emoji/emoji_html';
|
||||
import { isModernEmojiEnabled } from '@/flavours/glitch/utils/environment';
|
||||
|
||||
import { AnimateEmojiProvider } from '../emoji/context';
|
||||
import { EmojiHTML } from '../emoji/html';
|
||||
import { Skeleton } from '../skeleton';
|
||||
|
||||
import type { DisplayNameProps } from './index';
|
||||
@@ -14,9 +15,10 @@ export const DisplayNameWithoutDomain: FC<
|
||||
ComponentPropsWithoutRef<'span'>
|
||||
> = ({ account, className, children, ...props }) => {
|
||||
return (
|
||||
<span
|
||||
<AnimateEmojiProvider
|
||||
{...props}
|
||||
className={classNames('display-name animate-parent', className)}
|
||||
as='span'
|
||||
className={classNames('display-name', className)}
|
||||
>
|
||||
<bdi>
|
||||
{account ? (
|
||||
@@ -27,8 +29,8 @@ export const DisplayNameWithoutDomain: FC<
|
||||
? account.get('display_name')
|
||||
: account.get('display_name_html')
|
||||
}
|
||||
shallow
|
||||
as='strong'
|
||||
extraEmojis={account.get('emojis')}
|
||||
/>
|
||||
) : (
|
||||
<strong className='display-name__html'>
|
||||
@@ -37,6 +39,6 @@ export const DisplayNameWithoutDomain: FC<
|
||||
)}
|
||||
</bdi>
|
||||
{children}
|
||||
</span>
|
||||
</AnimateEmojiProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { ComponentPropsWithoutRef, FC } from 'react';
|
||||
|
||||
import { EmojiHTML } from '@/flavours/glitch/features/emoji/emoji_html';
|
||||
import { isModernEmojiEnabled } from '@/flavours/glitch/utils/environment';
|
||||
|
||||
import { EmojiHTML } from '../emoji/html';
|
||||
|
||||
import type { DisplayNameProps } from './index';
|
||||
|
||||
export const DisplayNameSimple: FC<
|
||||
@@ -12,12 +13,19 @@ export const DisplayNameSimple: FC<
|
||||
if (!account) {
|
||||
return null;
|
||||
}
|
||||
const accountName = isModernEmojiEnabled()
|
||||
? account.get('display_name')
|
||||
: account.get('display_name_html');
|
||||
|
||||
return (
|
||||
<bdi>
|
||||
<EmojiHTML {...props} htmlString={accountName} shallow as='span' />
|
||||
<EmojiHTML
|
||||
{...props}
|
||||
as='span'
|
||||
htmlString={
|
||||
isModernEmojiEnabled()
|
||||
? account.get('display_name')
|
||||
: account.get('display_name_html')
|
||||
}
|
||||
extraEmojis={account.get('emojis')}
|
||||
/>
|
||||
</bdi>
|
||||
);
|
||||
};
|
||||
|
||||
108
app/javascript/flavours/glitch/components/emoji/context.tsx
Normal file
108
app/javascript/flavours/glitch/components/emoji/context.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import type { MouseEventHandler, PropsWithChildren } from 'react';
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { cleanExtraEmojis } from '@/flavours/glitch/features/emoji/normalize';
|
||||
import { autoPlayGif } from '@/flavours/glitch/initial_state';
|
||||
import { polymorphicForwardRef } from '@/types/polymorphic';
|
||||
import type {
|
||||
CustomEmojiMapArg,
|
||||
ExtraCustomEmojiMap,
|
||||
} from 'flavours/glitch/features/emoji/types';
|
||||
|
||||
// Animation context
|
||||
export const AnimateEmojiContext = createContext<boolean | null>(null);
|
||||
|
||||
// Polymorphic provider component
|
||||
type AnimateEmojiProviderProps = Required<PropsWithChildren> & {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const AnimateEmojiProvider = polymorphicForwardRef<
|
||||
'div',
|
||||
AnimateEmojiProviderProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
as: Wrapper = 'div',
|
||||
className,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const [animate, setAnimate] = useState(autoPlayGif ?? false);
|
||||
|
||||
const handleEnter: MouseEventHandler<HTMLDivElement> = useCallback(
|
||||
(event) => {
|
||||
onMouseEnter?.(event);
|
||||
if (!autoPlayGif) {
|
||||
setAnimate(true);
|
||||
}
|
||||
},
|
||||
[onMouseEnter],
|
||||
);
|
||||
const handleLeave: MouseEventHandler<HTMLDivElement> = useCallback(
|
||||
(event) => {
|
||||
onMouseLeave?.(event);
|
||||
if (!autoPlayGif) {
|
||||
setAnimate(false);
|
||||
}
|
||||
},
|
||||
[onMouseLeave],
|
||||
);
|
||||
|
||||
// If there's a parent context or GIFs autoplay, we don't need handlers.
|
||||
const parentContext = useContext(AnimateEmojiContext);
|
||||
if (parentContext !== null || autoPlayGif === true) {
|
||||
return (
|
||||
<Wrapper
|
||||
{...props}
|
||||
className={classNames(className, 'animate-parent')}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Wrapper
|
||||
{...props}
|
||||
className={classNames(className, 'animate-parent')}
|
||||
onMouseEnter={handleEnter}
|
||||
onMouseLeave={handleLeave}
|
||||
ref={ref}
|
||||
>
|
||||
<AnimateEmojiContext.Provider value={animate}>
|
||||
{children}
|
||||
</AnimateEmojiContext.Provider>
|
||||
</Wrapper>
|
||||
);
|
||||
},
|
||||
);
|
||||
AnimateEmojiProvider.displayName = 'AnimateEmojiProvider';
|
||||
|
||||
// Handle custom emoji
|
||||
export const CustomEmojiContext = createContext<ExtraCustomEmojiMap>({});
|
||||
|
||||
export const CustomEmojiProvider = ({
|
||||
children,
|
||||
emojis: rawEmojis,
|
||||
}: PropsWithChildren<{ emojis?: CustomEmojiMapArg }>) => {
|
||||
const emojis = useMemo(() => cleanExtraEmojis(rawEmojis) ?? {}, [rawEmojis]);
|
||||
return (
|
||||
<CustomEmojiContext.Provider value={emojis}>
|
||||
{children}
|
||||
</CustomEmojiContext.Provider>
|
||||
);
|
||||
};
|
||||
61
app/javascript/flavours/glitch/components/emoji/html.tsx
Normal file
61
app/javascript/flavours/glitch/components/emoji/html.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { useMemo } from 'react';
|
||||
import type { ComponentPropsWithoutRef, ElementType } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import type { CustomEmojiMapArg } from '@/flavours/glitch/features/emoji/types';
|
||||
import { isModernEmojiEnabled } from '@/flavours/glitch/utils/environment';
|
||||
import { htmlStringToComponents } from '@/flavours/glitch/utils/html';
|
||||
|
||||
import { AnimateEmojiProvider, CustomEmojiProvider } from './context';
|
||||
import { textToEmojis } from './index';
|
||||
|
||||
type EmojiHTMLProps<Element extends ElementType = 'div'> = Omit<
|
||||
ComponentPropsWithoutRef<Element>,
|
||||
'dangerouslySetInnerHTML' | 'className'
|
||||
> & {
|
||||
htmlString: string;
|
||||
extraEmojis?: CustomEmojiMapArg;
|
||||
as?: Element;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const ModernEmojiHTML = ({
|
||||
extraEmojis,
|
||||
htmlString,
|
||||
as: asProp = 'div', // Rename for syntax highlighting
|
||||
shallow,
|
||||
className = '',
|
||||
...props
|
||||
}: EmojiHTMLProps<ElementType>) => {
|
||||
const contents = useMemo(
|
||||
() => htmlStringToComponents(htmlString, { onText: textToEmojis }),
|
||||
[htmlString],
|
||||
);
|
||||
|
||||
return (
|
||||
<CustomEmojiProvider emojis={extraEmojis}>
|
||||
<AnimateEmojiProvider {...props} as={asProp} className={className}>
|
||||
{contents}
|
||||
</AnimateEmojiProvider>
|
||||
</CustomEmojiProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const LegacyEmojiHTML = <Element extends ElementType>(
|
||||
props: EmojiHTMLProps<Element>,
|
||||
) => {
|
||||
const { as: asElement, htmlString, extraEmojis, className, ...rest } = props;
|
||||
const Wrapper = asElement ?? 'div';
|
||||
return (
|
||||
<Wrapper
|
||||
{...rest}
|
||||
dangerouslySetInnerHTML={{ __html: htmlString }}
|
||||
className={classNames(className, 'animate-parent')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const EmojiHTML = isModernEmojiEnabled()
|
||||
? ModernEmojiHTML
|
||||
: LegacyEmojiHTML;
|
||||
99
app/javascript/flavours/glitch/components/emoji/index.tsx
Normal file
99
app/javascript/flavours/glitch/components/emoji/index.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import type { FC } from 'react';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
|
||||
import { EMOJI_TYPE_CUSTOM } from '@/flavours/glitch/features/emoji/constants';
|
||||
import { useEmojiAppState } from '@/flavours/glitch/features/emoji/hooks';
|
||||
import { unicodeHexToUrl } from '@/flavours/glitch/features/emoji/normalize';
|
||||
import {
|
||||
isStateLoaded,
|
||||
loadEmojiDataToState,
|
||||
shouldRenderImage,
|
||||
stringToEmojiState,
|
||||
tokenizeText,
|
||||
} from '@/flavours/glitch/features/emoji/render';
|
||||
|
||||
import { AnimateEmojiContext, CustomEmojiContext } from './context';
|
||||
|
||||
interface EmojiProps {
|
||||
code: string;
|
||||
showFallback?: boolean;
|
||||
showLoading?: boolean;
|
||||
}
|
||||
|
||||
export const Emoji: FC<EmojiProps> = ({
|
||||
code,
|
||||
showFallback = true,
|
||||
showLoading = true,
|
||||
}) => {
|
||||
const customEmoji = useContext(CustomEmojiContext);
|
||||
|
||||
// First, set the emoji state based on the input code.
|
||||
const [state, setState] = useState(() =>
|
||||
stringToEmojiState(code, customEmoji),
|
||||
);
|
||||
|
||||
// If we don't have data, then load emoji data asynchronously.
|
||||
const appState = useEmojiAppState();
|
||||
useEffect(() => {
|
||||
if (state !== null) {
|
||||
void loadEmojiDataToState(state, appState.currentLocale).then(setState);
|
||||
}
|
||||
}, [appState.currentLocale, state]);
|
||||
|
||||
const animate = useContext(AnimateEmojiContext);
|
||||
const fallback = showFallback ? code : null;
|
||||
|
||||
// If the code is invalid or we otherwise know it's not valid, show the fallback.
|
||||
if (!state) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
if (!shouldRenderImage(state, appState.mode)) {
|
||||
return code;
|
||||
}
|
||||
|
||||
if (!isStateLoaded(state)) {
|
||||
if (showLoading) {
|
||||
return <span className='emojione emoji-loading' title={code} />;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
if (state.type === EMOJI_TYPE_CUSTOM) {
|
||||
const shortcode = `:${state.code}:`;
|
||||
return (
|
||||
<img
|
||||
src={animate ? state.data.url : state.data.static_url}
|
||||
alt={shortcode}
|
||||
title={shortcode}
|
||||
className='emojione custom-emoji'
|
||||
loading='lazy'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const src = unicodeHexToUrl(state.code, appState.darkTheme);
|
||||
|
||||
return (
|
||||
<img
|
||||
src={src}
|
||||
alt={state.data.unicode}
|
||||
title={state.data.label}
|
||||
className='emojione'
|
||||
loading='lazy'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Takes a text string and converts it to an array of React nodes.
|
||||
* @param text The text to be tokenized and converted.
|
||||
*/
|
||||
export function textToEmojis(text: string) {
|
||||
return tokenizeText(text).map((token, index) => {
|
||||
if (typeof token === 'string') {
|
||||
return token;
|
||||
}
|
||||
return <Emoji code={token.code} key={`emoji-${token.code}-${index}`} />;
|
||||
});
|
||||
}
|
||||
@@ -13,11 +13,13 @@ import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react'
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
import { Poll } from 'flavours/glitch/components/poll';
|
||||
import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context';
|
||||
import { autoPlayGif, languages as preloadedLanguages } from 'flavours/glitch/initial_state';
|
||||
import { languages as preloadedLanguages } from 'flavours/glitch/initial_state';
|
||||
import { decode as decodeIDNA } from 'flavours/glitch/utils/idna';
|
||||
import { EmojiHTML } from '../features/emoji/emoji_html';
|
||||
|
||||
import { isModernEmojiEnabled } from '../utils/environment';
|
||||
|
||||
import { EmojiHTML } from './emoji/html';
|
||||
|
||||
const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top)
|
||||
|
||||
const textMatchesTarget = (text, origin, host) => {
|
||||
|
||||
Reference in New Issue
Block a user