From a5fbe2f5c18ce15d24cfc7060837d7ea6a6ff557 Mon Sep 17 00:00:00 2001 From: diondiondion Date: Mon, 8 Sep 2025 10:50:46 +0200 Subject: [PATCH] Fix missing icons and subtitle in mobile boost/quote menu (#36038) --- .../mastodon/components/dropdown_menu.tsx | 32 +- ...n.stories.tsx => boost_button.stories.tsx} | 6 +- .../components/status/boost_button.tsx | 253 +++++++++++ .../components/status/boost_button_utils.ts | 161 +++++++ .../components/status/reblog_button.tsx | 425 ------------------ .../components/status_action_bar/index.jsx | 4 +- .../features/status/components/action_bar.jsx | 4 +- .../features/ui/components/actions_modal.tsx | 14 +- .../mastodon/models/dropdown_menu.ts | 6 + app/javascript/mastodon/utils/types.ts | 16 + .../styles/mastodon/components.scss | 47 +- 11 files changed, 498 insertions(+), 470 deletions(-) rename app/javascript/mastodon/components/status/{reblog_button.stories.tsx => boost_button.stories.tsx} (92%) create mode 100644 app/javascript/mastodon/components/status/boost_button.tsx create mode 100644 app/javascript/mastodon/components/status/boost_button_utils.ts delete mode 100644 app/javascript/mastodon/components/status/reblog_button.tsx create mode 100644 app/javascript/mastodon/utils/types.ts diff --git a/app/javascript/mastodon/components/dropdown_menu.tsx b/app/javascript/mastodon/components/dropdown_menu.tsx index 27af0ba6c..8e765d1a6 100644 --- a/app/javascript/mastodon/components/dropdown_menu.tsx +++ b/app/javascript/mastodon/components/dropdown_menu.tsx @@ -36,6 +36,7 @@ import { import type { MenuItem } from 'mastodon/models/dropdown_menu'; import { useAppDispatch, useAppSelector } from 'mastodon/store'; +import { Icon } from './icon'; import type { IconProp } from './icon'; import { IconButton } from './icon_button'; @@ -68,6 +69,27 @@ interface DropdownMenuProps { onItemClick?: ItemClickFn; } +export const DropdownMenuItemContent: React.FC<{ item: MenuItem }> = ({ + item, +}) => { + if (item === null) { + return null; + } + + const { text, description, icon } = item; + return ( + <> + {icon && } + + {text} + {Boolean(description) && ( + {description} + )} + + + ); +}; + export const DropdownMenu = ({ items, loading, @@ -200,7 +222,7 @@ export const DropdownMenu = ({ return
  • ; } - const { text, dangerous } = option; + const { text, highlighted, disabled, dangerous } = option; let element: React.ReactElement; @@ -211,8 +233,9 @@ export const DropdownMenu = ({ onClick={handleItemClick} onKeyUp={handleItemKeyUp} data-index={i} + disabled={disabled} > - {text} + ); } else if (isExternalLinkItem(option)) { @@ -227,7 +250,7 @@ export const DropdownMenu = ({ onKeyUp={handleItemKeyUp} data-index={i} > - {text} + ); } else { @@ -239,7 +262,7 @@ export const DropdownMenu = ({ onKeyUp={handleItemKeyUp} data-index={i} > - {text} + ); } @@ -247,6 +270,7 @@ export const DropdownMenu = ({ return (
  • ( - 0} /> diff --git a/app/javascript/mastodon/components/status/boost_button.tsx b/app/javascript/mastodon/components/status/boost_button.tsx new file mode 100644 index 000000000..b34988de4 --- /dev/null +++ b/app/javascript/mastodon/components/status/boost_button.tsx @@ -0,0 +1,253 @@ +import { useCallback, useMemo } from 'react'; +import type { FC, KeyboardEvent, MouseEvent, MouseEventHandler } from 'react'; + +import { useIntl } from 'react-intl'; + +import classNames from 'classnames'; + +import { quoteComposeById } from '@/mastodon/actions/compose_typed'; +import { toggleReblog } from '@/mastodon/actions/interactions'; +import { openModal } from '@/mastodon/actions/modal'; +import type { ActionMenuItem } from '@/mastodon/models/dropdown_menu'; +import type { Status } from '@/mastodon/models/status'; +import { useAppDispatch, useAppSelector } from '@/mastodon/store'; +import { isFeatureEnabled } from '@/mastodon/utils/environment'; +import type { SomeRequired } from '@/mastodon/utils/types'; + +import type { RenderItemFn, RenderItemFnHandlers } from '../dropdown_menu'; +import { Dropdown, DropdownMenuItemContent } from '../dropdown_menu'; +import { IconButton } from '../icon_button'; + +import { + boostItemState, + messages, + quoteItemState, + selectStatusState, +} from './boost_button_utils'; + +const renderMenuItem: RenderItemFn = ( + item, + index, + handlers, + focusRefCallback, +) => ( + +); + +interface ReblogButtonProps { + status: Status; + counters?: boolean; +} + +type ActionMenuItemWithIcon = SomeRequired; + +export const StatusBoostButton: FC = ({ + status, + counters, +}) => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + const statusState = useAppSelector((state) => + selectStatusState(state, status), + ); + const { + isLoggedIn, + isReblogged, + isReblogAllowed, + isQuoteAutomaticallyAccepted, + isQuoteManuallyAccepted, + } = statusState; + + const isMenuDisabled = + !isQuoteAutomaticallyAccepted && + !isQuoteManuallyAccepted && + !isReblogAllowed; + + const statusId = status.get('id') as string; + const wasBoosted = !!status.get('reblogged'); + + const items = useMemo(() => { + const boostItem = boostItemState(statusState); + const quoteItem = quoteItemState(statusState); + return [ + { + text: intl.formatMessage(boostItem.title), + description: boostItem.meta + ? intl.formatMessage(boostItem.meta) + : undefined, + icon: boostItem.iconComponent, + highlighted: wasBoosted, + disabled: boostItem.disabled, + action: (event) => { + if (isLoggedIn) { + dispatch(toggleReblog(statusId, event.shiftKey)); + } + }, + }, + { + text: intl.formatMessage(quoteItem.title), + description: quoteItem.meta + ? intl.formatMessage(quoteItem.meta) + : undefined, + icon: quoteItem.iconComponent, + disabled: quoteItem.disabled, + action: () => { + if (isLoggedIn) { + dispatch(quoteComposeById(statusId)); + } + }, + }, + ] satisfies [ActionMenuItemWithIcon, ActionMenuItemWithIcon]; + }, [dispatch, intl, isLoggedIn, statusId, statusState, wasBoosted]); + + const boostIcon = items[0].icon; + + const handleDropdownOpen = useCallback( + (event: MouseEvent | KeyboardEvent) => { + if (!isLoggedIn) { + dispatch( + openModal({ + modalType: 'INTERACTION', + modalProps: { + type: 'reblog', + accountId: status.getIn(['account', 'id']), + url: status.get('uri'), + }, + }), + ); + } else if (event.shiftKey) { + dispatch(toggleReblog(status.get('id'), true)); + return false; + } + return true; + }, + [dispatch, isLoggedIn, status], + ); + + return ( + + + + ); +}; + +interface ReblogMenuItemProps { + item: ActionMenuItem; + index: number; + handlers: RenderItemFnHandlers; + focusRefCallback?: (c: HTMLAnchorElement | HTMLButtonElement | null) => void; +} + +const ReblogMenuItem: FC = ({ + index, + item, + handlers, + focusRefCallback, +}) => { + const { text, highlighted, disabled } = item; + + return ( +
  • + +
  • + ); +}; + +// Legacy helpers + +// Switch between the legacy and new reblog button based on feature flag. +export const BoostButton: FC = (props) => { + if (isFeatureEnabled('outgoing_quotes')) { + return ; + } + return ; +}; + +export const LegacyReblogButton: FC = ({ + status, + counters, +}) => { + const intl = useIntl(); + const statusState = useAppSelector((state) => + selectStatusState(state, status), + ); + + const { title, meta, iconComponent, disabled } = useMemo( + () => boostItemState(statusState), + [statusState], + ); + + const dispatch = useAppDispatch(); + const handleClick: MouseEventHandler = useCallback( + (event) => { + if (statusState.isLoggedIn) { + dispatch(toggleReblog(status.get('id') as string, event.shiftKey)); + } else { + dispatch( + openModal({ + modalType: 'INTERACTION', + modalProps: { + type: 'reblog', + accountId: status.getIn(['account', 'id']), + url: status.get('uri'), + }, + }), + ); + } + }, + [dispatch, status, statusState.isLoggedIn], + ); + + return ( + + ); +}; diff --git a/app/javascript/mastodon/components/status/boost_button_utils.ts b/app/javascript/mastodon/components/status/boost_button_utils.ts new file mode 100644 index 000000000..34fa26ace --- /dev/null +++ b/app/javascript/mastodon/components/status/boost_button_utils.ts @@ -0,0 +1,161 @@ +import { defineMessages } from 'react-intl'; +import type { MessageDescriptor } from 'react-intl'; + +import type { Status, StatusVisibility } from '@/mastodon/models/status'; +import { createAppSelector } from '@/mastodon/store'; +import FormatQuote from '@/material-icons/400-24px/format_quote-fill.svg?react'; +import FormatQuoteOff from '@/material-icons/400-24px/format_quote_off-fill.svg?react'; +import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react'; +import RepeatActiveIcon from '@/svg-icons/repeat_active.svg?react'; +import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg?react'; +import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg?react'; +import RepeatPrivateActiveIcon from '@/svg-icons/repeat_private_active.svg?react'; + +import type { IconProp } from '../icon'; + +export const messages = defineMessages({ + all_disabled: { + id: 'status.all_disabled', + defaultMessage: 'Boosts and quotes are disabled', + }, + quote: { id: 'status.quote', defaultMessage: 'Quote' }, + quote_cannot: { + id: 'status.cannot_quote', + defaultMessage: 'Quotes are disabled on this post', + }, + quote_followers_only: { + id: 'status.quote_followers_only', + defaultMessage: 'Only followers can quote this post', + }, + quote_manual_review: { + id: 'status.quote_manual_review', + defaultMessage: 'Author will manually review', + }, + quote_private: { + id: 'status.quote_private', + defaultMessage: 'Private posts cannot be quoted', + }, + reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, + reblog_or_quote: { + id: 'status.reblog_or_quote', + defaultMessage: 'Boost or quote', + }, + reblog_cancel: { + id: 'status.cancel_reblog_private', + defaultMessage: 'Unboost', + }, + reblog_private: { + id: 'status.reblog_private', + defaultMessage: 'Share again with your followers', + }, + reblog_cannot: { + id: 'status.cannot_reblog', + defaultMessage: 'This post cannot be boosted', + }, + request_quote: { + id: 'status.request_quote', + defaultMessage: 'Request to quote', + }, +}); + +export const selectStatusState = createAppSelector( + [ + (state) => state.meta.get('me') as string | undefined, + (_, status: Status) => status, + ], + (userId, status) => { + const isPublic = ['public', 'unlisted'].includes( + status.get('visibility') as StatusVisibility, + ); + const isMineAndPrivate = + userId === status.getIn(['account', 'id']) && + status.get('visibility') === 'private'; + return { + isLoggedIn: !!userId, + isPublic, + isMine: userId === status.getIn(['account', 'id']), + isPrivateReblog: + userId === status.getIn(['account', 'id']) && + status.get('visibility') === 'private', + isReblogged: !!status.get('reblogged'), + isReblogAllowed: isPublic || isMineAndPrivate, + isQuoteAutomaticallyAccepted: + status.getIn(['quote_approval', 'current_user']) === 'automatic' && + (isPublic || isMineAndPrivate), + isQuoteManuallyAccepted: + status.getIn(['quote_approval', 'current_user']) === 'manual' && + (isPublic || isMineAndPrivate), + isQuoteFollowersOnly: + status.getIn(['quote_approval', 'automatic', 0]) === 'followers' || + status.getIn(['quote_approval', 'manual', 0]) === 'followers', + }; + }, +); + +export type StatusState = ReturnType; + +export interface MenuItemState { + title: MessageDescriptor; + meta?: MessageDescriptor; + iconComponent: IconProp; + disabled?: boolean; +} + +export function boostItemState({ + isPublic, + isPrivateReblog, + isReblogged, +}: StatusState): MenuItemState { + if (isReblogged) { + return { + title: messages.reblog_cancel, + iconComponent: isPublic ? RepeatActiveIcon : RepeatPrivateActiveIcon, + }; + } + const iconText: MenuItemState = { + title: messages.reblog, + iconComponent: RepeatIcon, + }; + + if (isPrivateReblog) { + iconText.meta = messages.reblog_private; + iconText.iconComponent = RepeatPrivateIcon; + } else if (!isPublic) { + iconText.meta = messages.reblog_cannot; + iconText.iconComponent = RepeatDisabledIcon; + iconText.disabled = true; + } + return iconText; +} + +export function quoteItemState({ + isMine, + isQuoteAutomaticallyAccepted, + isQuoteManuallyAccepted, + isQuoteFollowersOnly, + isPublic, +}: StatusState): MenuItemState { + const iconText: MenuItemState = { + title: messages.quote, + iconComponent: FormatQuote, + }; + + if (!isPublic && !isMine) { + iconText.disabled = true; + iconText.iconComponent = FormatQuoteOff; + iconText.meta = messages.quote_private; + } else if (isQuoteAutomaticallyAccepted) { + iconText.title = messages.quote; + } else if (isQuoteManuallyAccepted) { + iconText.title = messages.request_quote; + iconText.meta = messages.quote_manual_review; + } else { + iconText.disabled = true; + iconText.iconComponent = FormatQuoteOff; + iconText.meta = isQuoteFollowersOnly + ? messages.quote_followers_only + : messages.quote_cannot; + } + + return iconText; +} diff --git a/app/javascript/mastodon/components/status/reblog_button.tsx b/app/javascript/mastodon/components/status/reblog_button.tsx deleted file mode 100644 index 079ca5d7c..000000000 --- a/app/javascript/mastodon/components/status/reblog_button.tsx +++ /dev/null @@ -1,425 +0,0 @@ -import { useCallback, useMemo } from 'react'; -import type { - FC, - KeyboardEvent, - MouseEvent, - MouseEventHandler, - SVGProps, -} from 'react'; - -import type { MessageDescriptor } from 'react-intl'; -import { defineMessages, useIntl } from 'react-intl'; - -import classNames from 'classnames'; - -import { quoteComposeById } from '@/mastodon/actions/compose_typed'; -import { toggleReblog } from '@/mastodon/actions/interactions'; -import { openModal } from '@/mastodon/actions/modal'; -import type { ActionMenuItem } from '@/mastodon/models/dropdown_menu'; -import type { Status, StatusVisibility } from '@/mastodon/models/status'; -import { - createAppSelector, - useAppDispatch, - useAppSelector, -} from '@/mastodon/store'; -import { isFeatureEnabled } from '@/mastodon/utils/environment'; -import FormatQuote from '@/material-icons/400-24px/format_quote-fill.svg?react'; -import FormatQuoteOff from '@/material-icons/400-24px/format_quote_off-fill.svg?react'; -import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react'; -import RepeatActiveIcon from '@/svg-icons/repeat_active.svg?react'; -import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg?react'; -import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg?react'; -import RepeatPrivateActiveIcon from '@/svg-icons/repeat_private_active.svg?react'; - -import type { RenderItemFn, RenderItemFnHandlers } from '../dropdown_menu'; -import { Dropdown } from '../dropdown_menu'; -import { Icon } from '../icon'; -import { IconButton } from '../icon_button'; - -const messages = defineMessages({ - all_disabled: { - id: 'status.all_disabled', - defaultMessage: 'Boosts and quotes are disabled', - }, - quote: { id: 'status.quote', defaultMessage: 'Quote' }, - quote_cannot: { - id: 'status.cannot_quote', - defaultMessage: 'Quotes are disabled on this post', - }, - quote_followers_only: { - id: 'status.quote_followers_only', - defaultMessage: 'Only followers can quote this post', - }, - quote_manual_review: { - id: 'status.quote_manual_review', - defaultMessage: 'Author will manually review', - }, - quote_private: { - id: 'status.quote_private', - defaultMessage: 'Private posts cannot be quoted', - }, - reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, - reblog_or_quote: { - id: 'status.reblog_or_quote', - defaultMessage: 'Boost or quote', - }, - reblog_cancel: { - id: 'status.cancel_reblog_private', - defaultMessage: 'Unboost', - }, - reblog_private: { - id: 'status.reblog_private', - defaultMessage: 'Share again with your followers', - }, - reblog_cannot: { - id: 'status.cannot_reblog', - defaultMessage: 'This post cannot be boosted', - }, - request_quote: { - id: 'status.request_quote', - defaultMessage: 'Request to quote', - }, -}); - -interface ReblogButtonProps { - status: Status; - counters?: boolean; -} - -export const StatusReblogButton: FC = ({ - status, - counters, -}) => { - const intl = useIntl(); - - const statusState = useAppSelector((state) => - selectStatusState(state, status), - ); - const { - isLoggedIn, - isReblogged, - isReblogAllowed, - isQuoteAutomaticallyAccepted, - isQuoteManuallyAccepted, - } = statusState; - const { iconComponent } = useMemo( - () => reblogIconText(statusState), - [statusState], - ); - const disabled = - !isQuoteAutomaticallyAccepted && - !isQuoteManuallyAccepted && - !isReblogAllowed; - - const dispatch = useAppDispatch(); - const statusId = status.get('id') as string; - const items: ActionMenuItem[] = useMemo( - () => [ - { - text: 'reblog', - action: (event) => { - if (isLoggedIn) { - dispatch(toggleReblog(statusId, event.shiftKey)); - } - }, - }, - { - text: 'quote', - action: () => { - if (isLoggedIn) { - dispatch(quoteComposeById(statusId)); - } - }, - }, - ], - [dispatch, isLoggedIn, statusId], - ); - - const handleDropdownOpen = useCallback( - (event: MouseEvent | KeyboardEvent) => { - if (!isLoggedIn) { - dispatch( - openModal({ - modalType: 'INTERACTION', - modalProps: { - type: 'reblog', - accountId: status.getIn(['account', 'id']), - url: status.get('uri'), - }, - }), - ); - } else if (event.shiftKey) { - dispatch(toggleReblog(status.get('id'), true)); - return false; - } - return true; - }, - [dispatch, isLoggedIn, status], - ); - - const renderMenuItem: RenderItemFn = useCallback( - (item, index, handlers, focusRefCallback) => ( - - ), - [status], - ); - - return ( - - - - ); -}; - -interface ReblogMenuItemProps { - status: Status; - item: ActionMenuItem; - index: number; - handlers: RenderItemFnHandlers; - focusRefCallback?: (c: HTMLAnchorElement | HTMLButtonElement | null) => void; -} - -const ReblogMenuItem: FC = ({ - status, - index, - item: { text }, - handlers, - focusRefCallback, -}) => { - const intl = useIntl(); - const statusState = useAppSelector((state) => - selectStatusState(state, status), - ); - const { title, meta, iconComponent, disabled } = useMemo( - () => - text === 'quote' - ? quoteIconText(statusState) - : reblogIconText(statusState), - [statusState, text], - ); - const active = useMemo( - () => text === 'reblog' && !!status.get('reblogged'), - [status, text], - ); - - return ( -
  • - -
  • - ); -}; - -// Legacy helpers - -// Switch between the legacy and new reblog button based on feature flag. -export const ReblogButton: FC = (props) => { - if (isFeatureEnabled('outgoing_quotes')) { - return ; - } - return ; -}; - -export const LegacyReblogButton: FC = ({ - status, - counters, -}) => { - const intl = useIntl(); - const statusState = useAppSelector((state) => - selectStatusState(state, status), - ); - - const { title, meta, iconComponent, disabled } = useMemo( - () => reblogIconText(statusState), - [statusState], - ); - - const dispatch = useAppDispatch(); - const handleClick: MouseEventHandler = useCallback( - (event) => { - if (statusState.isLoggedIn) { - dispatch(toggleReblog(status.get('id') as string, event.shiftKey)); - } else { - dispatch( - openModal({ - modalType: 'INTERACTION', - modalProps: { - type: 'reblog', - accountId: status.getIn(['account', 'id']), - url: status.get('uri'), - }, - }), - ); - } - }, - [dispatch, status, statusState.isLoggedIn], - ); - - return ( - - ); -}; - -// Helpers for copy and state for status. -const selectStatusState = createAppSelector( - [ - (state) => state.meta.get('me') as string | undefined, - (_, status: Status) => status, - ], - (userId, status) => { - const isPublic = ['public', 'unlisted'].includes( - status.get('visibility') as StatusVisibility, - ); - const isMineAndPrivate = - userId === status.getIn(['account', 'id']) && - status.get('visibility') === 'private'; - return { - isLoggedIn: !!userId, - isPublic, - isMine: userId === status.getIn(['account', 'id']), - isPrivateReblog: - userId === status.getIn(['account', 'id']) && - status.get('visibility') === 'private', - isReblogged: !!status.get('reblogged'), - isReblogAllowed: isPublic || isMineAndPrivate, - isQuoteAutomaticallyAccepted: - status.getIn(['quote_approval', 'current_user']) === 'automatic' && - (isPublic || isMineAndPrivate), - isQuoteManuallyAccepted: - status.getIn(['quote_approval', 'current_user']) === 'manual' && - (isPublic || isMineAndPrivate), - isQuoteFollowersOnly: - status.getIn(['quote_approval', 'automatic', 0]) === 'followers' || - status.getIn(['quote_approval', 'manual', 0]) === 'followers', - }; - }, -); -type StatusState = ReturnType; - -interface IconText { - title: MessageDescriptor; - meta?: MessageDescriptor; - iconComponent: FC>; - disabled?: boolean; -} - -function reblogIconText({ - isPublic, - isPrivateReblog, - isReblogged, -}: StatusState): IconText { - if (isReblogged) { - return { - title: messages.reblog_cancel, - iconComponent: isPublic ? RepeatActiveIcon : RepeatPrivateActiveIcon, - }; - } - const iconText: IconText = { - title: messages.reblog, - iconComponent: RepeatIcon, - }; - - if (isPrivateReblog) { - iconText.meta = messages.reblog_private; - iconText.iconComponent = RepeatPrivateIcon; - } else if (!isPublic) { - iconText.meta = messages.reblog_cannot; - iconText.iconComponent = RepeatDisabledIcon; - iconText.disabled = true; - } - return iconText; -} - -function quoteIconText({ - isMine, - isQuoteAutomaticallyAccepted, - isQuoteManuallyAccepted, - isQuoteFollowersOnly, - isPublic, -}: StatusState): IconText { - const iconText: IconText = { - title: messages.quote, - iconComponent: FormatQuote, - }; - - if (!isPublic && !isMine) { - iconText.disabled = true; - iconText.iconComponent = FormatQuoteOff; - iconText.meta = messages.quote_private; - } else if (isQuoteAutomaticallyAccepted) { - iconText.title = messages.quote; - } else if (isQuoteManuallyAccepted) { - iconText.title = messages.request_quote; - iconText.meta = messages.quote_manual_review; - } else { - iconText.disabled = true; - iconText.iconComponent = FormatQuoteOff; - iconText.meta = isQuoteFollowersOnly - ? messages.quote_followers_only - : messages.quote_cannot; - } - - return iconText; -} diff --git a/app/javascript/mastodon/components/status_action_bar/index.jsx b/app/javascript/mastodon/components/status_action_bar/index.jsx index 096924061..3aff359c1 100644 --- a/app/javascript/mastodon/components/status_action_bar/index.jsx +++ b/app/javascript/mastodon/components/status_action_bar/index.jsx @@ -24,7 +24,7 @@ import { me } from '../../initial_state'; import { IconButton } from '../icon_button'; import { isFeatureEnabled } from '../../utils/environment'; -import { ReblogButton } from '../status/reblog_button'; +import { BoostButton } from '../status/boost_button'; import { RemoveQuoteHint } from './remove_quote_hint'; const messages = defineMessages({ @@ -372,7 +372,7 @@ class StatusActionBar extends ImmutablePureComponent {
    - +
    diff --git a/app/javascript/mastodon/features/status/components/action_bar.jsx b/app/javascript/mastodon/features/status/components/action_bar.jsx index f5dea36cc..fa9d6497a 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.jsx +++ b/app/javascript/mastodon/features/status/components/action_bar.jsx @@ -20,7 +20,7 @@ import { IconButton } from '../../../components/icon_button'; import { Dropdown } from 'mastodon/components/dropdown_menu'; import { me } from '../../../initial_state'; import { isFeatureEnabled } from '@/mastodon/utils/environment'; -import { ReblogButton } from '@/mastodon/components/status/reblog_button'; +import { BoostButton } from '@/mastodon/components/status/boost_button'; const messages = defineMessages({ delete: { id: 'status.delete', defaultMessage: 'Delete' }, @@ -310,7 +310,7 @@ class ActionBar extends PureComponent {
    - +
    diff --git a/app/javascript/mastodon/features/ui/components/actions_modal.tsx b/app/javascript/mastodon/features/ui/components/actions_modal.tsx index da42b8639..2577b21a1 100644 --- a/app/javascript/mastodon/features/ui/components/actions_modal.tsx +++ b/app/javascript/mastodon/features/ui/components/actions_modal.tsx @@ -1,6 +1,7 @@ import classNames from 'classnames'; import { Link } from 'react-router-dom'; +import { DropdownMenuItemContent } from 'mastodon/components/dropdown_menu'; import type { MenuItem } from 'mastodon/models/dropdown_menu'; import { isActionItem, @@ -18,14 +19,14 @@ export const ActionsModal: React.FC<{ return
  • ; } - const { text, dangerous } = option; + const { text, highlighted, disabled, dangerous } = option; let element: React.ReactElement; if (isActionItem(option)) { element = ( - ); } else if (isExternalLinkItem(option)) { @@ -38,21 +39,22 @@ export const ActionsModal: React.FC<{ onClick={onClick} data-index={i} > - {text} + ); } else { element = ( - {text} + ); } return (
  • diff --git a/app/javascript/mastodon/models/dropdown_menu.ts b/app/javascript/mastodon/models/dropdown_menu.ts index 963b5071c..01da28693 100644 --- a/app/javascript/mastodon/models/dropdown_menu.ts +++ b/app/javascript/mastodon/models/dropdown_menu.ts @@ -1,7 +1,13 @@ import type { KeyboardEvent, MouseEvent, TouchEvent } from 'react'; +import type { IconProp } from '../components/icon'; + interface BaseMenuItem { text: string; + description?: string; + icon?: IconProp; + highlighted?: boolean; + disabled?: boolean; dangerous?: boolean; } diff --git a/app/javascript/mastodon/utils/types.ts b/app/javascript/mastodon/utils/types.ts new file mode 100644 index 000000000..24b9ee180 --- /dev/null +++ b/app/javascript/mastodon/utils/types.ts @@ -0,0 +1,16 @@ +/** + * Extend an existing type and make some of its properties required or optional. + * @example + * interface Person { + * name: string; + * age?: number; + * likesIceCream?: boolean; + * } + * + * type PersonWithSomeRequired = SomeRequired; + * type PersonWithSomeOptional = SomeOptional; + */ + +export type SomeRequired = T & Required>; +export type SomeOptional = Pick> & + Partial>; diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 1b5867e8b..2c7a57f7d 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -2874,10 +2874,26 @@ a.account__display-name { color: $error-value-color; } + &--highlighted { + color: $highlight-text-color; + } + + &-content { + display: flex; + flex-direction: column; + } + + &-subtitle { + font-weight: 400; + } + a, button { font: inherit; - display: block; + display: flex; + align-items: center; + gap: 8px; + white-space: inherit; width: 100%; padding: 10px 14px; border: 0; @@ -2886,9 +2902,6 @@ a.account__display-name { box-sizing: border-box; text-decoration: none; color: inherit; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; text-align: inherit; border-radius: 4px; @@ -2908,30 +2921,8 @@ a.account__display-name { } } -.reblog-button { - &__item { - max-width: 360px; - - button { - display: flex; - align-items: center; - gap: 8px; - white-space: inherit; - } - - div { - display: flex; - flex-direction: column; - } - - &.active:not(.disabled) { - color: $highlight-text-color; - } - } - - &__meta { - font-weight: 400; - } +.reblog-menu-item { + max-width: 360px; } .inline-account {