Update confirmation dialogs for follow button actions "unfollow", "unblock", and "withdraw request" (#36289)

This commit is contained in:
diondiondion 2025-09-30 16:55:25 +02:00 committed by GitHub
parent c12b8f51c1
commit 473bd84c24
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 218 additions and 86 deletions

View File

@ -8,7 +8,6 @@ import { useIdentity } from '@/mastodon/identity_context';
import { import {
fetchRelationships, fetchRelationships,
followAccount, followAccount,
unblockAccount,
unmuteAccount, unmuteAccount,
} from 'mastodon/actions/accounts'; } from 'mastodon/actions/accounts';
import { openModal } from 'mastodon/actions/modal'; import { openModal } from 'mastodon/actions/modal';
@ -59,7 +58,8 @@ export const FollowButton: React.FC<{
accountId?: string; accountId?: string;
compact?: boolean; compact?: boolean;
labelLength?: 'auto' | 'short' | 'long'; labelLength?: 'auto' | 'short' | 'long';
}> = ({ accountId, compact, labelLength = 'auto' }) => { className?: string;
}> = ({ accountId, compact, labelLength = 'auto', className }) => {
const intl = useIntl(); const intl = useIntl();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { signedIn } = useIdentity(); const { signedIn } = useIdentity();
@ -96,12 +96,24 @@ export const FollowButton: React.FC<{
return; return;
} else if (relationship.muting) { } else if (relationship.muting) {
dispatch(unmuteAccount(accountId)); dispatch(unmuteAccount(accountId));
} else if (account && (relationship.following || relationship.requested)) { } else if (account && relationship.following) {
dispatch( dispatch(
openModal({ modalType: 'CONFIRM_UNFOLLOW', modalProps: { account } }), openModal({ modalType: 'CONFIRM_UNFOLLOW', modalProps: { account } }),
); );
} else if (account && relationship.requested) {
dispatch(
openModal({
modalType: 'CONFIRM_WITHDRAW_REQUEST',
modalProps: { account },
}),
);
} else if (relationship.blocking) { } else if (relationship.blocking) {
dispatch(unblockAccount(accountId)); dispatch(
openModal({
modalType: 'CONFIRM_UNBLOCK',
modalProps: { account },
}),
);
} else { } else {
dispatch(followAccount(accountId)); dispatch(followAccount(accountId));
} }
@ -144,7 +156,7 @@ export const FollowButton: React.FC<{
href='/settings/profile' href='/settings/profile'
target='_blank' target='_blank'
rel='noopener' rel='noopener'
className={classNames('button button-secondary', { className={classNames(className, 'button button-secondary', {
'button--compact': compact, 'button--compact': compact,
})} })}
> >
@ -158,13 +170,12 @@ export const FollowButton: React.FC<{
onClick={handleClick} onClick={handleClick}
disabled={ disabled={
relationship?.blocked_by || relationship?.blocked_by ||
relationship?.blocking ||
(!(relationship?.following || relationship?.requested) && (!(relationship?.following || relationship?.requested) &&
(account?.suspended || !!account?.moved)) (account?.suspended || !!account?.moved))
} }
secondary={following} secondary={following}
compact={compact} compact={compact}
className={following ? 'button--destructive' : undefined} className={classNames(className, { 'button--destructive': following })}
> >
{label} {label}
</Button> </Button>

View File

@ -34,7 +34,6 @@ import { initMuteModal } from 'mastodon/actions/mutes';
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 { Badge, AutomatedBadge, GroupBadge } from 'mastodon/components/badge'; import { Badge, AutomatedBadge, GroupBadge } from 'mastodon/components/badge';
import { Button } from 'mastodon/components/button';
import { CopyIconButton } from 'mastodon/components/copy_icon_button'; import { CopyIconButton } from 'mastodon/components/copy_icon_button';
import { import {
FollowersCounter, FollowersCounter,
@ -384,7 +383,7 @@ export const AccountHeader: React.FC<{
const isRemote = account?.acct !== account?.username; const isRemote = account?.acct !== account?.username;
const remoteDomain = isRemote ? account?.acct.split('@')[1] : null; const remoteDomain = isRemote ? account?.acct.split('@')[1] : null;
const menu = useMemo(() => { const menuItems = useMemo(() => {
const arr: MenuItem[] = []; const arr: MenuItem[] = [];
if (!account) { if (!account) {
@ -606,6 +605,15 @@ export const AccountHeader: React.FC<{
handleUnblockDomain, handleUnblockDomain,
]); ]);
const menu = accountId !== me && (
<Dropdown
disabled={menuItems.length === 0}
items={menuItems}
icon='ellipsis-v'
iconComponent={MoreHorizIcon}
/>
);
if (!account) { if (!account) {
return null; return null;
} }
@ -719,21 +727,16 @@ export const AccountHeader: React.FC<{
); );
} }
if (relationship?.blocking) { const isMovedAndUnfollowedAccount = account.moved && !relationship?.following;
if (!isMovedAndUnfollowedAccount) {
actionBtn = ( actionBtn = (
<Button <FollowButton
text={intl.formatMessage(messages.unblock, { accountId={accountId}
name: account.username, className='account__header__follow-button'
})} labelLength='long'
onClick={handleBlock}
/> />
); );
} else {
actionBtn = <FollowButton accountId={accountId} />;
}
if (account.moved && !relationship?.following) {
actionBtn = '';
} }
if (account.locked) { if (account.locked) {
@ -815,18 +818,11 @@ export const AccountHeader: React.FC<{
/> />
</a> </a>
<div className='account__header__tabs__buttons'> <div className='account__header__buttons account__header__buttons--desktop'>
{!hidden && actionBtn}
{!hidden && bellBtn} {!hidden && bellBtn}
{!hidden && shareBtn} {!hidden && shareBtn}
{accountId !== me && ( {menu}
<Dropdown
disabled={menu.length === 0}
items={menu}
icon='ellipsis-v'
iconComponent={MoreHorizIcon}
/>
)}
{!hidden && actionBtn}
</div> </div>
</div> </div>
@ -856,6 +852,12 @@ export const AccountHeader: React.FC<{
<FamiliarFollowers accountId={accountId} /> <FamiliarFollowers accountId={accountId} />
)} )}
<div className='account__header__buttons account__header__buttons--mobile'>
{!hidden && actionBtn}
{!hidden && bellBtn}
{menu}
</div>
{!(suspended || hidden) && ( {!(suspended || hidden) && (
<div className='account__header__extra'> <div className='account__header__extra'>
<div <div

View File

@ -11,7 +11,7 @@ export interface BaseConfirmationModalProps {
export const ConfirmationModal: React.FC< export const ConfirmationModal: React.FC<
{ {
title: React.ReactNode; title: React.ReactNode;
message: React.ReactNode; message?: React.ReactNode;
confirm: React.ReactNode; confirm: React.ReactNode;
cancel?: React.ReactNode; cancel?: React.ReactNode;
secondary?: React.ReactNode; secondary?: React.ReactNode;
@ -48,7 +48,7 @@ export const ConfirmationModal: React.FC<
<div className='safety-action-modal__top'> <div className='safety-action-modal__top'>
<div className='safety-action-modal__confirmation'> <div className='safety-action-modal__confirmation'>
<h1>{title}</h1> <h1>{title}</h1>
<p>{message}</p> {message && <p>{message}</p>}
</div> </div>
</div> </div>

View File

@ -5,7 +5,9 @@ export {
ConfirmReplyModal, ConfirmReplyModal,
ConfirmEditStatusModal, ConfirmEditStatusModal,
} from './discard_draft_confirmation'; } from './discard_draft_confirmation';
export { ConfirmWithdrawRequestModal } from './withdraw_follow_request';
export { ConfirmUnfollowModal } from './unfollow'; export { ConfirmUnfollowModal } from './unfollow';
export { ConfirmUnblockModal } from './unblock';
export { ConfirmClearNotificationsModal } from './clear_notifications'; export { ConfirmClearNotificationsModal } from './clear_notifications';
export { ConfirmLogOutModal } from './log_out'; export { ConfirmLogOutModal } from './log_out';
export { ConfirmFollowToListModal } from './follow_to_list'; export { ConfirmFollowToListModal } from './follow_to_list';

View File

@ -0,0 +1,45 @@
import { useCallback } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { unblockAccount } from 'mastodon/actions/accounts';
import type { Account } from 'mastodon/models/account';
import { useAppDispatch } from 'mastodon/store';
import type { BaseConfirmationModalProps } from './confirmation_modal';
import { ConfirmationModal } from './confirmation_modal';
const messages = defineMessages({
unblockConfirm: {
id: 'confirmations.unblock.confirm',
defaultMessage: 'Unblock',
},
});
export const ConfirmUnblockModal: React.FC<
{
account: Account;
} & BaseConfirmationModalProps
> = ({ account, onClose }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const onConfirm = useCallback(() => {
dispatch(unblockAccount(account.id));
}, [dispatch, account.id]);
return (
<ConfirmationModal
title={
<FormattedMessage
id='confirmations.unblock.title'
defaultMessage='Unblock {name}?'
values={{ name: `@${account.acct}` }}
/>
}
confirm={intl.formatMessage(messages.unblockConfirm)}
onConfirm={onConfirm}
onClose={onClose}
/>
);
};

View File

@ -10,10 +10,6 @@ import type { BaseConfirmationModalProps } from './confirmation_modal';
import { ConfirmationModal } from './confirmation_modal'; import { ConfirmationModal } from './confirmation_modal';
const messages = defineMessages({ const messages = defineMessages({
unfollowTitle: {
id: 'confirmations.unfollow.title',
defaultMessage: 'Unfollow user?',
},
unfollowConfirm: { unfollowConfirm: {
id: 'confirmations.unfollow.confirm', id: 'confirmations.unfollow.confirm',
defaultMessage: 'Unfollow', defaultMessage: 'Unfollow',
@ -34,12 +30,11 @@ export const ConfirmUnfollowModal: React.FC<
return ( return (
<ConfirmationModal <ConfirmationModal
title={intl.formatMessage(messages.unfollowTitle)} title={
message={
<FormattedMessage <FormattedMessage
id='confirmations.unfollow.message' id='confirmations.unfollow.title'
defaultMessage='Are you sure you want to unfollow {name}?' defaultMessage='Unfollow {name}?'
values={{ name: <strong>@{account.acct}</strong> }} values={{ name: `@${account.acct}` }}
/> />
} }
confirm={intl.formatMessage(messages.unfollowConfirm)} confirm={intl.formatMessage(messages.unfollowConfirm)}

View File

@ -0,0 +1,45 @@
import { useCallback } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { unfollowAccount } from 'mastodon/actions/accounts';
import type { Account } from 'mastodon/models/account';
import { useAppDispatch } from 'mastodon/store';
import type { BaseConfirmationModalProps } from './confirmation_modal';
import { ConfirmationModal } from './confirmation_modal';
const messages = defineMessages({
withdrawConfirm: {
id: 'confirmations.withdraw_request.confirm',
defaultMessage: 'Withdraw request',
},
});
export const ConfirmWithdrawRequestModal: React.FC<
{
account: Account;
} & BaseConfirmationModalProps
> = ({ account, onClose }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const onConfirm = useCallback(() => {
dispatch(unfollowAccount(account.id));
}, [dispatch, account.id]);
return (
<ConfirmationModal
title={
<FormattedMessage
id='confirmations.withdraw_request.title'
defaultMessage='Withdraw request to follow {name}?'
values={{ name: `@${account.acct}` }}
/>
}
confirm={intl.formatMessage(messages.withdrawConfirm)}
onConfirm={onConfirm}
onClose={onClose}
/>
);
};

View File

@ -32,7 +32,9 @@ import {
ConfirmDeleteListModal, ConfirmDeleteListModal,
ConfirmReplyModal, ConfirmReplyModal,
ConfirmEditStatusModal, ConfirmEditStatusModal,
ConfirmUnblockModal,
ConfirmUnfollowModal, ConfirmUnfollowModal,
ConfirmWithdrawRequestModal,
ConfirmClearNotificationsModal, ConfirmClearNotificationsModal,
ConfirmLogOutModal, ConfirmLogOutModal,
ConfirmFollowToListModal, ConfirmFollowToListModal,
@ -57,7 +59,9 @@ export const MODAL_COMPONENTS = {
'CONFIRM_DELETE_LIST': () => Promise.resolve({ default: ConfirmDeleteListModal }), 'CONFIRM_DELETE_LIST': () => Promise.resolve({ default: ConfirmDeleteListModal }),
'CONFIRM_REPLY': () => Promise.resolve({ default: ConfirmReplyModal }), 'CONFIRM_REPLY': () => Promise.resolve({ default: ConfirmReplyModal }),
'CONFIRM_EDIT_STATUS': () => Promise.resolve({ default: ConfirmEditStatusModal }), 'CONFIRM_EDIT_STATUS': () => Promise.resolve({ default: ConfirmEditStatusModal }),
'CONFIRM_UNBLOCK': () => Promise.resolve({ default: ConfirmUnblockModal }),
'CONFIRM_UNFOLLOW': () => Promise.resolve({ default: ConfirmUnfollowModal }), 'CONFIRM_UNFOLLOW': () => Promise.resolve({ default: ConfirmUnfollowModal }),
'CONFIRM_WITHDRAW_REQUEST': () => Promise.resolve({ default: ConfirmWithdrawRequestModal }),
'CONFIRM_CLEAR_NOTIFICATIONS': () => Promise.resolve({ default: ConfirmClearNotificationsModal }), 'CONFIRM_CLEAR_NOTIFICATIONS': () => Promise.resolve({ default: ConfirmClearNotificationsModal }),
'CONFIRM_LOG_OUT': () => Promise.resolve({ default: ConfirmLogOutModal }), 'CONFIRM_LOG_OUT': () => Promise.resolve({ default: ConfirmLogOutModal }),
'CONFIRM_FOLLOW_TO_LIST': () => Promise.resolve({ default: ConfirmFollowToListModal }), 'CONFIRM_FOLLOW_TO_LIST': () => Promise.resolve({ default: ConfirmFollowToListModal }),

View File

@ -257,9 +257,12 @@
"confirmations.revoke_quote.confirm": "Remove post", "confirmations.revoke_quote.confirm": "Remove post",
"confirmations.revoke_quote.message": "This action cannot be undone.", "confirmations.revoke_quote.message": "This action cannot be undone.",
"confirmations.revoke_quote.title": "Remove post?", "confirmations.revoke_quote.title": "Remove post?",
"confirmations.unblock.confirm": "Unblock",
"confirmations.unblock.title": "Unblock {name}?",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "confirmations.unfollow.title": "Unfollow {name}?",
"confirmations.unfollow.title": "Unfollow user?", "confirmations.withdraw_request.confirm": "Withdraw request",
"confirmations.withdraw_request.title": "Withdraw request to follow {name}?",
"content_warning.hide": "Hide post", "content_warning.hide": "Hide post",
"content_warning.show": "Show anyway", "content_warning.show": "Show anyway",
"content_warning.show_more": "Show more", "content_warning.show_more": "Show more",

View File

@ -6402,8 +6402,11 @@ a.status-card {
line-height: 24px; line-height: 24px;
color: $primary-text-color; color: $primary-text-color;
font-weight: 500; font-weight: 500;
&:not(:only-child) {
margin-bottom: 8px; margin-bottom: 8px;
} }
}
strong { strong {
font-weight: 700; font-weight: 700;
@ -8407,47 +8410,6 @@ noscript {
overflow: hidden; overflow: hidden;
margin-inline-start: -2px; // aligns the pfp with content below margin-inline-start: -2px; // aligns the pfp with content below
&__buttons {
display: flex;
align-items: center;
gap: 8px;
padding-top: 55px;
overflow: hidden;
.button {
flex-shrink: 1;
white-space: nowrap;
min-width: 80px;
}
.icon-button {
border: 1px solid var(--background-border-color);
border-radius: 4px;
box-sizing: content-box;
padding: 5px;
.icon {
width: 24px;
height: 24px;
}
&.copied {
border-color: $valid-value-color;
}
}
.optional {
@container account-header (max-width: 372px) {
display: none;
}
// Fallback for older browsers with no container queries support
@media screen and (max-width: (372px + 55px)) {
display: none;
}
}
}
&__name { &__name {
margin-top: 16px; margin-top: 16px;
margin-bottom: 16px; margin-bottom: 16px;
@ -8496,6 +8458,69 @@ noscript {
} }
} }
&__follow-button {
flex-grow: 1;
}
&__buttons {
display: flex;
align-items: center;
gap: 8px;
$button-breakpoint: 420px;
$button-fallback-breakpoint: #{$button-breakpoint} + 55px;
&--desktop {
margin-top: 55px;
@container (width < #{$button-breakpoint}) {
display: none;
}
@supports (not (container-type: inline-size)) {
@media (max-width: #{$button-fallback-breakpoint}) {
display: none;
}
}
}
&--mobile {
margin-block: 16px;
@container (width >= #{$button-breakpoint}) {
display: none;
}
@supports (not (container-type: inline-size)) {
@media (min-width: (#{$button-fallback-breakpoint} + 1px)) {
display: none;
}
}
}
.button {
flex-shrink: 1;
white-space: nowrap;
min-width: 80px;
}
.icon-button {
border: 1px solid var(--background-border-color);
border-radius: 4px;
box-sizing: content-box;
padding: 5px;
.icon {
width: 24px;
height: 24px;
}
&.copied {
border-color: $valid-value-color;
}
}
}
&__bio { &__bio {
.account__header__content { .account__header__content {
color: $primary-text-color; color: $primary-text-color;