From 3b43a085d076044abbbb69d928d704d3c9213b1c Mon Sep 17 00:00:00 2001 From: Echo Date: Mon, 21 Sep 2026 19:21:44 +0000 Subject: [PATCH] Status redesign: Update menu (#40636) --- app/javascript/mastodon/actions/accounts.js | 2 +- .../components/account_header/buttons.tsx | 1 - .../components/account_header/menu.tsx | 1 - .../mastodon/components/follow_button.tsx | 1 - .../mastodon/components/status/action_bar.tsx | 444 +-------------- .../mastodon/components/status/hooks.ts | 519 +++++++++++++++++- .../mastodon/components/status/meta.tsx | 115 ++-- .../mastodon/components/status/status.tsx | 2 +- .../components/status/styles.module.scss | 13 +- .../mastodon/features/status/redesign.tsx | 97 +++- app/javascript/mastodon/locales/en.json | 4 + 11 files changed, 678 insertions(+), 521 deletions(-) diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js index abedbc18251..232c55af6f4 100644 --- a/app/javascript/mastodon/actions/accounts.js +++ b/app/javascript/mastodon/actions/accounts.js @@ -150,7 +150,7 @@ export function fetchAccountFail(id, error) { * @param {boolean} [options.reblogs] * @param {boolean} [options.notify] * @param {string} [options.ref] - * @returns {function(): void} + * @returns {import('@reduxjs/toolkit').ThunkAction} */ export function followAccount(id, options = { reblogs: true }) { return (dispatch, getState) => { diff --git a/app/javascript/mastodon/components/account_header/buttons.tsx b/app/javascript/mastodon/components/account_header/buttons.tsx index 2f33a88b6b9..ab924cf6d59 100644 --- a/app/javascript/mastodon/components/account_header/buttons.tsx +++ b/app/javascript/mastodon/components/account_header/buttons.tsx @@ -71,7 +71,6 @@ const AccountButtonsOther: FC< const dispatch = useAppDispatch(); const handleNotifyToggle = useCallback(() => { if (account) { - // @ts-expect-error this action is not typed yet dispatch(followAccount(account.id, { notify: !relationship?.notifying })); } }, [dispatch, account, relationship]); diff --git a/app/javascript/mastodon/components/account_header/menu.tsx b/app/javascript/mastodon/components/account_header/menu.tsx index f0097bbb268..bd76fcd7e8d 100644 --- a/app/javascript/mastodon/components/account_header/menu.tsx +++ b/app/javascript/mastodon/components/account_header/menu.tsx @@ -372,7 +372,6 @@ function getMenuItems({ ), action: () => { dispatch( - // @ts-expect-error this action is not typed yet followAccount(account.id, { reblogs: !relationship.showing_reblogs, }), diff --git a/app/javascript/mastodon/components/follow_button.tsx b/app/javascript/mastodon/components/follow_button.tsx index 60b47f4bb51..8cbacf21c32 100644 --- a/app/javascript/mastodon/components/follow_button.tsx +++ b/app/javascript/mastodon/components/follow_button.tsx @@ -141,7 +141,6 @@ export function useFollowButton({ }), ); } else { - // @ts-expect-error this action is not typed yet dispatch(followAccount(accountId, { ref: reference })); } }, [ diff --git a/app/javascript/mastodon/components/status/action_bar.tsx b/app/javascript/mastodon/components/status/action_bar.tsx index c92da829fe7..55df6f674c2 100644 --- a/app/javascript/mastodon/components/status/action_bar.tsx +++ b/app/javascript/mastodon/components/status/action_bar.tsx @@ -1,5 +1,4 @@ -import type React from 'react'; -import { useCallback, useMemo } from 'react'; +import { useCallback } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; @@ -15,44 +14,15 @@ import { ShareFatIcon, } from '@phosphor-icons/react'; -import { - muteAccount, - unblockAccount, - unmuteAccount, -} from '@/mastodon/actions/accounts'; -import { initBlockModal } from '@/mastodon/actions/blocks'; -import { directCompose, mentionCompose } from '@/mastodon/actions/compose'; -import { - initDomainBlockModal, - unblockDomain, -} from '@/mastodon/actions/domain_blocks'; -import type { StatusInteractionIntent } from '@/mastodon/actions/interactions'; import { statusInteraction } from '@/mastodon/actions/interactions'; import { fetchStatus } from '@/mastodon/actions/statuses'; import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId'; -import { useRelationship } from '@/mastodon/hooks/useRelationship'; -import { useStatus } from '@/mastodon/hooks/useStatus'; -import { useIdentity } from '@/mastodon/identity_context'; +import { useAccountStatus } from '@/mastodon/hooks/useStatus'; import { quickBoosting } from '@/mastodon/initial_state'; -import type { Account } from '@/mastodon/models/account'; import type { MenuItem as DropdownItem } from '@/mastodon/models/dropdown_menu'; -import type { Relationship } from '@/mastodon/models/relationship'; -import type { StatusShape } from '@/mastodon/models/status'; -import { - PERMISSION_MANAGE_FEDERATION, - PERMISSION_MANAGE_USERS, -} from '@/mastodon/permissions'; -import type { - StatusConditions, - StatusInteractionsAllowed, -} from '@/mastodon/selectors/statuses'; -import { - selectStatusConditions, - selectStatusInteractionsAllowed, -} from '@/mastodon/selectors/statuses'; -import type { AppDispatch } from '@/mastodon/store'; +import type { AccountStatusShape } from '@/mastodon/models/status'; +import { selectStatusConditions } from '@/mastodon/selectors/statuses'; import { useAppDispatch, useAppSelector } from '@/mastodon/store'; -import { isRedesignEnabled } from '@/mastodon/utils/environment'; import { Button, @@ -71,7 +41,7 @@ import { } from '../menu'; import { boostItemState, quoteItemState } from './boost_button_utils'; -import { useStatusContext } from './hooks'; +import { useStatusContext, useStatusMenuActions } from './hooks'; import { RemoveQuoteHint } from './legacy/action_bar/remove_quote_hint'; import classes from './styles.module.scss'; @@ -79,83 +49,26 @@ interface StatusActionBarProps { statusId: string; withDismiss?: boolean; withCounters?: boolean; - onlyInteractions?: boolean; + /** Only show methods to respond (reply, boost, like) and not sharing, bookmarking, and the overflow menu. */ + onlyResponses?: boolean; } const messages = defineMessages({ - delete: { id: 'status.delete', defaultMessage: 'Delete' }, - redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' }, - edit: { id: 'status.edit', defaultMessage: 'Edit' }, - direct: { id: 'status.direct', defaultMessage: 'Privately mention @{name}' }, - mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' }, - mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, - block: { id: 'account.block', defaultMessage: 'Block @{name}' }, - reply: { id: 'status.reply', defaultMessage: 'Reply' }, - share: { id: 'status.share', defaultMessage: 'Share' }, replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' }, favourite: { id: 'status.favourite', defaultMessage: 'Favorite' }, removeFavourite: { id: 'status.remove_favourite', defaultMessage: 'Remove from favorites', }, - open: { id: 'status.open', defaultMessage: 'Expand this status' }, - report: { id: 'status.report', defaultMessage: 'Report @{name}' }, - muteConversation: { - id: 'status.mute_conversation', - defaultMessage: 'Mute conversation', - }, - unmuteConversation: { - id: 'status.unmute_conversation', - defaultMessage: 'Unmute conversation', - }, - pin: { id: 'status.pin', defaultMessage: 'Pin on profile' }, - unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' }, - embed: { id: 'status.embed', defaultMessage: 'Get embed code' }, - admin_account: { - id: 'status.admin_account', - defaultMessage: 'Open moderation interface for @{name}', - }, - admin_status: { - id: 'status.admin_status', - defaultMessage: 'Open this post in the moderation interface', - }, - admin_domain: { - id: 'status.admin_domain', - defaultMessage: 'Open moderation interface for {domain}', - }, - copy: { id: 'status.copy', defaultMessage: 'Copy link to post' }, - blockDomain: { - id: 'account.block_domain', - defaultMessage: 'Block domain {domain}', - }, - unblockDomain: { - id: 'account.unblock_domain', - defaultMessage: 'Unblock domain {domain}', - }, - unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, - unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, - filter: { id: 'status.filter', defaultMessage: 'Filter this post' }, - openOriginalPage: { - id: 'account.open_original_page', - defaultMessage: 'Open original page', - }, - revokeQuote: { - id: 'status.revoke_quote', - defaultMessage: 'Remove my post from @{name}’s post', - }, - quotePolicyChange: { - id: 'status.quote_policy_change', - defaultMessage: 'Change who can quote', - }, }); export const StatusActionBar: React.FC = ({ statusId, withDismiss, withCounters, - onlyInteractions, + onlyResponses, }) => { - const status = useStatus(statusId); + const status = useAccountStatus(statusId); const quotedAccountId = useAppSelector( (state) => state.statuses.getIn([status?.quote?.quoted_status, 'account']) ?? null, @@ -215,7 +128,7 @@ export const StatusActionBar: React.FC = ({ const shouldShowQuoteRemovalHint = isQuotingMe && contextType === 'notifications'; - const interactionButtons = ( + const responseButtons = ( <>