Add basic UI support for invalid handles in Web UI (#40045)

This commit is contained in:
Claire
2026-08-06 16:24:28 +02:00
committed by GitHub
parent d303a23f7f
commit e21dc88ce9
11 changed files with 163 additions and 50 deletions

View File

@@ -71,6 +71,7 @@ export interface BaseApiAccountJSON {
memorial?: boolean;
hide_collections: boolean;
email_subscriptions?: boolean;
invalid_handle?: boolean;
}
// See app/serializers/rest/muted_account_serializer.rb

View File

@@ -226,6 +226,10 @@ const redesignMessages = defineMessages({
id: 'account.menu.open_original_page',
defaultMessage: 'View on {domain}',
},
openOriginalPageInvalid: {
id: 'account.menu.open_original_page_no_domain',
defaultMessage: 'View on original server',
},
removeFollower: {
id: 'account.menu.remove_follower',
defaultMessage: 'Remove follower',
@@ -270,32 +274,37 @@ function getMenuItems({
// Open on remote page.
if (isRemote) {
items.push({
text: intl.formatMessage(redesignMessages.openOriginalPage, {
domain: remoteDomain,
}),
text: account.invalid_handle
? intl.formatMessage(redesignMessages.openOriginalPageInvalid)
: intl.formatMessage(redesignMessages.openOriginalPage, {
domain: remoteDomain,
}),
href: account.url,
});
}
// Mention and direct message options
if (signedIn && !account.suspended) {
items.push(
null,
{
text: intl.formatMessage(redesignMessages.mention),
action: () => {
dispatch(mentionCompose(account));
if (account.invalid_handle) items.push(null);
else {
items.push(
null,
{
text: intl.formatMessage(redesignMessages.mention),
action: () => {
dispatch(mentionCompose(account));
},
},
},
{
text: intl.formatMessage(redesignMessages.direct),
action: () => {
dispatch(directCompose(account));
{
text: intl.formatMessage(redesignMessages.direct),
action: () => {
dispatch(directCompose(account));
},
},
},
null,
);
null,
);
}
}
if (!signedIn) {
@@ -484,7 +493,7 @@ function getMenuItems({
});
}
if (remoteDomain) {
if (remoteDomain && !account.invalid_handle) {
items.push(null, {
text: intl.formatMessage(
relationship?.domain_blocking
@@ -524,6 +533,7 @@ function getMenuItems({
}
if (
remoteDomain &&
!account.invalid_handle &&
(permissions & PERMISSION_MANAGE_FEDERATION) ===
PERMISSION_MANAGE_FEDERATION
) {

View File

@@ -62,17 +62,84 @@ export const AccountName: FC<{ accountId: string }> = ({ accountId }) => {
{relationship?.followed_by && <FollowsYouBadge />}
</div>
<AccountNameHelp
username={username}
domain={domain}
isSelf={account.id === me}
/>
{account.invalid_handle ? (
<InvalidAccountHelp />
) : (
<AccountNameHelp
username={username}
domain={domain}
isSelf={account.id === me}
/>
)}
<AccountBadges accountId={accountId} />
</div>
);
};
const InvalidAccountHelp: FC = () => {
const accessibilityId = useId();
const intl = useIntl();
const [open, setOpen] = useState(false);
const [triggerElement, setTriggerElement] =
useState<HTMLButtonElement | null>(null);
const handleClick = useCallback(() => {
setOpen((prev) => !prev);
}, []);
return (
<>
<button
type='button'
ref={setTriggerElement}
className={classNames(classes.handleHelpButton)}
onClick={handleClick}
aria-expanded={open}
aria-controls={accessibilityId}
>
<FormattedMessage
id='account.hame.invalid_handle'
defaultMessage='Handle unavailable'
/>
<Icon
id='help'
icon={HelpIcon}
aria-label={intl.formatMessage(messages.nameInfo)}
/>
</button>
<Popover
isOpen={open}
reference={triggerElement}
onClose={handleClick}
offset={5}
>
{({ props }) => (
<div
{...props}
role='region'
id={accessibilityId}
className={classNames('dropdown-animation', classes.handleHelp)}
>
<FormattedMessage
id='account.name.help.invalid_header'
defaultMessage="This user's handle is being updated"
tagName='h3'
/>
<FormattedMessage
id='account.name.help.invalid_explanation'
defaultMessage='This can happen when a user changes username, and is generally temporary. If this persists, it may be because of an unavailable server or some misconfiguration on their end.'
tagName='p'
/>
</div>
)}
</Popover>
</>
);
};
const AccountNameHelp: FC<{
username: string;
domain: string;

View File

@@ -142,7 +142,8 @@
font-weight: 600;
}
> ol {
> ol,
> p {
margin: 12px 0;
}

View File

@@ -1,26 +1,42 @@
import { useMemo } from 'react';
import type { ComponentPropsWithoutRef, FC } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { Skeleton } from '../skeleton';
import type { DisplayNameProps } from './index';
import { DisplayNameWithoutDomain } from './no-domain';
const messages = defineMessages({
invalidHandle: {
id: 'account.hame.invalid_handle',
defaultMessage: 'Handle unavailable',
},
});
export function useAccountHandle(
account: DisplayNameProps['account'],
localDomain: DisplayNameProps['localDomain'],
) {
const intl = useIntl();
return useMemo(() => {
if (!account) {
return null;
}
if (account.invalid_handle)
return intl.formatMessage(messages.invalidHandle);
let acct = account.acct;
if (!acct.includes('@') && localDomain) {
acct = `${acct}@${localDomain}`;
}
return `@${acct}`;
}, [account, localDomain]);
}, [account, localDomain, intl]);
}
export const DisplayNameDefault: FC<

View File

@@ -465,23 +465,25 @@ function getMenuItems({
return menu;
}
menu.push({
text: intl.formatMessage(messages.mention, {
name: account.username,
}),
action: () => {
dispatch(mentionCompose(account));
},
});
menu.push({
text: intl.formatMessage(messages.direct, {
name: account.username,
}),
action: () => {
dispatch(directCompose(account));
},
});
menu.push(null);
if (!account.invalid_handle) {
menu.push({
text: intl.formatMessage(messages.mention, {
name: account.username,
}),
action: () => {
dispatch(mentionCompose(account));
},
});
menu.push({
text: intl.formatMessage(messages.direct, {
name: account.username,
}),
action: () => {
dispatch(directCompose(account));
},
});
menu.push(null);
}
if (interactions.revokeQuote) {
menu.push({

View File

@@ -314,9 +314,11 @@ class StatusActionBar extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick, dangerous: true });
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick, dangerous: true });
} else {
menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.handleMentionClick });
menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.handleDirectClick });
menu.push(null);
if (!account.get('invalid_handle')) {
menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.handleMentionClick });
menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.handleDirectClick });
menu.push(null);
}
if (isQuotingMe) {
menu.push({ text: intl.formatMessage(messages.revokeQuote, { name: account.get('username') }), action: this.handleRevokeQuoteClick, dangerous: true });
@@ -342,7 +344,7 @@ class StatusActionBar extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.handleReport, dangerous: true });
if (account.get('acct') !== account.get('username')) {
if (account.get('acct') !== account.get('username') && !account.get('invalid_handle')) {
const domain = account.get('acct').split('@')[1];
menu.push(null);

View File

@@ -266,8 +266,10 @@ class ActionBar extends PureComponent {
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick, dangerous: true });
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick, dangerous: true });
} else {
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
menu.push(null);
if (!account.get('invalid_handle')) {
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
menu.push(null);
}
if (quotedAccountId === me) {
menu.push({ text: intl.formatMessage(messages.revokeQuote, { name: account.get('username') }), action: this.handleRevokeQuoteClick, dangerous: true });
@@ -287,7 +289,7 @@ class ActionBar extends PureComponent {
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport, dangerous: true });
if (account.get('acct') !== account.get('username')) {
if (account.get('acct') !== account.get('username') && !account.get('invalid_handle')) {
const domain = account.get('acct').split('@')[1];
menu.push(null);
@@ -307,6 +309,7 @@ class ActionBar extends PureComponent {
}
if (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION) {
const domain = account.get('acct').split('@')[1];
menu.push({ text: intl.formatMessage(messages.admin_domain, { domain: domain }), href: `/admin/instances/${domain}` });
}
}

View File

@@ -67,6 +67,7 @@
"account.follows.empty": "This user doesn't follow anyone yet.",
"account.follows_you": "Follows you",
"account.go_to_profile": "Go to profile",
"account.hame.invalid_handle": "Handle unavailable",
"account.hide_reblogs": "Hide boosts from @{name}",
"account.in_memoriam": "In Memoriam.",
"account.join_modal.day": "Day",
@@ -98,6 +99,7 @@
"account.menu.mute": "Mute account",
"account.menu.note.description": "Visible only to you",
"account.menu.open_original_page": "View on {domain}",
"account.menu.open_original_page_no_domain": "View on original server",
"account.menu.remove_follower": "Remove follower",
"account.menu.report": "Report account",
"account.menu.share": "Share…",
@@ -116,6 +118,8 @@
"account.name.help.domain_self": "{domain} is your server that hosts your profile and posts.",
"account.name.help.footer": "Just like you can send emails to people using different email providers, you can interact with people on other Mastodon servers, and with anyone on other Fediverse apps.",
"account.name.help.header": "A handle is like an email address",
"account.name.help.invalid_explanation": "This can happen when a user changes username, and is generally temporary. If this persists, it may be because of an unavailable server or some misconfiguration on their end.",
"account.name.help.invalid_header": "This user's handle is being updated",
"account.name.help.username": "{username} is this accounts username on their server. Someone on another server might have the same username.",
"account.name.help.username_self": "{username} is your username on this server. Someone on another server might have the same username.",
"account.name_info": "What does this mean?",

View File

@@ -112,6 +112,7 @@ export const accountDefaultValues: AccountShape = {
moved: null,
hide_collections: false,
email_subscriptions: false,
invalid_handle: false,
// This comes from `ApiMutedAccountJSON`, but we should eventually
// store that in a different object.
mute_expires_at: null,