diff --git a/app/javascript/mastodon/components/__tests__/__snapshots__/display_name-test.jsx.snap b/app/javascript/mastodon/components/__tests__/__snapshots__/display_name-test.jsx.snap deleted file mode 100644 index 9d1b236fad0..00000000000 --- a/app/javascript/mastodon/components/__tests__/__snapshots__/display_name-test.jsx.snap +++ /dev/null @@ -1,27 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[` > renders display name + account name 1`] = ` - - - Foo

", - } - } - /> -
- - - @ - bar@baz - -
-`; diff --git a/app/javascript/mastodon/components/__tests__/display_name-test.jsx b/app/javascript/mastodon/components/__tests__/display_name-test.jsx deleted file mode 100644 index 05a0f47170f..00000000000 --- a/app/javascript/mastodon/components/__tests__/display_name-test.jsx +++ /dev/null @@ -1,19 +0,0 @@ -import { fromJS } from 'immutable'; - -import renderer from 'react-test-renderer'; - -import { DisplayName } from '../display_name'; - -describe('', () => { - it('renders display name + account name', () => { - const account = fromJS({ - username: 'bar', - acct: 'bar@baz', - display_name_html: '

Foo

', - }); - const component = renderer.create(); - const tree = component.toJSON(); - - expect(tree).toMatchSnapshot(); - }); -}); diff --git a/app/javascript/mastodon/components/display_name.tsx b/app/javascript/mastodon/components/display_name.tsx deleted file mode 100644 index 8409244827e..00000000000 --- a/app/javascript/mastodon/components/display_name.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import React from 'react'; - -import type { List } from 'immutable'; - -import type { Account } from 'mastodon/models/account'; - -import { autoPlayGif } from '../initial_state'; - -import { Skeleton } from './skeleton'; - -interface Props { - account?: Account; - others?: List; - localDomain?: string; -} - -export class DisplayName extends React.PureComponent { - handleMouseEnter: React.ReactEventHandler = ({ - currentTarget, - }) => { - if (autoPlayGif) { - return; - } - - const emojis = - currentTarget.querySelectorAll('img.custom-emoji'); - - emojis.forEach((emoji) => { - const originalSrc = emoji.getAttribute('data-original'); - if (originalSrc != null) emoji.src = originalSrc; - }); - }; - - handleMouseLeave: React.ReactEventHandler = ({ - currentTarget, - }) => { - if (autoPlayGif) { - return; - } - - const emojis = - currentTarget.querySelectorAll('img.custom-emoji'); - - emojis.forEach((emoji) => { - const staticSrc = emoji.getAttribute('data-static'); - if (staticSrc != null) emoji.src = staticSrc; - }); - }; - - render() { - const { others, localDomain } = this.props; - - let displayName: React.ReactNode, - suffix: React.ReactNode, - account: Account | undefined; - - if (others && others.size > 0) { - account = others.first(); - } else if (this.props.account) { - account = this.props.account; - } - - if (others && others.size > 1) { - displayName = others - .take(2) - .map((a) => ( - - - - )) - .reduce((prev, cur) => [prev, ', ', cur]); - - if (others.size - 2 > 0) { - suffix = `+${others.size - 2}`; - } - } else if (account) { - let acct = account.get('acct'); - - if (!acct.includes('@') && localDomain) { - acct = `${acct}@${localDomain}`; - } - - displayName = ( - - - - ); - suffix = @{acct}; - } else { - displayName = ( - - - - - - ); - suffix = ( - - - - ); - } - - return ( - - {displayName} {suffix} - - ); - } -} diff --git a/app/javascript/mastodon/components/display_name/display_name.stories.tsx b/app/javascript/mastodon/components/display_name/display_name.stories.tsx new file mode 100644 index 00000000000..8d58ebb9772 --- /dev/null +++ b/app/javascript/mastodon/components/display_name/display_name.stories.tsx @@ -0,0 +1,69 @@ +import type { ComponentProps } from 'react'; + +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { accountFactoryState } from '@/testing/factories'; + +import { DisplayName } from '.'; + +type PageProps = Omit, 'account'> & { + name: string; + username: string; + loading: boolean; +}; + +const meta = { + title: 'Components/DisplayName', + args: { + username: 'mastodon@mastodon.social', + name: 'Test User 🧪', + loading: false, + simple: false, + noDomain: false, + localDomain: 'mastodon.social', + }, + tags: [], + render({ name, username, loading, ...args }) { + const account = !loading + ? accountFactoryState({ + display_name: name, + acct: username, + }) + : undefined; + return ; + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Primary: Story = { + args: {}, +}; + +export const Loading: Story = { + args: { + loading: true, + }, +}; + +export const NoDomain: Story = { + args: { + noDomain: true, + }, +}; + +export const Simple: Story = { + args: { + simple: true, + }, +}; + +export const LocalUser: Story = { + args: { + username: 'locale', + name: 'Local User', + localDomain: '', + }, +}; diff --git a/app/javascript/mastodon/components/display_name/index.tsx b/app/javascript/mastodon/components/display_name/index.tsx new file mode 100644 index 00000000000..5cc7efdbecd --- /dev/null +++ b/app/javascript/mastodon/components/display_name/index.tsx @@ -0,0 +1,87 @@ +import type { ComponentPropsWithoutRef, FC } from 'react'; +import { useMemo } from 'react'; + +import { EmojiHTML } from '@/mastodon/features/emoji/emoji_html'; +import type { Account } from '@/mastodon/models/account'; +import { isModernEmojiEnabled } from '@/mastodon/utils/environment'; + +import { Skeleton } from '../skeleton'; + +interface Props { + account?: Account; + localDomain?: string; + simple?: boolean; + noDomain?: boolean; +} + +export const DisplayName: FC> = ({ + account, + localDomain, + simple = false, + noDomain = false, + className = '', + ...props +}) => { + const username = useMemo(() => { + if (!account || noDomain) { + return null; + } + let acct = account.get('acct'); + + if (!acct.includes('@') && localDomain) { + acct = `${acct}@${localDomain}`; + } + return `@${acct}`; + }, [account, localDomain, noDomain]); + if (!account) { + if (simple) { + return null; + } + return ( + + + + + + + {!noDomain && ( + + + + )} + + ); + } + if (simple) { + return ( + + ); + } + return ( + + + + + {username && {username}} + + ); +}; diff --git a/app/javascript/mastodon/features/emoji/emoji_html.tsx b/app/javascript/mastodon/features/emoji/emoji_html.tsx index fdda62a3e61..0b677cb1189 100644 --- a/app/javascript/mastodon/features/emoji/emoji_html.tsx +++ b/app/javascript/mastodon/features/emoji/emoji_html.tsx @@ -10,16 +10,22 @@ type EmojiHTMLProps = Omit< htmlString: string; extraEmojis?: CustomEmojiMapArg; as?: Element; + shallow?: boolean; }; export const EmojiHTML = ({ extraEmojis, htmlString, as: asElement, // Rename for syntax highlighting + shallow, ...props }: EmojiHTMLProps) => { const Wrapper = asElement ?? 'div'; - const emojifiedHtml = useEmojify(htmlString, extraEmojis); + const emojifiedHtml = useEmojify({ + text: htmlString, + extraEmojis, + deep: !shallow, + }); if (emojifiedHtml === null) { return null; diff --git a/app/javascript/mastodon/features/emoji/hooks.ts b/app/javascript/mastodon/features/emoji/hooks.ts index 47af37b3731..7e91486780a 100644 --- a/app/javascript/mastodon/features/emoji/hooks.ts +++ b/app/javascript/mastodon/features/emoji/hooks.ts @@ -8,7 +8,7 @@ import { isModernEmojiEnabled } from '@/mastodon/utils/environment'; import { toSupportedLocale } from './locale'; import { determineEmojiMode } from './mode'; -import { emojifyElement } from './render'; +import { emojifyElement, emojifyText } from './render'; import type { CustomEmojiMapArg, EmojiAppState, @@ -16,7 +16,17 @@ import type { } from './types'; import { stringHasAnyEmoji } from './utils'; -export function useEmojify(text: string, extraEmojis?: CustomEmojiMapArg) { +interface UseEmojifyOptions { + text: string; + extraEmojis?: CustomEmojiMapArg; + deep?: boolean; +} + +export function useEmojify({ + text, + extraEmojis, + deep = true, +}: UseEmojifyOptions) { const [emojifiedText, setEmojifiedText] = useState(null); const appState = useEmojiAppState(); @@ -37,16 +47,23 @@ export function useEmojify(text: string, extraEmojis?: CustomEmojiMapArg) { const emojify = useCallback( async (input: string) => { - const wrapper = document.createElement('div'); - wrapper.innerHTML = input; - const result = await emojifyElement(wrapper, appState, extra); + let result: string | null = null; + if (deep) { + const wrapper = document.createElement('div'); + wrapper.innerHTML = input; + if (await emojifyElement(wrapper, appState, extra)) { + result = wrapper.innerHTML; + } + } else { + result = await emojifyText(text, appState, extra); + } if (result) { - setEmojifiedText(result.innerHTML); + setEmojifiedText(result); } else { setEmojifiedText(input); } }, - [appState, extra], + [appState, deep, extra, text], ); useLayoutEffect(() => { if (isModernEmojiEnabled() && !!text.trim() && stringHasAnyEmoji(text)) { diff --git a/app/javascript/mastodon/features/emoji/render.ts b/app/javascript/mastodon/features/emoji/render.ts index 6486e65a709..68c4b8964b3 100644 --- a/app/javascript/mastodon/features/emoji/render.ts +++ b/app/javascript/mastodon/features/emoji/render.ts @@ -103,7 +103,7 @@ export async function emojifyText( text: string, appState: EmojiAppState, extraEmojis: ExtraCustomEmojiMap = {}, -): Promise { +) { const cacheKey = createCacheKey(text, appState, extraEmojis); const cached = getCached(cacheKey); if (cached !== undefined) { diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index d6f0087cc67..2642dff0391 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -2567,7 +2567,6 @@ a.account__display-name { } .display-name { - display: block; max-width: 100%; overflow: hidden; text-overflow: ellipsis;