mirror of
https://github.com/mastodon/mastodon.git
synced 2025-10-05 08:33:00 +00:00
Replace DisplayName with new component
This commit is contained in:
parent
6bca52453a
commit
f84982e5cf
|
@ -1,27 +0,0 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<DisplayName /> > renders display name + account name 1`] = `
|
||||
<span
|
||||
className="display-name"
|
||||
onMouseEnter={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
>
|
||||
<bdi>
|
||||
<strong
|
||||
className="display-name__html"
|
||||
dangerouslySetInnerHTML={
|
||||
{
|
||||
"__html": "<p>Foo</p>",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</bdi>
|
||||
|
||||
<span
|
||||
className="display-name__account"
|
||||
>
|
||||
@
|
||||
bar@baz
|
||||
</span>
|
||||
</span>
|
||||
`;
|
|
@ -1,19 +0,0 @@
|
|||
import { fromJS } from 'immutable';
|
||||
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
import { DisplayName } from '../display_name';
|
||||
|
||||
describe('<DisplayName />', () => {
|
||||
it('renders display name + account name', () => {
|
||||
const account = fromJS({
|
||||
username: 'bar',
|
||||
acct: 'bar@baz',
|
||||
display_name_html: '<p>Foo</p>',
|
||||
});
|
||||
const component = renderer.create(<DisplayName account={account} />);
|
||||
const tree = component.toJSON();
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -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<Account>;
|
||||
localDomain?: string;
|
||||
}
|
||||
|
||||
export class DisplayName extends React.PureComponent<Props> {
|
||||
handleMouseEnter: React.ReactEventHandler<HTMLSpanElement> = ({
|
||||
currentTarget,
|
||||
}) => {
|
||||
if (autoPlayGif) {
|
||||
return;
|
||||
}
|
||||
|
||||
const emojis =
|
||||
currentTarget.querySelectorAll<HTMLImageElement>('img.custom-emoji');
|
||||
|
||||
emojis.forEach((emoji) => {
|
||||
const originalSrc = emoji.getAttribute('data-original');
|
||||
if (originalSrc != null) emoji.src = originalSrc;
|
||||
});
|
||||
};
|
||||
|
||||
handleMouseLeave: React.ReactEventHandler<HTMLSpanElement> = ({
|
||||
currentTarget,
|
||||
}) => {
|
||||
if (autoPlayGif) {
|
||||
return;
|
||||
}
|
||||
|
||||
const emojis =
|
||||
currentTarget.querySelectorAll<HTMLImageElement>('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) => (
|
||||
<bdi key={a.get('id')}>
|
||||
<strong
|
||||
className='display-name__html'
|
||||
dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }}
|
||||
/>
|
||||
</bdi>
|
||||
))
|
||||
.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 = (
|
||||
<bdi>
|
||||
<strong
|
||||
className='display-name__html'
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: account.get('display_name_html'),
|
||||
}}
|
||||
/>
|
||||
</bdi>
|
||||
);
|
||||
suffix = <span className='display-name__account'>@{acct}</span>;
|
||||
} else {
|
||||
displayName = (
|
||||
<bdi>
|
||||
<strong className='display-name__html'>
|
||||
<Skeleton width='10ch' />
|
||||
</strong>
|
||||
</bdi>
|
||||
);
|
||||
suffix = (
|
||||
<span className='display-name__account'>
|
||||
<Skeleton width='7ch' />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
className='display-name'
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
>
|
||||
{displayName} {suffix}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<ComponentProps<typeof DisplayName>, '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 <DisplayName {...args} account={account} />;
|
||||
},
|
||||
} satisfies Meta<PageProps>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
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: '',
|
||||
},
|
||||
};
|
87
app/javascript/mastodon/components/display_name/index.tsx
Normal file
87
app/javascript/mastodon/components/display_name/index.tsx
Normal file
|
@ -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<Props & ComponentPropsWithoutRef<'span'>> = ({
|
||||
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 (
|
||||
<span {...props} className={`display-name ${className}`}>
|
||||
<bdi>
|
||||
<strong className='display-name__html'>
|
||||
<Skeleton width='10ch' />
|
||||
</strong>
|
||||
</bdi>
|
||||
{!noDomain && (
|
||||
<span className='display-name__account'>
|
||||
<Skeleton width='7ch' />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (simple) {
|
||||
return (
|
||||
<EmojiHTML
|
||||
{...props}
|
||||
htmlString={
|
||||
isModernEmojiEnabled()
|
||||
? account.get('display_name')
|
||||
: account.get('display_name_html')
|
||||
}
|
||||
shallow
|
||||
as='span'
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span {...props} className={`display-name ${className}`}>
|
||||
<bdi>
|
||||
<EmojiHTML
|
||||
{...props}
|
||||
className={`display-name__html ${className}`}
|
||||
htmlString={
|
||||
isModernEmojiEnabled()
|
||||
? account.get('display_name')
|
||||
: account.get('display_name_html')
|
||||
}
|
||||
shallow
|
||||
as='strong'
|
||||
/>
|
||||
</bdi>
|
||||
{username && <span className='display-name__account'>{username}</span>}
|
||||
</span>
|
||||
);
|
||||
};
|
|
@ -10,16 +10,22 @@ type EmojiHTMLProps<Element extends ElementType = 'div'> = Omit<
|
|||
htmlString: string;
|
||||
extraEmojis?: CustomEmojiMapArg;
|
||||
as?: Element;
|
||||
shallow?: boolean;
|
||||
};
|
||||
|
||||
export const EmojiHTML = <Element extends ElementType>({
|
||||
extraEmojis,
|
||||
htmlString,
|
||||
as: asElement, // Rename for syntax highlighting
|
||||
shallow,
|
||||
...props
|
||||
}: EmojiHTMLProps<Element>) => {
|
||||
const Wrapper = asElement ?? 'div';
|
||||
const emojifiedHtml = useEmojify(htmlString, extraEmojis);
|
||||
const emojifiedHtml = useEmojify({
|
||||
text: htmlString,
|
||||
extraEmojis,
|
||||
deep: !shallow,
|
||||
});
|
||||
|
||||
if (emojifiedHtml === null) {
|
||||
return null;
|
||||
|
|
|
@ -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<string | null>(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)) {
|
||||
|
|
|
@ -103,7 +103,7 @@ export async function emojifyText(
|
|||
text: string,
|
||||
appState: EmojiAppState,
|
||||
extraEmojis: ExtraCustomEmojiMap = {},
|
||||
): Promise<string | null> {
|
||||
) {
|
||||
const cacheKey = createCacheKey(text, appState, extraEmojis);
|
||||
const cached = getCached(cacheKey);
|
||||
if (cached !== undefined) {
|
||||
|
|
|
@ -2567,7 +2567,6 @@ a.account__display-name {
|
|||
}
|
||||
|
||||
.display-name {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
|
Loading…
Reference in New Issue
Block a user