diff --git a/app/javascript/mastodon/components/follow_button.tsx b/app/javascript/mastodon/components/follow_button.tsx
index 15a9046848..97aaecd1aa 100644
--- a/app/javascript/mastodon/components/follow_button.tsx
+++ b/app/javascript/mastodon/components/follow_button.tsx
@@ -8,7 +8,6 @@ import { useIdentity } from '@/mastodon/identity_context';
import {
fetchRelationships,
followAccount,
- unblockAccount,
unmuteAccount,
} from 'mastodon/actions/accounts';
import { openModal } from 'mastodon/actions/modal';
@@ -59,7 +58,8 @@ export const FollowButton: React.FC<{
accountId?: string;
compact?: boolean;
labelLength?: 'auto' | 'short' | 'long';
-}> = ({ accountId, compact, labelLength = 'auto' }) => {
+ className?: string;
+}> = ({ accountId, compact, labelLength = 'auto', className }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const { signedIn } = useIdentity();
@@ -96,12 +96,24 @@ export const FollowButton: React.FC<{
return;
} else if (relationship.muting) {
dispatch(unmuteAccount(accountId));
- } else if (account && (relationship.following || relationship.requested)) {
+ } else if (account && relationship.following) {
dispatch(
openModal({ modalType: 'CONFIRM_UNFOLLOW', modalProps: { account } }),
);
+ } else if (account && relationship.requested) {
+ dispatch(
+ openModal({
+ modalType: 'CONFIRM_WITHDRAW_REQUEST',
+ modalProps: { account },
+ }),
+ );
} else if (relationship.blocking) {
- dispatch(unblockAccount(accountId));
+ dispatch(
+ openModal({
+ modalType: 'CONFIRM_UNBLOCK',
+ modalProps: { account },
+ }),
+ );
} else {
dispatch(followAccount(accountId));
}
@@ -144,7 +156,7 @@ export const FollowButton: React.FC<{
href='/settings/profile'
target='_blank'
rel='noopener'
- className={classNames('button button-secondary', {
+ className={classNames(className, 'button button-secondary', {
'button--compact': compact,
})}
>
@@ -158,13 +170,12 @@ export const FollowButton: React.FC<{
onClick={handleClick}
disabled={
relationship?.blocked_by ||
- relationship?.blocking ||
(!(relationship?.following || relationship?.requested) &&
(account?.suspended || !!account?.moved))
}
secondary={following}
compact={compact}
- className={following ? 'button--destructive' : undefined}
+ className={classNames(className, { 'button--destructive': following })}
>
{label}
diff --git a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx
index 2be026c8f9..776157ccf5 100644
--- a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx
+++ b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx
@@ -34,7 +34,6 @@ import { initMuteModal } from 'mastodon/actions/mutes';
import { initReport } from 'mastodon/actions/reports';
import { Avatar } from 'mastodon/components/avatar';
import { Badge, AutomatedBadge, GroupBadge } from 'mastodon/components/badge';
-import { Button } from 'mastodon/components/button';
import { CopyIconButton } from 'mastodon/components/copy_icon_button';
import {
FollowersCounter,
@@ -384,7 +383,7 @@ export const AccountHeader: React.FC<{
const isRemote = account?.acct !== account?.username;
const remoteDomain = isRemote ? account?.acct.split('@')[1] : null;
- const menu = useMemo(() => {
+ const menuItems = useMemo(() => {
const arr: MenuItem[] = [];
if (!account) {
@@ -606,6 +605,15 @@ export const AccountHeader: React.FC<{
handleUnblockDomain,
]);
+ const menu = accountId !== me && (
+
+ );
+
if (!account) {
return null;
}
@@ -719,21 +727,16 @@ export const AccountHeader: React.FC<{
);
}
- if (relationship?.blocking) {
+ const isMovedAndUnfollowedAccount = account.moved && !relationship?.following;
+
+ if (!isMovedAndUnfollowedAccount) {
actionBtn = (
-
);
- } else {
- actionBtn = ;
- }
-
- if (account.moved && !relationship?.following) {
- actionBtn = '';
}
if (account.locked) {
@@ -815,18 +818,11 @@ export const AccountHeader: React.FC<{
/>
-
+
+ {!hidden && actionBtn}
{!hidden && bellBtn}
{!hidden && shareBtn}
- {accountId !== me && (
-
- )}
- {!hidden && actionBtn}
+ {menu}
@@ -856,6 +852,12 @@ export const AccountHeader: React.FC<{
)}
+
+ {!hidden && actionBtn}
+ {!hidden && bellBtn}
+ {menu}
+
+
{!(suspended || hidden) && (
{title}
-
{message}
+ {message &&
{message}
}
diff --git a/app/javascript/mastodon/features/ui/components/confirmation_modals/index.ts b/app/javascript/mastodon/features/ui/components/confirmation_modals/index.ts
index 63c93b54b6..9aff30eeac 100644
--- a/app/javascript/mastodon/features/ui/components/confirmation_modals/index.ts
+++ b/app/javascript/mastodon/features/ui/components/confirmation_modals/index.ts
@@ -5,7 +5,9 @@ export {
ConfirmReplyModal,
ConfirmEditStatusModal,
} from './discard_draft_confirmation';
+export { ConfirmWithdrawRequestModal } from './withdraw_follow_request';
export { ConfirmUnfollowModal } from './unfollow';
+export { ConfirmUnblockModal } from './unblock';
export { ConfirmClearNotificationsModal } from './clear_notifications';
export { ConfirmLogOutModal } from './log_out';
export { ConfirmFollowToListModal } from './follow_to_list';
diff --git a/app/javascript/mastodon/features/ui/components/confirmation_modals/unblock.tsx b/app/javascript/mastodon/features/ui/components/confirmation_modals/unblock.tsx
new file mode 100644
index 0000000000..e2154ef48d
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/components/confirmation_modals/unblock.tsx
@@ -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 (
+
+ }
+ confirm={intl.formatMessage(messages.unblockConfirm)}
+ onConfirm={onConfirm}
+ onClose={onClose}
+ />
+ );
+};
diff --git a/app/javascript/mastodon/features/ui/components/confirmation_modals/unfollow.tsx b/app/javascript/mastodon/features/ui/components/confirmation_modals/unfollow.tsx
index 58e39da07b..45b8d458b1 100644
--- a/app/javascript/mastodon/features/ui/components/confirmation_modals/unfollow.tsx
+++ b/app/javascript/mastodon/features/ui/components/confirmation_modals/unfollow.tsx
@@ -10,10 +10,6 @@ import type { BaseConfirmationModalProps } from './confirmation_modal';
import { ConfirmationModal } from './confirmation_modal';
const messages = defineMessages({
- unfollowTitle: {
- id: 'confirmations.unfollow.title',
- defaultMessage: 'Unfollow user?',
- },
unfollowConfirm: {
id: 'confirmations.unfollow.confirm',
defaultMessage: 'Unfollow',
@@ -34,12 +30,11 @@ export const ConfirmUnfollowModal: React.FC<
return (
@{account.acct} }}
+ id='confirmations.unfollow.title'
+ defaultMessage='Unfollow {name}?'
+ values={{ name: `@${account.acct}` }}
/>
}
confirm={intl.formatMessage(messages.unfollowConfirm)}
diff --git a/app/javascript/mastodon/features/ui/components/confirmation_modals/withdraw_follow_request.tsx b/app/javascript/mastodon/features/ui/components/confirmation_modals/withdraw_follow_request.tsx
new file mode 100644
index 0000000000..a0bd236637
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/components/confirmation_modals/withdraw_follow_request.tsx
@@ -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 (
+
+ }
+ confirm={intl.formatMessage(messages.withdrawConfirm)}
+ onConfirm={onConfirm}
+ onClose={onClose}
+ />
+ );
+};
diff --git a/app/javascript/mastodon/features/ui/components/modal_root.jsx b/app/javascript/mastodon/features/ui/components/modal_root.jsx
index caaa4e590b..944feb325e 100644
--- a/app/javascript/mastodon/features/ui/components/modal_root.jsx
+++ b/app/javascript/mastodon/features/ui/components/modal_root.jsx
@@ -32,7 +32,9 @@ import {
ConfirmDeleteListModal,
ConfirmReplyModal,
ConfirmEditStatusModal,
+ ConfirmUnblockModal,
ConfirmUnfollowModal,
+ ConfirmWithdrawRequestModal,
ConfirmClearNotificationsModal,
ConfirmLogOutModal,
ConfirmFollowToListModal,
@@ -57,7 +59,9 @@ export const MODAL_COMPONENTS = {
'CONFIRM_DELETE_LIST': () => Promise.resolve({ default: ConfirmDeleteListModal }),
'CONFIRM_REPLY': () => Promise.resolve({ default: ConfirmReplyModal }),
'CONFIRM_EDIT_STATUS': () => Promise.resolve({ default: ConfirmEditStatusModal }),
+ 'CONFIRM_UNBLOCK': () => Promise.resolve({ default: ConfirmUnblockModal }),
'CONFIRM_UNFOLLOW': () => Promise.resolve({ default: ConfirmUnfollowModal }),
+ 'CONFIRM_WITHDRAW_REQUEST': () => Promise.resolve({ default: ConfirmWithdrawRequestModal }),
'CONFIRM_CLEAR_NOTIFICATIONS': () => Promise.resolve({ default: ConfirmClearNotificationsModal }),
'CONFIRM_LOG_OUT': () => Promise.resolve({ default: ConfirmLogOutModal }),
'CONFIRM_FOLLOW_TO_LIST': () => Promise.resolve({ default: ConfirmFollowToListModal }),
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 9dc405ab14..45b7c03462 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -257,9 +257,12 @@
"confirmations.revoke_quote.confirm": "Remove post",
"confirmations.revoke_quote.message": "This action cannot be undone.",
"confirmations.revoke_quote.title": "Remove post?",
+ "confirmations.unblock.confirm": "Unblock",
+ "confirmations.unblock.title": "Unblock {name}?",
"confirmations.unfollow.confirm": "Unfollow",
- "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
- "confirmations.unfollow.title": "Unfollow user?",
+ "confirmations.unfollow.title": "Unfollow {name}?",
+ "confirmations.withdraw_request.confirm": "Withdraw request",
+ "confirmations.withdraw_request.title": "Withdraw request to follow {name}?",
"content_warning.hide": "Hide post",
"content_warning.show": "Show anyway",
"content_warning.show_more": "Show more",
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 614b268ec7..0881ace5ad 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -6402,7 +6402,10 @@ a.status-card {
line-height: 24px;
color: $primary-text-color;
font-weight: 500;
- margin-bottom: 8px;
+
+ &:not(:only-child) {
+ margin-bottom: 8px;
+ }
}
strong {
@@ -8407,47 +8410,6 @@ noscript {
overflow: hidden;
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 {
margin-top: 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 {
.account__header__content {
color: $primary-text-color;