Refactor: Replace all display name usage for new component (#36137)

This commit is contained in:
Echo 2025-09-17 11:00:57 +02:00 committed by GitHub
parent ff03938808
commit dfef7d9407
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 198 additions and 364 deletions

View File

@ -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>
`;

View File

@ -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();
});
});

View File

@ -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>
);
}
}

View File

@ -0,0 +1,36 @@
import { useMemo } from 'react';
import type { ComponentPropsWithoutRef, FC } from 'react';
import { Skeleton } from '../skeleton';
import type { DisplayNameProps } from './index';
import { DisplayNameWithoutDomain } from './no-domain';
export const DisplayNameDefault: FC<
Omit<DisplayNameProps, 'variant'> & ComponentPropsWithoutRef<'span'>
> = ({ account, localDomain, className, ...props }) => {
const username = useMemo(() => {
if (!account) {
return null;
}
let acct = account.get('acct');
if (!acct.includes('@') && localDomain) {
acct = `${acct}@${localDomain}`;
}
return `@${acct}`;
}, [account, localDomain]);
return (
<DisplayNameWithoutDomain
account={account}
className={className}
{...props}
>
{' '}
<span className='display-name__account'>
{username ?? <Skeleton width='7ch' />}
</span>
</DisplayNameWithoutDomain>
);
};

View File

@ -18,8 +18,6 @@ const meta = {
username: 'mastodon@mastodon.social', username: 'mastodon@mastodon.social',
name: 'Test User 🧪', name: 'Test User 🧪',
loading: false, loading: false,
simple: false,
noDomain: false,
localDomain: 'mastodon.social', localDomain: 'mastodon.social',
}, },
tags: [], tags: [],
@ -50,13 +48,13 @@ export const Loading: Story = {
export const NoDomain: Story = { export const NoDomain: Story = {
args: { args: {
noDomain: true, variant: 'noDomain',
}, },
}; };
export const Simple: Story = { export const Simple: Story = {
args: { args: {
simple: true, variant: 'simple',
}, },
}; };
@ -76,6 +74,6 @@ export const Linked: Story = {
acct: username, acct: username,
}) })
: undefined; : undefined;
return <LinkedDisplayName {...args} account={account} />; return <LinkedDisplayName {...args} displayProps={{ account }} />;
}, },
}; };

View File

@ -1,110 +1,37 @@
import type { ComponentPropsWithoutRef, FC } from 'react'; import type { ComponentPropsWithoutRef, FC } from 'react';
import { useMemo } from 'react';
import classNames from 'classnames';
import type { LinkProps } from 'react-router-dom'; import type { LinkProps } from 'react-router-dom';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { EmojiHTML } from '@/mastodon/features/emoji/emoji_html';
import type { Account } from '@/mastodon/models/account'; import type { Account } from '@/mastodon/models/account';
import { isModernEmojiEnabled } from '@/mastodon/utils/environment';
import { Skeleton } from '../skeleton'; import { DisplayNameDefault } from './default';
import { DisplayNameWithoutDomain } from './no-domain';
import { DisplayNameSimple } from './simple';
interface Props { export interface DisplayNameProps {
account?: Account; account?: Account;
localDomain?: string; localDomain?: string;
simple?: boolean; variant?: 'default' | 'simple' | 'noDomain';
noDomain?: boolean;
} }
export const DisplayName: FC<Props & ComponentPropsWithoutRef<'span'>> = ({ export const DisplayName: FC<
account, DisplayNameProps & ComponentPropsWithoutRef<'span'>
localDomain, > = ({ variant = 'default', ...props }) => {
simple = false, if (variant === 'simple') {
noDomain = false, return <DisplayNameSimple {...props} />;
className, } else if (variant === 'noDomain') {
...props return <DisplayNameWithoutDomain {...props} />;
}) => {
const username = useMemo(() => {
if (!account || noDomain) {
return null;
} }
let acct = account.get('acct'); return <DisplayNameDefault {...props} />;
if (!acct.includes('@') && localDomain) {
acct = `${acct}@${localDomain}`;
}
return `@${acct}`;
}, [account, localDomain, noDomain]);
if (!account) {
if (simple) {
return null;
}
return (
<span {...props} className={classNames('display-name', className)}>
<bdi>
<strong className='display-name__html'>
<Skeleton width='10ch' />
</strong>
</bdi>
{!noDomain && (
<span className='display-name__account'>
&nbsp;
<Skeleton width='7ch' />
</span>
)}
</span>
);
}
const accountName = isModernEmojiEnabled()
? account.get('display_name')
: account.get('display_name_html');
if (simple) {
return (
<bdi>
<EmojiHTML {...props} htmlString={accountName} shallow as='span' />
</bdi>
);
}
return (
<span {...props} className={classNames('display-name', className)}>
<bdi>
<EmojiHTML
className='display-name__html'
htmlString={accountName}
shallow
as='strong'
/>
</bdi>
{username && (
<span className='display-name__account'>&nbsp;{username}</span>
)}
</span>
);
}; };
export const LinkedDisplayName: FC< export const LinkedDisplayName: FC<
Props & { asProps?: ComponentPropsWithoutRef<'span'> } & Partial<LinkProps> Omit<LinkProps, 'to'> & {
> = ({ displayProps: DisplayNameProps & ComponentPropsWithoutRef<'span'>;
account, }
asProps = {}, > = ({ displayProps, children, ...linkProps }) => {
className, const { account } = displayProps;
localDomain,
simple,
noDomain,
...linkProps
}) => {
const displayProps = {
account,
className,
localDomain,
simple,
noDomain,
...asProps,
};
if (!account) { if (!account) {
return <DisplayName {...displayProps} />; return <DisplayName {...displayProps} />;
} }
@ -113,9 +40,11 @@ export const LinkedDisplayName: FC<
<Link <Link
to={`/@${account.acct}`} to={`/@${account.acct}`}
title={`@${account.acct}`} title={`@${account.acct}`}
data-id={account.id}
data-hover-card-account={account.id} data-hover-card-account={account.id}
{...linkProps} {...linkProps}
> >
{children}
<DisplayName {...displayProps} /> <DisplayName {...displayProps} />
</Link> </Link>
); );

View File

@ -0,0 +1,39 @@
import type { ComponentPropsWithoutRef, FC } from 'react';
import classNames from 'classnames';
import { EmojiHTML } from '@/mastodon/features/emoji/emoji_html';
import { isModernEmojiEnabled } from '@/mastodon/utils/environment';
import { Skeleton } from '../skeleton';
import type { DisplayNameProps } from './index';
export const DisplayNameWithoutDomain: FC<
Omit<DisplayNameProps, 'variant' | 'localDomain'> &
ComponentPropsWithoutRef<'span'>
> = ({ account, className, children, ...props }) => {
return (
<span {...props} className={classNames('display-name', className)}>
<bdi>
{account ? (
<EmojiHTML
className='display-name__html'
htmlString={
isModernEmojiEnabled()
? account.get('display_name')
: account.get('display_name_html')
}
shallow
as='strong'
/>
) : (
<strong className='display-name__html'>
<Skeleton width='10ch' />
</strong>
)}
</bdi>
{children}
</span>
);
};

View File

@ -0,0 +1,23 @@
import type { ComponentPropsWithoutRef, FC } from 'react';
import { EmojiHTML } from '@/mastodon/features/emoji/emoji_html';
import { isModernEmojiEnabled } from '@/mastodon/utils/environment';
import type { DisplayNameProps } from './index';
export const DisplayNameSimple: FC<
Omit<DisplayNameProps, 'variant' | 'localDomain'> &
ComponentPropsWithoutRef<'span'>
> = ({ account, ...props }) => {
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' />
</bdi>
);
};

View File

@ -28,7 +28,7 @@ import { displayMedia } from '../initial_state';
import { Avatar } from './avatar'; import { Avatar } from './avatar';
import { AvatarOverlay } from './avatar_overlay'; import { AvatarOverlay } from './avatar_overlay';
import { DisplayName } from './display_name'; import { LinkedDisplayName } from './display_name';
import { getHashtagBarForStatus } from './hashtag_bar'; import { getHashtagBarForStatus } from './hashtag_bar';
import { RelativeTimestamp } from './relative_timestamp'; import { RelativeTimestamp } from './relative_timestamp';
import StatusActionBar from './status_action_bar'; import StatusActionBar from './status_action_bar';
@ -409,12 +409,20 @@ class Status extends ImmutablePureComponent {
const matchedFilters = status.get('matched_filters'); const matchedFilters = status.get('matched_filters');
if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') { if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
const display_name_html = { __html: status.getIn(['account', 'display_name_html']) }; const name = (
<LinkedDisplayName
displayProps={{
account: status.get('account'),
variant: 'simple'
}}
className='status__display-name muted'
/>
)
prepend = ( prepend = (
<div className='status__prepend'> <div className='status__prepend'>
<div className='status__prepend__icon'><Icon id='retweet' icon={RepeatIcon} /></div> <div className='status__prepend__icon'><Icon id='retweet' icon={RepeatIcon} /></div>
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <Link data-id={status.getIn(['account', 'id'])} data-hover-card-account={status.getIn(['account', 'id'])} to={`/@${status.getIn(['account', 'acct'])}`} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></Link> }} /> <FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name }} />
</div> </div>
); );
@ -570,13 +578,11 @@ class Status extends ImmutablePureComponent {
<RelativeTimestamp timestamp={status.get('created_at')} />{status.get('edited_at') && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get('edited_at'), { year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) })}> *</abbr>} <RelativeTimestamp timestamp={status.get('created_at')} />{status.get('edited_at') && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get('edited_at'), { year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) })}> *</abbr>}
</Link> </Link>
<Link to={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} data-hover-card-account={status.getIn(['account', 'id'])} className='status__display-name'> <LinkedDisplayName displayProps={{account: status.get('account')}} className='status__display-name'>
<div className='status__avatar'> <div className='status__avatar'>
{statusAvatar} {statusAvatar}
</div> </div>
</LinkedDisplayName>
<DisplayName account={status.get('account')} />
</Link>
{isQuotedPost && !!this.props.onQuoteCancel && ( {isQuotedPost && !!this.props.onQuoteCancel && (
<IconButton <IconButton

View File

@ -2,9 +2,10 @@ import { FormattedMessage } from 'react-intl';
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react'; import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
import { DisplayedName } from 'mastodon/features/notifications_v2/components/displayed_name';
import { useAppSelector } from 'mastodon/store'; import { useAppSelector } from 'mastodon/store';
import { LinkedDisplayName } from './display_name';
export const StatusThreadLabel: React.FC<{ export const StatusThreadLabel: React.FC<{
accountId: string; accountId: string;
inReplyToAccountId: string; inReplyToAccountId: string;
@ -27,7 +28,13 @@ export const StatusThreadLabel: React.FC<{
<FormattedMessage <FormattedMessage
id='status.replied_to' id='status.replied_to'
defaultMessage='Replied to {name}' defaultMessage='Replied to {name}'
values={{ name: <DisplayedName accountIds={[inReplyToAccountId]} /> }} values={{
name: (
<LinkedDisplayName
displayProps={{ account: inReplyToAccount, variant: 'simple' }}
/>
),
}}
/> />
); );
} else { } else {

View File

@ -6,6 +6,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import CheckIcon from '@/material-icons/400-24px/check.svg?react'; import CheckIcon from '@/material-icons/400-24px/check.svg?react';
import CloseIcon from '@/material-icons/400-24px/close.svg?react'; import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
import { DisplayName } from '@/mastodon/components/display_name';
export default class FollowRequestNote extends ImmutablePureComponent { export default class FollowRequestNote extends ImmutablePureComponent {
@ -19,7 +20,7 @@ export default class FollowRequestNote extends ImmutablePureComponent {
return ( return (
<div className='follow-request-banner'> <div className='follow-request-banner'>
<div className='follow-request-banner__message'> <div className='follow-request-banner__message'>
<FormattedMessage id='account.requested_follow' defaultMessage='{name} has requested to follow you' values={{ name: <bdi><strong dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi> }} /> <FormattedMessage id='account.requested_follow' defaultMessage='{name} has requested to follow you' values={{ name: <DisplayName account={account} variant='simple' /> }} />
</div> </div>
<div className='follow-request-banner__action'> <div className='follow-request-banner__action'>

View File

@ -7,6 +7,7 @@ import { Helmet } from 'react-helmet';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import { AccountBio } from '@/mastodon/components/account_bio'; import { AccountBio } from '@/mastodon/components/account_bio';
import { DisplayName } from '@/mastodon/components/display_name';
import CheckIcon from '@/material-icons/400-24px/check.svg?react'; import CheckIcon from '@/material-icons/400-24px/check.svg?react';
import LockIcon from '@/material-icons/400-24px/lock.svg?react'; import LockIcon from '@/material-icons/400-24px/lock.svg?react';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
@ -774,7 +775,6 @@ export const AccountHeader: React.FC<{
); );
} }
const displayNameHtml = { __html: account.display_name_html };
const fields = account.fields; const fields = account.fields;
const isLocal = !account.acct.includes('@'); const isLocal = !account.acct.includes('@');
const username = account.acct.split('@')[0]; const username = account.acct.split('@')[0];
@ -863,7 +863,7 @@ export const AccountHeader: React.FC<{
<div className='account__header__tabs__name'> <div className='account__header__tabs__name'>
<h1> <h1>
<span dangerouslySetInnerHTML={displayNameHtml} /> <DisplayName account={account} variant='simple' />
<small> <small>
<span> <span>
@{username} @{username}

View File

@ -1,33 +1,26 @@
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import { Avatar } from '@/mastodon/components/avatar'; import { Avatar } from '@/mastodon/components/avatar';
import { AvatarGroup } from '@/mastodon/components/avatar_group'; import { AvatarGroup } from '@/mastodon/components/avatar_group';
import { LinkedDisplayName } from '@/mastodon/components/display_name';
import type { Account } from '@/mastodon/models/account'; import type { Account } from '@/mastodon/models/account';
import { useFetchFamiliarFollowers } from '../hooks/familiar_followers'; import { useFetchFamiliarFollowers } from '../hooks/familiar_followers';
const AccountLink: React.FC<{ account?: Account }> = ({ account }) => {
if (!account) {
return null;
}
return (
<Link
to={`/@${account.acct}`}
data-hover-card-account={account.id}
dangerouslySetInnerHTML={{ __html: account.display_name_html }}
/>
);
};
const FamiliarFollowersReadout: React.FC<{ familiarFollowers: Account[] }> = ({ const FamiliarFollowersReadout: React.FC<{ familiarFollowers: Account[] }> = ({
familiarFollowers, familiarFollowers,
}) => { }) => {
const messageData = { const messageData = {
name1: <AccountLink account={familiarFollowers.at(0)} />, name1: (
name2: <AccountLink account={familiarFollowers.at(1)} />, <LinkedDisplayName
displayProps={{ account: familiarFollowers.at(0), variant: 'simple' }}
/>
),
name2: (
<LinkedDisplayName
displayProps={{ account: familiarFollowers.at(1), variant: 'simple' }}
/>
),
othersCount: familiarFollowers.length - 2, othersCount: familiarFollowers.length - 2,
}; };

View File

@ -2,8 +2,8 @@ import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { DisplayName } from '@/mastodon/components/display_name';
import { AvatarOverlay } from 'mastodon/components/avatar_overlay'; import { AvatarOverlay } from 'mastodon/components/avatar_overlay';
import { DisplayName } from 'mastodon/components/display_name';
import { useAppSelector } from 'mastodon/store'; import { useAppSelector } from 'mastodon/store';
export const MovedNote: React.FC<{ export const MovedNote: React.FC<{
@ -20,15 +20,7 @@ export const MovedNote: React.FC<{
id='account.moved_to' id='account.moved_to'
defaultMessage='{name} has indicated that their new account is now:' defaultMessage='{name} has indicated that their new account is now:'
values={{ values={{
name: ( name: <DisplayName account={from} variant='simple' />,
<bdi>
<strong
dangerouslySetInnerHTML={{
__html: from?.display_name_html ?? '',
}}
/>
</bdi>
),
}} }}
/> />
</div> </div>

View File

@ -6,6 +6,7 @@ import { useCallback } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { DisplayName } from '@/mastodon/components/display_name';
import { toggleStatusSpoilers } from 'mastodon/actions/statuses'; import { toggleStatusSpoilers } from 'mastodon/actions/statuses';
import { DetailedStatus } from 'mastodon/features/status/components/detailed_status'; import { DetailedStatus } from 'mastodon/features/status/components/detailed_status';
import { me } from 'mastodon/initial_state'; import { me } from 'mastodon/initial_state';
@ -79,11 +80,7 @@ export const HighlightedPost: React.FC<{
id='annual_report.summary.highlighted_post.possessive' id='annual_report.summary.highlighted_post.possessive'
defaultMessage="{name}'s" defaultMessage="{name}'s"
values={{ values={{
name: account && ( name: <DisplayName account={account} variant='simple' />,
<bdi
dangerouslySetInnerHTML={{ __html: account.display_name_html }}
/>
),
}} }}
/> />
</strong> </strong>

View File

@ -25,6 +25,7 @@ import StatusContent from 'mastodon/components/status_content';
import { Dropdown } from 'mastodon/components/dropdown_menu'; import { Dropdown } from 'mastodon/components/dropdown_menu';
import { autoPlayGif } from 'mastodon/initial_state'; import { autoPlayGif } from 'mastodon/initial_state';
import { makeGetStatus } from 'mastodon/selectors'; import { makeGetStatus } from 'mastodon/selectors';
import { LinkedDisplayName } from '@/mastodon/components/display_name';
const messages = defineMessages({ const messages = defineMessages({
more: { id: 'status.more', defaultMessage: 'More' }, more: { id: 'status.more', defaultMessage: 'More' },
@ -139,15 +140,8 @@ export const Conversation = ({ conversation, scrollKey }) => {
menu.push({ text: intl.formatMessage(messages.delete), action: handleDelete }); menu.push({ text: intl.formatMessage(messages.delete), action: handleDelete });
const names = accounts.map(a => ( const names = accounts.map((account) => (
<Link to={`/@${a.get('acct')}`} key={a.get('id')} data-hover-card-account={a.get('id')}> <LinkedDisplayName displayProps={{account, variant: 'simple'}} key={account.get('id')} />
<bdi>
<strong
className='display-name__html'
dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }}
/>
</bdi>
</Link>
)).reduce((prev, cur) => [prev, ', ', cur]); )).reduce((prev, cur) => [prev, ', ', cur]);
const handlers = { const handlers = {

View File

@ -1,9 +1,8 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { Avatar } from 'mastodon/components/avatar'; import { Avatar } from 'mastodon/components/avatar';
import { useAppSelector } from 'mastodon/store'; import { useAppSelector } from 'mastodon/store';
import { LinkedDisplayName } from '@/mastodon/components/display_name';
export const AuthorLink = ({ accountId }) => { export const AuthorLink = ({ accountId }) => {
const account = useAppSelector(state => state.getIn(['accounts', accountId])); const account = useAppSelector(state => state.getIn(['accounts', accountId]));
@ -13,10 +12,9 @@ export const AuthorLink = ({ accountId }) => {
} }
return ( return (
<Link to={`/@${account.get('acct')}`} className='story__details__shared__author-link' data-hover-card-account={accountId}> <LinkedDisplayName displayProps={{account}} className='story__details__shared__author-link'>
<Avatar account={account} size={16} /> <Avatar account={account} size={16} />
<bdi dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /> </LinkedDisplayName>
</Link>
); );
}; };

View File

@ -7,6 +7,7 @@ import classNames from 'classnames';
import { escapeRegExp } from 'lodash'; import { escapeRegExp } from 'lodash';
import { useDebouncedCallback } from 'use-debounce'; import { useDebouncedCallback } from 'use-debounce';
import { DisplayName } from '@/mastodon/components/display_name';
import { openModal, closeModal } from 'mastodon/actions/modal'; import { openModal, closeModal } from 'mastodon/actions/modal';
import { apiRequest } from 'mastodon/api'; import { apiRequest } from 'mastodon/api';
import { Button } from 'mastodon/components/button'; import { Button } from 'mastodon/components/button';
@ -404,15 +405,13 @@ const InteractionModal: React.FC<{
url: string; url: string;
}> = ({ accountId, url }) => { }> = ({ accountId, url }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const displayNameHtml = useAppSelector(
(state) => state.accounts.get(accountId)?.display_name_html ?? '',
);
const signupUrl = useAppSelector( const signupUrl = useAppSelector(
(state) => (state) =>
(state.server.getIn(['server', 'registrations', 'url'], null) || (state.server.getIn(['server', 'registrations', 'url'], null) ||
'/auth/sign_up') as string, '/auth/sign_up') as string,
); );
const name = <bdi dangerouslySetInnerHTML={{ __html: displayNameHtml }} />; const account = useAppSelector((state) => state.accounts.get(accountId));
const name = <DisplayName account={account} variant='simple' />;
const handleSignupClick = useCallback(() => { const handleSignupClick = useCallback(() => {
dispatch( dispatch(

View File

@ -18,6 +18,7 @@ import PersonAddIcon from '@/material-icons/400-24px/person_add-fill.svg?react';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react'; import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react'; import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
import { Account } from 'mastodon/components/account'; import { Account } from 'mastodon/components/account';
import { LinkedDisplayName } from '@/mastodon/components/display_name';
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
import { Hotkeys } from 'mastodon/components/hotkeys'; import { Hotkeys } from 'mastodon/components/hotkeys';
import { StatusQuoteManager } from 'mastodon/components/status_quoted'; import { StatusQuoteManager } from 'mastodon/components/status_quoted';
@ -485,8 +486,10 @@ class Notification extends ImmutablePureComponent {
} }
const targetAccount = report.get('target_account'); const targetAccount = report.get('target_account');
const targetDisplayNameHtml = { __html: targetAccount.get('display_name_html') }; const targetLink = <LinkedDisplayName
const targetLink = <bdi><Link className='notification__display-name' title={targetAccount.get('acct')} data-hover-card-account={targetAccount.get('id')} to={`/@${targetAccount.get('acct')}`} dangerouslySetInnerHTML={targetDisplayNameHtml} /></bdi>; className='notification__display-name'
displayProps={{account:targetAccount, variant: 'simple'}}
/>;
return ( return (
<Hotkeys handlers={this.getHandlers()}> <Hotkeys handlers={this.getHandlers()}>
@ -508,8 +511,7 @@ class Notification extends ImmutablePureComponent {
render () { render () {
const { notification } = this.props; const { notification } = this.props;
const account = notification.get('account'); const account = notification.get('account');
const displayNameHtml = { __html: account.get('display_name_html') }; const link = <LinkedDisplayName className='notification__display-name' displayProps={{account, variant: 'simple'}} />;
const link = <bdi><Link className='notification__display-name' href={`/@${account.get('acct')}`} title={account.get('acct')} data-hover-card-account={account.get('id')} to={`/@${account.get('acct')}`} dangerouslySetInnerHTML={displayNameHtml} /></bdi>;
switch(notification.get('type')) { switch(notification.get('type')) {
case 'follow': case 'follow':

View File

@ -16,6 +16,7 @@ import { acceptNotificationRequest, dismissNotificationRequest } from 'mastodon/
import { initReport } from 'mastodon/actions/reports'; import { initReport } from 'mastodon/actions/reports';
import { Avatar } from 'mastodon/components/avatar'; import { Avatar } from 'mastodon/components/avatar';
import { CheckBox } from 'mastodon/components/check_box'; import { CheckBox } from 'mastodon/components/check_box';
import { DisplayName } from '@/mastodon/components/display_name';
import { IconButton } from 'mastodon/components/icon_button'; import { IconButton } from 'mastodon/components/icon_button';
import { Dropdown } from 'mastodon/components/dropdown_menu'; import { Dropdown } from 'mastodon/components/dropdown_menu';
import { makeGetAccount } from 'mastodon/selectors'; import { makeGetAccount } from 'mastodon/selectors';
@ -96,7 +97,7 @@ export const NotificationRequest = ({ id, accountId, notificationsCount, checked
<div className='notification-request__name'> <div className='notification-request__name'>
<div className='notification-request__name__display-name'> <div className='notification-request__name__display-name'>
<bdi><strong dangerouslySetInnerHTML={{ __html: account?.get('display_name_html') }} /></bdi> <DisplayName account={account} variant='simple' />
</div> </div>
<span>@{account?.get('acct')}</span> <span>@{account?.get('acct')}</span>

View File

@ -1,22 +0,0 @@
import { Link } from 'react-router-dom';
import { useAppSelector } from 'mastodon/store';
export const DisplayedName: React.FC<{
accountIds: string[];
}> = ({ accountIds }) => {
const lastAccountId = accountIds[0] ?? '0';
const account = useAppSelector((state) => state.accounts.get(lastAccountId));
if (!account) return null;
return (
<Link
to={`/@${account.acct}`}
title={`@${account.acct}`}
data-hover-card-account={account.id}
>
<bdi dangerouslySetInnerHTML={{ __html: account.display_name_html }} />
</Link>
);
};

View File

@ -2,6 +2,7 @@ import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import { DisplayName } from '@/mastodon/components/display_name';
import FlagIcon from '@/material-icons/400-24px/flag-fill.svg?react'; import FlagIcon from '@/material-icons/400-24px/flag-fill.svg?react';
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp'; import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
@ -42,11 +43,9 @@ export const NotificationAdminReport: React.FC<{
if (!account || !targetAccount) return null; if (!account || !targetAccount) return null;
const domain = account.acct.split('@')[1];
const values = { const values = {
name: <bdi>{domain ?? `@${account.acct}`}</bdi>, name: <DisplayName account={account} variant='simple' />,
target: <bdi>@{targetAccount.acct}</bdi>, target: <DisplayName account={targetAccount} variant='simple' />,
category: intl.formatMessage(messages[report.category]), category: intl.formatMessage(messages[report.category]),
count: report.status_ids.length, count: report.status_ids.length,
}; };

View File

@ -3,6 +3,7 @@ import type { JSX } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { LinkedDisplayName } from '@/mastodon/components/display_name';
import { replyComposeById } from 'mastodon/actions/compose'; import { replyComposeById } from 'mastodon/actions/compose';
import { navigateToStatus } from 'mastodon/actions/statuses'; import { navigateToStatus } from 'mastodon/actions/statuses';
import { Avatar } from 'mastodon/components/avatar'; import { Avatar } from 'mastodon/components/avatar';
@ -14,7 +15,6 @@ import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
import { NOTIFICATIONS_GROUP_MAX_AVATARS } from 'mastodon/models/notification_group'; import { NOTIFICATIONS_GROUP_MAX_AVATARS } from 'mastodon/models/notification_group';
import { useAppSelector, useAppDispatch } from 'mastodon/store'; import { useAppSelector, useAppDispatch } from 'mastodon/store';
import { DisplayedName } from './displayed_name';
import { EmbeddedStatus } from './embedded_status'; import { EmbeddedStatus } from './embedded_status';
const AVATAR_SIZE = 28; const AVATAR_SIZE = 28;
@ -61,15 +61,18 @@ export const NotificationGroupWithStatus: React.FC<{
additionalContent, additionalContent,
}) => { }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const account = useAppSelector((state) =>
state.accounts.get(accountIds.at(0) ?? ''),
);
const label = useMemo( const label = useMemo(
() => () =>
labelRenderer( labelRenderer(
<DisplayedName accountIds={accountIds} />, <LinkedDisplayName displayProps={{ account, variant: 'simple' }} />,
count, count,
labelSeeMoreHref, labelSeeMoreHref,
), ),
[labelRenderer, accountIds, count, labelSeeMoreHref], [labelRenderer, account, count, labelSeeMoreHref],
); );
const isPrivateMention = useAppSelector( const isPrivateMention = useAppSelector(

View File

@ -2,6 +2,7 @@ import { useMemo } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { LinkedDisplayName } from '@/mastodon/components/display_name';
import { replyComposeById } from 'mastodon/actions/compose'; import { replyComposeById } from 'mastodon/actions/compose';
import { toggleReblog, toggleFavourite } from 'mastodon/actions/interactions'; import { toggleReblog, toggleFavourite } from 'mastodon/actions/interactions';
import { import {
@ -15,7 +16,6 @@ import { StatusQuoteManager } from 'mastodon/components/status_quoted';
import { getStatusHidden } from 'mastodon/selectors/filters'; import { getStatusHidden } from 'mastodon/selectors/filters';
import { useAppSelector, useAppDispatch } from 'mastodon/store'; import { useAppSelector, useAppDispatch } from 'mastodon/store';
import { DisplayedName } from './displayed_name';
import type { LabelRenderer } from './notification_group_with_status'; import type { LabelRenderer } from './notification_group_with_status';
export const NotificationWithStatus: React.FC<{ export const NotificationWithStatus: React.FC<{
@ -39,9 +39,16 @@ export const NotificationWithStatus: React.FC<{
}) => { }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const account = useAppSelector((state) =>
state.accounts.get(accountIds.at(0) ?? ''),
);
const label = useMemo( const label = useMemo(
() => labelRenderer(<DisplayedName accountIds={accountIds} />, count), () =>
[labelRenderer, accountIds, count], labelRenderer(
<LinkedDisplayName displayProps={{ account, variant: 'simple' }} />,
count,
),
[labelRenderer, account, count],
); );
const isPrivateMention = useAppSelector( const isPrivateMention = useAppSelector(