mirror of
https://github.com/mastodon/mastodon.git
synced 2025-10-05 16:42:47 +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;
|
htmlString: string;
|
||||||
extraEmojis?: CustomEmojiMapArg;
|
extraEmojis?: CustomEmojiMapArg;
|
||||||
as?: Element;
|
as?: Element;
|
||||||
|
shallow?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EmojiHTML = <Element extends ElementType>({
|
export const EmojiHTML = <Element extends ElementType>({
|
||||||
extraEmojis,
|
extraEmojis,
|
||||||
htmlString,
|
htmlString,
|
||||||
as: asElement, // Rename for syntax highlighting
|
as: asElement, // Rename for syntax highlighting
|
||||||
|
shallow,
|
||||||
...props
|
...props
|
||||||
}: EmojiHTMLProps<Element>) => {
|
}: EmojiHTMLProps<Element>) => {
|
||||||
const Wrapper = asElement ?? 'div';
|
const Wrapper = asElement ?? 'div';
|
||||||
const emojifiedHtml = useEmojify(htmlString, extraEmojis);
|
const emojifiedHtml = useEmojify({
|
||||||
|
text: htmlString,
|
||||||
|
extraEmojis,
|
||||||
|
deep: !shallow,
|
||||||
|
});
|
||||||
|
|
||||||
if (emojifiedHtml === null) {
|
if (emojifiedHtml === null) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { isModernEmojiEnabled } from '@/mastodon/utils/environment';
|
||||||
|
|
||||||
import { toSupportedLocale } from './locale';
|
import { toSupportedLocale } from './locale';
|
||||||
import { determineEmojiMode } from './mode';
|
import { determineEmojiMode } from './mode';
|
||||||
import { emojifyElement } from './render';
|
import { emojifyElement, emojifyText } from './render';
|
||||||
import type {
|
import type {
|
||||||
CustomEmojiMapArg,
|
CustomEmojiMapArg,
|
||||||
EmojiAppState,
|
EmojiAppState,
|
||||||
|
@ -16,7 +16,17 @@ import type {
|
||||||
} from './types';
|
} from './types';
|
||||||
import { stringHasAnyEmoji } from './utils';
|
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 [emojifiedText, setEmojifiedText] = useState<string | null>(null);
|
||||||
|
|
||||||
const appState = useEmojiAppState();
|
const appState = useEmojiAppState();
|
||||||
|
@ -37,16 +47,23 @@ export function useEmojify(text: string, extraEmojis?: CustomEmojiMapArg) {
|
||||||
|
|
||||||
const emojify = useCallback(
|
const emojify = useCallback(
|
||||||
async (input: string) => {
|
async (input: string) => {
|
||||||
|
let result: string | null = null;
|
||||||
|
if (deep) {
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
wrapper.innerHTML = input;
|
wrapper.innerHTML = input;
|
||||||
const result = await emojifyElement(wrapper, appState, extra);
|
if (await emojifyElement(wrapper, appState, extra)) {
|
||||||
|
result = wrapper.innerHTML;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result = await emojifyText(text, appState, extra);
|
||||||
|
}
|
||||||
if (result) {
|
if (result) {
|
||||||
setEmojifiedText(result.innerHTML);
|
setEmojifiedText(result);
|
||||||
} else {
|
} else {
|
||||||
setEmojifiedText(input);
|
setEmojifiedText(input);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[appState, extra],
|
[appState, deep, extra, text],
|
||||||
);
|
);
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (isModernEmojiEnabled() && !!text.trim() && stringHasAnyEmoji(text)) {
|
if (isModernEmojiEnabled() && !!text.trim() && stringHasAnyEmoji(text)) {
|
||||||
|
|
|
@ -103,7 +103,7 @@ export async function emojifyText(
|
||||||
text: string,
|
text: string,
|
||||||
appState: EmojiAppState,
|
appState: EmojiAppState,
|
||||||
extraEmojis: ExtraCustomEmojiMap = {},
|
extraEmojis: ExtraCustomEmojiMap = {},
|
||||||
): Promise<string | null> {
|
) {
|
||||||
const cacheKey = createCacheKey(text, appState, extraEmojis);
|
const cacheKey = createCacheKey(text, appState, extraEmojis);
|
||||||
const cached = getCached(cacheKey);
|
const cached = getCached(cacheKey);
|
||||||
if (cached !== undefined) {
|
if (cached !== undefined) {
|
||||||
|
|
|
@ -2567,7 +2567,6 @@ a.account__display-name {
|
||||||
}
|
}
|
||||||
|
|
||||||
.display-name {
|
.display-name {
|
||||||
display: block;
|
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user