From 224e68dd39478c79c8308c06eeb1603d36dc8ecf Mon Sep 17 00:00:00 2001 From: Echo Date: Wed, 23 Sep 2026 09:03:40 +0000 Subject: [PATCH] Status redesign: Replace icons for status actions (#40655) --- .../mastodon/components/status/action_bar.tsx | 138 +++++---------- .../components/status/boost_button_utils.ts | 35 ++-- .../mastodon/components/status/hooks.ts | 165 +++++++++++++++--- .../mastodon/components/status/icons.tsx | 49 ++++++ .../status/legacy/action_bar/index.jsx | 17 +- .../components/status/legacy/boost_button.tsx | 17 +- .../components/status/legacy/status.jsx | 4 +- .../components/status/legacy/thread_label.tsx | 4 +- .../components/conversation.tsx | 4 +- .../features/navigation_panel/index.tsx | 18 +- .../notifications/components/notification.jsx | 12 +- .../components/notification_favourite.tsx | 4 +- .../components/notification_mention.tsx | 4 +- .../components/notification_quote.tsx | 4 +- .../components/notification_reblog.tsx | 4 +- .../features/notifications_v2/filter_bar.tsx | 14 +- .../picture_in_picture/components/footer.tsx | 100 ++--------- .../features/status/components/action_bar.jsx | 30 ++-- .../features/ui/components/block_modal.jsx | 4 +- .../features/ui/components/boost_modal.tsx | 7 +- .../ui/components/domain_block_modal.tsx | 6 +- .../features/ui/components/mute_modal.jsx | 4 +- app/javascript/svg-icons/boost_active.svg | 1 + 23 files changed, 361 insertions(+), 284 deletions(-) create mode 100644 app/javascript/mastodon/components/status/icons.tsx create mode 100644 app/javascript/svg-icons/boost_active.svg diff --git a/app/javascript/mastodon/components/status/action_bar.tsx b/app/javascript/mastodon/components/status/action_bar.tsx index d111922f0e1..9f2e4460548 100644 --- a/app/javascript/mastodon/components/status/action_bar.tsx +++ b/app/javascript/mastodon/components/status/action_bar.tsx @@ -1,18 +1,10 @@ import { useCallback } from 'react'; -import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; +import { FormattedMessage } from 'react-intl'; import classNames from 'classnames'; -import { - ArrowsClockwiseIcon, - BookmarkSimpleIcon, - ChatCircleIcon, - DotsThreeIcon, - HeartIcon, - QuotesIcon, - ShareFatIcon, -} from '@phosphor-icons/react'; +import { DotsThreeIcon, ShareFatIcon } from '@phosphor-icons/react'; import { statusInteraction } from '@/mastodon/actions/interactions'; import { fetchStatus } from '@/mastodon/actions/statuses'; @@ -20,7 +12,6 @@ import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId'; import { useAccountStatus } from '@/mastodon/hooks/useStatus'; import { quickBoosting } from '@/mastodon/initial_state'; import type { AccountStatusShape } from '@/mastodon/models/status'; -import { selectStatusConditions } from '@/mastodon/selectors/statuses'; import { useAppDispatch, useAppSelector } from '@/mastodon/store'; import { @@ -29,7 +20,6 @@ import { ToggleButton, ToggleIconButton, } from '../button/redesign'; -import { iconWeight, useIconWeight } from '../icon'; import { Menu, MenuItem, @@ -38,8 +28,11 @@ import { LegacyDropdownMenuItems, } from '../menu'; -import { boostItemState, quoteItemState } from './boost_button_utils'; -import { useStatusContext, useStatusMenuActions } from './hooks'; +import { + useStatusContext, + useStatusIcons, + useStatusMenuActions, +} from './hooks'; import { RemoveQuoteHint } from './legacy/action_bar/remove_quote_hint'; import classes from './styles.module.scss'; @@ -51,15 +44,6 @@ interface StatusActionBarProps { onlyResponses?: boolean; } -const messages = defineMessages({ - replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' }, - favourite: { id: 'status.like', defaultMessage: 'Like' }, - removeFavourite: { - id: 'status.unlike', - defaultMessage: 'Unlike', - }, -}); - export const StatusActionBar: React.FC = ({ statusId, withDismiss, @@ -77,12 +61,6 @@ export const StatusActionBar: React.FC = ({ // Actions const dispatch = useAppDispatch(); - const handleReplyClick = useCallback(() => { - dispatch(statusInteraction({ statusId, intent: 'reply', contextType })); - }, [contextType, dispatch, statusId]); - const handleFavouriteClick = useCallback(() => { - dispatch(statusInteraction({ statusId, intent: 'favourite', contextType })); - }, [contextType, dispatch, statusId]); const handleShareClick = useCallback(() => { if (!statusUrl) { return; @@ -99,17 +77,8 @@ export const StatusActionBar: React.FC = ({ dispatch(statusInteraction({ statusId, intent: 'copy', contextType })); } }, [contextType, dispatch, statusId, statusUrl]); - const handleBookmarkClick = useCallback(() => { - dispatch(statusInteraction({ statusId, intent: 'bookmark', contextType })); - }, [contextType, dispatch, statusId]); - const intl = useIntl(); - - const favouriteIcon = useIconWeight(HeartIcon, status?.favourited && 'fill'); - const bookmarkIcon = useIconWeight( - BookmarkSimpleIcon, - status?.bookmarked && 'fill', - ); + const { reply, like, bookmark } = useStatusIcons(statusId); if (!status) { return null; @@ -118,10 +87,6 @@ export const StatusActionBar: React.FC = ({ const isPublic = status.visibility === 'public' || status.visibility === 'unlisted'; - const favouriteTitle = intl.formatMessage( - status.favourited ? messages.removeFavourite : messages.favourite, - ); - const isQuotingMe = quotedAccountId === currentAccountId; const shouldShowQuoteRemovalHint = isQuotingMe && contextType === 'notifications'; @@ -132,11 +97,11 @@ export const StatusActionBar: React.FC = ({ size='sm' clipPadding variant='ghost' - title={intl.formatMessage(messages.replyAll)} - leadingIcon={ChatCircleIcon} - onClick={handleReplyClick} + title={reply.title} + leadingIcon={reply.icon} + onClick={reply.action} > - {withCounters && status.replies_count} + {withCounters && reply.counter} @@ -146,13 +111,13 @@ export const StatusActionBar: React.FC = ({ - {withCounters && status.favourites_count} + {withCounters && like.counter} ); @@ -179,18 +144,12 @@ export const StatusActionBar: React.FC = ({ - {!status.bookmarked ? ( - - ) : ( - - )} + {bookmark.title} = ({ statusId, children }) => { - const conditions = useAppSelector((state) => - selectStatusConditions(state, statusId), - ); - const { isBoosted } = conditions; - - const boostState = boostItemState(conditions); - const quoteState = quoteItemState(conditions); - const intl = useIntl(); - - const dispatch = useAppDispatch(); - const onReblog = useCallback(() => { - dispatch(statusInteraction({ statusId, intent: 'reblog' })); - }, [dispatch, statusId]); - const onQuote = useCallback(() => { - dispatch(statusInteraction({ statusId, intent: 'quote' })); - }, [dispatch, statusId]); + const { boost, quote } = useStatusIcons(statusId); if (quickBoosting) { return ( {children} @@ -251,36 +196,35 @@ const StatusReblogButton: React.FC<{ as={ToggleButton} size='sm' variant='ghost' - active={isBoosted} - leadingIcon={ArrowsClockwiseIcon} + active={boost.active} + title={boost.title} + leadingIcon={boost.icon} > {children} - {intl.formatMessage(boostState.title)} + {boost.title} - {intl.formatMessage(quoteState.title)} + {quote.title} ); }; -const QuotesFilledIcon = iconWeight(QuotesIcon, 'fill'); - const StatusActionMenu: React.FC<{ dismissQuoteHint: () => void; status: AccountStatusShape; diff --git a/app/javascript/mastodon/components/status/boost_button_utils.ts b/app/javascript/mastodon/components/status/boost_button_utils.ts index eab41958973..6e8a1f2d772 100644 --- a/app/javascript/mastodon/components/status/boost_button_utils.ts +++ b/app/javascript/mastodon/components/status/boost_button_utils.ts @@ -2,16 +2,20 @@ import { defineMessages } from 'react-intl'; import type { MessageDescriptor } from 'react-intl'; import type { StatusConditions } from '@/mastodon/selectors/statuses'; -import FormatQuote from '@/material-icons/400-24px/format_quote-fill.svg?react'; +import { isRedesignEnabled } from '@/mastodon/utils/environment'; 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'; +import { + StatusBoostActiveIcon, + StatusBoostIcon, + StatusQuoteIcon, +} from './icons'; + export const messages = defineMessages({ all_disabled: { id: 'status.all_disabled', @@ -76,20 +80,27 @@ export function boostItemState({ if (isBoosted) { return { title: messages.reblog_cancel, - iconComponent: isPublic ? RepeatActiveIcon : RepeatPrivateActiveIcon, + iconComponent: + isPublic || isRedesignEnabled() + ? StatusBoostActiveIcon + : RepeatPrivateActiveIcon, }; } const iconText: MenuItemState = { title: messages.reblog, - iconComponent: RepeatIcon, + iconComponent: StatusBoostIcon, }; if (isPrivateReblog) { iconText.meta = messages.reblog_private; - iconText.iconComponent = RepeatPrivateIcon; + if (!isRedesignEnabled()) { + iconText.iconComponent = RepeatPrivateIcon; + } } else if (!isPublic) { iconText.meta = messages.reblog_cannot; - iconText.iconComponent = RepeatDisabledIcon; + if (!isRedesignEnabled()) { + iconText.iconComponent = RepeatDisabledIcon; + } iconText.disabled = true; } return iconText; @@ -105,12 +116,14 @@ export function quoteItemState({ }: StatusConditions): MenuItemState { const iconText: MenuItemState = { title: messages.quote, - iconComponent: FormatQuote, + iconComponent: StatusQuoteIcon, }; if (!isPublic && !isMine) { iconText.disabled = true; - iconText.iconComponent = FormatQuoteOff; + if (!isRedesignEnabled()) { + iconText.iconComponent = FormatQuoteOff; + } iconText.meta = messages.quote_private; } else if (isQuoteAutomaticallyAccepted) { iconText.title = messages.quote; @@ -120,7 +133,9 @@ export function quoteItemState({ // We don't show the disabled state when logged out } else if (isLoggedIn) { iconText.disabled = true; - iconText.iconComponent = FormatQuoteOff; + if (!isRedesignEnabled()) { + iconText.iconComponent = FormatQuoteOff; + } iconText.meta = isQuoteFollowersOnly ? messages.quote_followers_only : messages.quote_cannot; diff --git a/app/javascript/mastodon/components/status/hooks.ts b/app/javascript/mastodon/components/status/hooks.ts index badc7a8d7e0..df48d030706 100644 --- a/app/javascript/mastodon/components/status/hooks.ts +++ b/app/javascript/mastodon/components/status/hooks.ts @@ -25,7 +25,7 @@ import { statusInteraction } from '@/mastodon/actions/interactions_typed'; import { openModal } from '@/mastodon/actions/modal'; import { toggleStatusSpoilers } from '@/mastodon/actions/statuses'; import { useRelationship } from '@/mastodon/hooks/useRelationship'; -import { useExpandedStatus } from '@/mastodon/hooks/useStatus'; +import { useExpandedStatus, useStatus } from '@/mastodon/hooks/useStatus'; import { useToggle } from '@/mastodon/hooks/useToggle'; import { useIdentity } from '@/mastodon/identity_context'; import { quickBoosting } from '@/mastodon/initial_state'; @@ -56,8 +56,18 @@ import type { OnElementHandler } from '@/mastodon/utils/html'; import { FOCUS_TARGET } from '../navigation_focus_target'; -import { quoteItemState } from './boost_button_utils'; +import { boostItemState, quoteItemState } from './boost_button_utils'; import { useElementHandledLink } from './handled_link'; +import { + StatusBookmarkActiveIcon, + StatusBookmarkIcon, + StatusBoostActiveIcon, + StatusBoostIcon, + StatusLikeActiveIcon, + StatusLikeIcon, + StatusReplyAllIcon, + StatusReplyIcon, +} from './icons'; import type { StatusContextType } from './types'; export const StatusContext = createContext<{ @@ -69,6 +79,23 @@ export function useStatusContext() { return use(StatusContext); } +export function useStatusInteractionFactory( + statusId?: string, + contextTypeArg?: StatusContextType, +) { + const statusContext = useStatusContext(); + const contextType = contextTypeArg ?? statusContext.contextType; + const dispatch = useAppDispatch(); + return useCallback( + (intent: StatusInteractionIntent) => { + return () => { + dispatch(statusInteraction({ statusId, intent, contextType })); + }; + }, + [contextType, dispatch, statusId], + ); +} + export function useStatusHandlers({ status, contextType, @@ -101,14 +128,7 @@ export function useStatusHandlers({ }, [dispatch, filterAction, onFilterToggle, showDespiteFilter, status]); // Interaction handlers - const handlerFactory = useCallback( - (intent: StatusInteractionIntent) => { - return () => { - dispatch(statusInteraction({ statusId, intent, contextType })); - }; - }, - [contextType, dispatch, statusId], - ); + const handlerFactory = useStatusInteractionFactory(statusId, contextType); const accountId = status?.account.id; const onMention = useCallback(() => { @@ -238,6 +258,120 @@ export function useStatusHandlers({ } export type StatusHandlers = ReturnType; +interface StatusIcon { + icon: React.FC>; + title: string; + meta?: string; + counter?: number; + active?: boolean; + action: () => void; + disabled: boolean; +} + +const iconMessages = defineMessages({ + reply: { id: 'status.reply', defaultMessage: 'Reply' }, + replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' }, + favourite: { id: 'status.favourite', defaultMessage: 'Favorite' }, + removeFavourite: { + id: 'status.remove_favourite', + defaultMessage: 'Remove from favorites', + }, + like: { id: 'status.like', defaultMessage: 'Like' }, + removeLike: { + id: 'status.unlike', + defaultMessage: 'Unlike', + }, + bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' }, + removeBookmark: { + id: 'status.remove_bookmark', + defaultMessage: 'Remove bookmark', + }, +}); + +export function useStatusIcons(statusId: string) { + const intl = useIntl(); + const conditions = useAppSelector((state) => + selectStatusConditions(state, statusId), + ); + const status = useStatus(statusId); + const interactions = useAppSelector((state) => + selectStatusInteractionsAllowed(state, statusId), + ); + const interactionFactory = useStatusInteractionFactory(statusId); + const isRedesign = isRedesignEnabled(); + + const isReplyAll = !!status?.in_reply_to_id; + const reply: StatusIcon = { + icon: isReplyAll ? StatusReplyAllIcon : StatusReplyIcon, + title: intl.formatMessage( + isReplyAll ? iconMessages.replyAll : iconMessages.reply, + ), + counter: status?.replies_count, + action: interactionFactory('reply'), + disabled: interactions.reply, + }; + + const boostState = boostItemState(conditions); + const boost: StatusIcon = { + icon: boostState.iconComponent, + title: intl.formatMessage(boostState.title), + meta: boostState.meta ? intl.formatMessage(boostState.meta) : undefined, + counter: status?.reblogs_count, + active: status?.reblogged ?? false, + action: interactionFactory('reblog'), + disabled: boostState.disabled ?? false, + }; + if (isRedesign) { + boost.icon = status?.reblogged ? StatusBoostActiveIcon : StatusBoostIcon; + } + + const quoteState = quoteItemState(conditions); + const quote: StatusIcon = { + icon: quoteState.iconComponent, + title: intl.formatMessage(quoteState.title), + meta: quoteState.meta ? intl.formatMessage(quoteState.meta) : undefined, + counter: status?.quotes_count, + action: interactionFactory('quote'), + disabled: quoteState.disabled ?? false, + }; + + const isLiked = !!status?.favourited; + const like: StatusIcon = { + icon: isLiked ? StatusLikeActiveIcon : StatusLikeIcon, + title: intl.formatMessage( + isLiked ? iconMessages.removeFavourite : iconMessages.favourite, + ), + counter: status?.favourites_count ?? 0, + active: isLiked, + action: interactionFactory('favourite'), + disabled: interactions.favourite, + }; + if (isRedesign) { + like.title = intl.formatMessage( + isLiked ? iconMessages.removeLike : iconMessages.like, + ); + } + + const isBookmarked = !!status?.bookmarked; + const bookmark: StatusIcon = { + icon: isBookmarked ? StatusBookmarkActiveIcon : StatusBookmarkIcon, + title: intl.formatMessage( + isBookmarked ? iconMessages.removeBookmark : iconMessages.bookmark, + ), + active: isBookmarked, + action: interactionFactory('bookmark'), + disabled: interactions.bookmark, + }; + + return { + reply, + boost, + quote, + like, + bookmark, + } as const; +} + const screenReaderMessages = defineMessages({ quote_noun: { id: 'status.quote_noun', @@ -436,16 +570,7 @@ export function useStatusMenuActions({ const interactions = useAppSelector((state) => selectStatusInteractionsAllowed(state, status.id), ); - const statusInteractionFactory = useCallback( - (intent: StatusInteractionIntent) => { - return () => { - dispatch( - statusInteraction({ statusId: status.id, contextType, intent }), - ); - }; - }, - [contextType, dispatch, status.id], - ); + const statusInteractionFactory = useStatusInteractionFactory(status.id); return useMemo( () => diff --git a/app/javascript/mastodon/components/status/icons.tsx b/app/javascript/mastodon/components/status/icons.tsx new file mode 100644 index 00000000000..e91bfa57442 --- /dev/null +++ b/app/javascript/mastodon/components/status/icons.tsx @@ -0,0 +1,49 @@ +import { + ArrowsClockwiseIcon, + BookmarkSimpleIcon, + ChatCircleIcon, + HeartIcon, + QuotesIcon, +} from '@phosphor-icons/react'; + +import { isRedesignEnabled } from '@/mastodon/utils/environment'; +import BookmarkLegacyIcon from '@/material-icons/400-24px/bookmark-fill.svg?react'; +import BookmarkBorderLegacyIcon from '@/material-icons/400-24px/bookmark.svg?react'; +import QuoteLegacyIcon from '@/material-icons/400-24px/format_quote-fill.svg?react'; +import BoostLegacyIcon from '@/material-icons/400-24px/repeat.svg?react'; +import ReplyLegacyIcon from '@/material-icons/400-24px/reply.svg?react'; +import ReplyAllLegacyIcon from '@/material-icons/400-24px/reply_all.svg?react'; +import StarLegacyIcon from '@/material-icons/400-24px/star-fill.svg?react'; +import StarBorderLegacyIcon from '@/material-icons/400-24px/star.svg?react'; +import BoostActiveIcon from '@/svg-icons/boost_active.svg?react'; +import BoostActiveLegacyIcon from '@/svg-icons/repeat_active.svg?react'; + +import { iconWeight } from '../icon'; + +export const StatusReplyIcon = isRedesignEnabled() + ? ChatCircleIcon + : ReplyLegacyIcon; +export const StatusReplyAllIcon = isRedesignEnabled() + ? StatusReplyIcon + : ReplyAllLegacyIcon; +export const StatusQuoteIcon = isRedesignEnabled() + ? QuoteLegacyIcon + : QuotesIcon; +export const StatusBoostIcon = isRedesignEnabled() + ? ArrowsClockwiseIcon + : BoostLegacyIcon; +export const StatusBoostActiveIcon = isRedesignEnabled() + ? BoostActiveIcon + : BoostActiveLegacyIcon; +export const StatusLikeIcon = isRedesignEnabled() + ? HeartIcon + : StarBorderLegacyIcon; +export const StatusLikeActiveIcon = isRedesignEnabled() + ? iconWeight(HeartIcon, 'fill') + : StarLegacyIcon; +export const StatusBookmarkIcon = isRedesignEnabled() + ? BookmarkSimpleIcon + : BookmarkBorderLegacyIcon; +export const StatusBookmarkActiveIcon = isRedesignEnabled() + ? iconWeight(BookmarkSimpleIcon, 'fill') + : BookmarkLegacyIcon; diff --git a/app/javascript/mastodon/components/status/legacy/action_bar/index.jsx b/app/javascript/mastodon/components/status/legacy/action_bar/index.jsx index 76ce2f2d3f7..76e171891db 100644 --- a/app/javascript/mastodon/components/status/legacy/action_bar/index.jsx +++ b/app/javascript/mastodon/components/status/legacy/action_bar/index.jsx @@ -8,13 +8,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import { ImmutablePureComponent } from 'react-immutable-pure-component'; import { connect } from 'react-redux'; -import BookmarkIcon from '@/material-icons/400-24px/bookmark-fill.svg?react'; -import BookmarkBorderIcon from '@/material-icons/400-24px/bookmark.svg?react'; import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; -import ReplyIcon from '@/material-icons/400-24px/reply.svg?react'; -import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react'; -import StarIcon from '@/material-icons/400-24px/star-fill.svg?react'; -import StarBorderIcon from '@/material-icons/400-24px/star.svg?react'; import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions'; import { WithRouterPropTypes } from 'mastodon/utils/react_router'; @@ -29,6 +23,7 @@ import { RemoveQuoteHint } from './remove_quote_hint'; import { quoteItemState } from '../../boost_button_utils'; import { selectStatusConditions } from '@/mastodon/selectors/statuses'; import { isRedesignEnabled } from '@/mastodon/utils/environment'; +import { StatusBookmarkActiveIcon, StatusBookmarkIcon, StatusLikeActiveIcon, StatusLikeIcon, StatusReplyAllIcon, StatusReplyIcon } from '../../icons'; const baseMessages = defineMessages({ @@ -386,11 +381,11 @@ class StatusActionBar extends ImmutablePureComponent { if (status.get('in_reply_to_id', null) === null) { replyIcon = 'reply'; - replyIconComponent = ReplyIcon; + replyIconComponent = StatusReplyIcon; replyTitle = intl.formatMessage(messages.reply); } else { replyIcon = 'reply-all'; - replyIconComponent = ReplyAllIcon; + replyIconComponent = StatusReplyAllIcon; replyTitle = intl.formatMessage(messages.replyAll); } @@ -403,16 +398,16 @@ class StatusActionBar extends ImmutablePureComponent { return (
- +
- +
- +
{(dismissQuoteHint) => ( diff --git a/app/javascript/mastodon/components/status/legacy/boost_button.tsx b/app/javascript/mastodon/components/status/legacy/boost_button.tsx index 3103c4c39ad..a4e275dcc60 100644 --- a/app/javascript/mastodon/components/status/legacy/boost_button.tsx +++ b/app/javascript/mastodon/components/status/legacy/boost_button.tsx @@ -28,6 +28,7 @@ import { messages, quoteItemState, } from '../boost_button_utils'; +import { useStatusIcons } from '../hooks'; const StandaloneBoostButton: FC = ({ statusId, @@ -135,6 +136,8 @@ const BoostOrQuoteMenu: FC = ({ statusId, counters }) => { ); }, [dispatch, status]); + const { boost, quote } = useStatusIcons(statusId); + const items = useMemo(() => { const boostItem = boostItemState(statusState); const quoteItem = quoteItemState(statusState); @@ -144,7 +147,7 @@ const BoostOrQuoteMenu: FC = ({ statusId, counters }) => { description: boostItem.meta ? intl.formatMessage(boostItem.meta) : undefined, - icon: boostItem.iconComponent, + icon: boost.icon, highlighted: wasBoosted, disabled: boostItem.disabled, action: (event) => { @@ -156,14 +159,22 @@ const BoostOrQuoteMenu: FC = ({ statusId, counters }) => { description: quoteItem.meta ? intl.formatMessage(quoteItem.meta) : undefined, - icon: quoteItem.iconComponent, + icon: quote.icon, disabled: quoteItem.disabled, action: () => { dispatch(quoteComposeById(statusId)); }, }, ] satisfies [ActionMenuItemWithIcon, ActionMenuItemWithIcon]; - }, [dispatch, intl, statusId, statusState, wasBoosted]); + }, [ + boost.icon, + dispatch, + intl, + quote.icon, + statusId, + statusState, + wasBoosted, + ]); const boostIcon = items[0].icon; diff --git a/app/javascript/mastodon/components/status/legacy/status.jsx b/app/javascript/mastodon/components/status/legacy/status.jsx index 2b060daec7f..439da5978a8 100644 --- a/app/javascript/mastodon/components/status/legacy/status.jsx +++ b/app/javascript/mastodon/components/status/legacy/status.jsx @@ -8,7 +8,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import { ImmutablePureComponent } from 'react-immutable-pure-component'; import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react'; -import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react'; import { Hotkeys } from '@/mastodon/components/hotkeys'; import { Icon } from '@/mastodon/components/icon'; import { PictureInPicturePlaceholder } from './picture_in_picture_placeholder'; @@ -34,6 +33,7 @@ import { getHashtagBarForStatus } from './hashtag_bar'; import StatusActionBar from './action_bar'; import StatusContent from './content'; import { StatusThreadLabel } from './thread_label'; +import { StatusBoostIcon } from '../icons'; const domParser = new DOMParser(); @@ -438,7 +438,7 @@ class Status extends ImmutablePureComponent { prepend = (
-
+
); diff --git a/app/javascript/mastodon/components/status/legacy/thread_label.tsx b/app/javascript/mastodon/components/status/legacy/thread_label.tsx index a38e2db277c..75ff639bd11 100644 --- a/app/javascript/mastodon/components/status/legacy/thread_label.tsx +++ b/app/javascript/mastodon/components/status/legacy/thread_label.tsx @@ -1,10 +1,10 @@ import { FormattedMessage } from 'react-intl'; -import ReplyIcon from '@/material-icons/400-24px/reply.svg?react'; import { Icon } from 'mastodon/components/icon'; import { useAppSelector } from 'mastodon/store'; import { LinkedDisplayName } from '../../display_name'; +import { StatusReplyIcon } from '../icons'; export const StatusThreadLabel: React.FC<{ accountId: string; @@ -49,7 +49,7 @@ export const StatusThreadLabel: React.FC<{ return (
- +
{label}
diff --git a/app/javascript/mastodon/features/direct_timeline/components/conversation.tsx b/app/javascript/mastodon/features/direct_timeline/components/conversation.tsx index cecaf29b168..9c08b19a10c 100644 --- a/app/javascript/mastodon/features/direct_timeline/components/conversation.tsx +++ b/app/javascript/mastodon/features/direct_timeline/components/conversation.tsx @@ -13,6 +13,7 @@ import type { import { LinkedDisplayName } from '@/mastodon/components/display_name'; import { AnimateEmojiProvider } from '@/mastodon/components/emoji/context'; +import { StatusReplyIcon } from '@/mastodon/components/status/icons'; import StatusContent from '@/mastodon/components/status/legacy/content'; import type { Account } from '@/mastodon/models/account'; import type { StatusShape } from '@/mastodon/models/status'; @@ -22,7 +23,6 @@ import { useAppSelector, } from '@/mastodon/store'; import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; -import ReplyIcon from '@/material-icons/400-24px/reply.svg?react'; import { replyCompose } from 'mastodon/actions/compose'; import { markConversationRead, @@ -256,7 +256,7 @@ export const Conversation: React.FC<{ className='status__action-bar-button' title={intl.formatMessage(messages.reply)} icon='reply' - iconComponent={ReplyIcon} + iconComponent={StatusReplyIcon} onClick={handleReply} /> diff --git a/app/javascript/mastodon/features/navigation_panel/index.tsx b/app/javascript/mastodon/features/navigation_panel/index.tsx index 92f79a5c780..9ed55beb340 100644 --- a/app/javascript/mastodon/features/navigation_panel/index.tsx +++ b/app/javascript/mastodon/features/navigation_panel/index.tsx @@ -10,11 +10,15 @@ import type { Map as ImmutableMap } from 'immutable'; import { animated, useSpring } from '@react-spring/web'; import { useDrag } from '@use-gesture/react'; +import { + StatusBookmarkActiveIcon, + StatusBookmarkIcon, + StatusLikeActiveIcon, + StatusLikeIcon, +} from '@/mastodon/components/status/icons'; import { useAccount } from '@/mastodon/hooks/useAccount'; import AddIcon from '@/material-icons/400-24px/add.svg?react'; import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react'; -import BookmarksActiveIcon from '@/material-icons/400-24px/bookmarks-fill.svg?react'; -import BookmarksIcon from '@/material-icons/400-24px/bookmarks.svg?react'; import CollectionsActiveIcon from '@/material-icons/400-24px/category-fill.svg?react'; import CollectionsIcon from '@/material-icons/400-24px/category.svg?react'; import HomeActiveIcon from '@/material-icons/400-24px/home-fill.svg?react'; @@ -26,8 +30,6 @@ import PersonAddActiveIcon from '@/material-icons/400-24px/person_add-fill.svg?r import PersonAddIcon from '@/material-icons/400-24px/person_add.svg?react'; import PublicIcon from '@/material-icons/400-24px/public.svg?react'; import SettingsIcon from '@/material-icons/400-24px/settings.svg?react'; -import StarActiveIcon from '@/material-icons/400-24px/star-fill.svg?react'; -import StarIcon from '@/material-icons/400-24px/star.svg?react'; import TrendingUpIcon from '@/material-icons/400-24px/trending_up.svg?react'; import { fetchFollowRequests } from 'mastodon/actions/accounts'; import { openNavigation, closeNavigation } from 'mastodon/actions/navigation'; @@ -345,8 +347,8 @@ export const NavigationPanel: React.FC<{ multiColumn?: boolean }> = ({ transparent to='/favourites' icon='star' - iconComponent={StarIcon} - activeIconComponent={StarActiveIcon} + iconComponent={StatusLikeIcon} + activeIconComponent={StatusLikeActiveIcon} text={intl.formatMessage(messages.favourites)} /> @@ -355,8 +357,8 @@ export const NavigationPanel: React.FC<{ multiColumn?: boolean }> = ({ transparent to='/bookmarks' icon='bookmarks' - iconComponent={BookmarksIcon} - activeIconComponent={BookmarksActiveIcon} + iconComponent={StatusBookmarkIcon} + activeIconComponent={StatusBookmarkActiveIcon} text={intl.formatMessage(messages.bookmarks)} /> diff --git a/app/javascript/mastodon/features/notifications/components/notification.jsx b/app/javascript/mastodon/features/notifications/components/notification.jsx index 621633c4228..05cf2372489 100644 --- a/app/javascript/mastodon/features/notifications/components/notification.jsx +++ b/app/javascript/mastodon/features/notifications/components/notification.jsx @@ -3,20 +3,17 @@ import PropTypes from 'prop-types'; import { FormattedMessage, defineMessages } from 'react-intl'; import classNames from 'classnames'; -import { Link, withRouter } from 'react-router-dom'; +import { withRouter } from 'react-router-dom'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { ImmutablePureComponent } from 'react-immutable-pure-component'; import EditIcon from '@/material-icons/400-24px/edit.svg?react'; import FlagIcon from '@/material-icons/400-24px/flag-fill.svg?react'; -import FormatQuoteIcon from '@/material-icons/400-24px/format_quote-fill.svg?react'; import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react'; import InsertChartIcon from '@/material-icons/400-24px/insert_chart.svg?react'; import PersonIcon from '@/material-icons/400-24px/person-fill.svg?react'; import PersonAddIcon from '@/material-icons/400-24px/person_add-fill.svg?react'; -import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react'; -import StarIcon from '@/material-icons/400-24px/star-fill.svg?react'; import { Account } from 'mastodon/components/account'; import { LinkedDisplayName } from '@/mastodon/components/display_name'; import { Icon } from 'mastodon/components/icon'; @@ -32,6 +29,7 @@ import { ModerationWarning } from './moderation_warning'; import { RelationshipsSeveranceEvent } from './relationships_severance_event'; import Report from './report'; import { isRedesignEnabled } from '@/mastodon/utils/environment'; +import { StatusBoostIcon, StatusLikeIcon, StatusQuoteIcon } from '@/mastodon/components/status/icons'; const messages = defineMessages({ favourite: { id: 'notification.favourite', defaultMessage: '{name} favorited your post' }, @@ -190,7 +188,7 @@ class Notification extends ImmutablePureComponent {
- + @@ -220,7 +218,7 @@ class Notification extends ImmutablePureComponent {
- + @@ -250,7 +248,7 @@ class Notification extends ImmutablePureComponent {
- + diff --git a/app/javascript/mastodon/features/notifications_v2/components/notification_favourite.tsx b/app/javascript/mastodon/features/notifications_v2/components/notification_favourite.tsx index d09a7fd9fac..900b73b8a1e 100644 --- a/app/javascript/mastodon/features/notifications_v2/components/notification_favourite.tsx +++ b/app/javascript/mastodon/features/notifications_v2/components/notification_favourite.tsx @@ -2,8 +2,8 @@ import { defineMessages, FormattedMessage } from 'react-intl'; import { Link } from 'react-router-dom'; +import { StatusLikeActiveIcon } from '@/mastodon/components/status/icons'; import { isRedesignEnabled } from '@/mastodon/utils/environment'; -import StarIcon from '@/material-icons/400-24px/star-fill.svg?react'; import type { NotificationGroupFavourite } from 'mastodon/models/notification_group'; import { useAppSelector } from 'mastodon/store'; @@ -117,7 +117,7 @@ export const NotificationFavourite: React.FC<{ return ( { key='mention' title={intl.formatMessage(tooltips.mentions)} > - + { key='favourite' title={intl.formatMessage(tooltips.favourites)} > - + { key='reblog' title={intl.formatMessage(tooltips.boosts)} > - + { - if (!status) { - return; - } - - if (signedIn) { - dispatch(toggleFavourite(status.get('id'))); - } else { - dispatch( - openModal({ - modalType: 'INTERACTION', - modalProps: { - intent: 'favourite', - accountId: status.getIn(['account', 'id']), - url: status.get('uri'), - }, - }), - ); - } - }, [dispatch, status, signedIn]); - const handleOpenClick = useCallback( (e: React.MouseEvent) => { if (e.button !== 0 || !status) { @@ -132,50 +86,22 @@ export const Footer: React.FC<{ [history, status, account, onClose], ); + const { reply, like } = useStatusIcons(statusId); + if (!status) { return null; } - let replyIcon, replyIconComponent, replyTitle; - - if (status.get('in_reply_to_id', null) === null) { - replyIcon = 'reply'; - replyIconComponent = ReplyIcon; - replyTitle = intl.formatMessage(messages.reply); - } else { - replyIcon = 'reply-all'; - replyIconComponent = ReplyAllIcon; - replyTitle = intl.formatMessage(messages.replyAll); - } - - let favouriteTitle = intl.formatMessage( - status.get('favourited') ? messages.removeFavourite : messages.favourite, - ); - if (isRedesignEnabled()) { - favouriteTitle = intl.formatMessage( - status.get('favourited') ? messages.unlike : messages.like, - ); - } - return (
@@ -183,12 +109,12 @@ export const Footer: React.FC<{ {withOpenButton && ( diff --git a/app/javascript/mastodon/features/status/components/action_bar.jsx b/app/javascript/mastodon/features/status/components/action_bar.jsx index 6d8831bb5ea..85f8d9fceb0 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.jsx +++ b/app/javascript/mastodon/features/status/components/action_bar.jsx @@ -6,13 +6,7 @@ import { defineMessages } from 'react-intl'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { connect } from 'react-redux'; -import BookmarkIcon from '@/material-icons/400-24px/bookmark-fill.svg?react'; -import BookmarkBorderIcon from '@/material-icons/400-24px/bookmark.svg?react'; import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; -import ReplyIcon from '@/material-icons/400-24px/reply.svg?react'; -import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react'; -import StarIcon from '@/material-icons/400-24px/star-fill.svg?react'; -import StarBorderIcon from '@/material-icons/400-24px/star.svg?react'; import { injectIntl } from '@/mastodon/components/intl'; import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions'; @@ -24,6 +18,14 @@ import { BoostButton } from '@/mastodon/components/status/legacy/boost_button'; import { quoteItemState } from '@/mastodon/components/status/boost_button_utils'; import { selectStatusConditions } from '@/mastodon/selectors/statuses'; import { isRedesignEnabled } from '@/mastodon/utils/environment'; +import { + StatusBookmarkActiveIcon, + StatusBookmarkIcon, + StatusLikeActiveIcon, + StatusLikeIcon, + StatusReplyAllIcon, + StatusReplyIcon, +} from '@/mastodon/components/status/icons'; const baseMessages = defineMessages({ delete: { id: 'status.delete', defaultMessage: 'Delete' }, @@ -331,10 +333,10 @@ class ActionBar extends PureComponent { if (status.get('in_reply_to_id', null) === null) { replyIcon = 'reply'; - replyIconComponent = ReplyIcon; + replyIconComponent = StatusReplyIcon; } else { replyIcon = 'reply-all'; - replyIconComponent = ReplyAllIcon; + replyIconComponent = StatusReplyAllIcon; } const bookmarkTitle = intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark); @@ -342,12 +344,18 @@ class ActionBar extends PureComponent { return (
-
+
+ +
-
-
+
+ +
+
+ +
diff --git a/app/javascript/mastodon/features/ui/components/block_modal.jsx b/app/javascript/mastodon/features/ui/components/block_modal.jsx index f4fa68ce6ac..85eaf247d50 100644 --- a/app/javascript/mastodon/features/ui/components/block_modal.jsx +++ b/app/javascript/mastodon/features/ui/components/block_modal.jsx @@ -11,13 +11,13 @@ import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?re import BlockIcon from '@/material-icons/400-24px/block.svg?react'; import CampaignIcon from '@/material-icons/400-24px/campaign.svg?react'; import CollectionsIcon from '@/material-icons/400-24px/category.svg?react'; -import ReplyIcon from '@/material-icons/400-24px/reply.svg?react'; import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react'; import { blockAccount } from 'mastodon/actions/accounts'; import { closeModal } from 'mastodon/actions/modal'; import { NavigationFocusTarget } from 'mastodon/components/navigation_focus_target'; import { Button } from 'mastodon/components/button'; import { Icon } from 'mastodon/components/icon'; +import { StatusReplyIcon } from '@/mastodon/components/status/icons'; export const BlockModal = ({ accountId, acct }) => { const dispatch = useDispatch(); @@ -71,7 +71,7 @@ export const BlockModal = ({ accountId, acct }) => {
  • -
    +
  • diff --git a/app/javascript/mastodon/features/ui/components/boost_modal.tsx b/app/javascript/mastodon/features/ui/components/boost_modal.tsx index 1ddd853c26c..6a1c76221ad 100644 --- a/app/javascript/mastodon/features/ui/components/boost_modal.tsx +++ b/app/javascript/mastodon/features/ui/components/boost_modal.tsx @@ -4,7 +4,7 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import classNames from 'classnames'; -import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react'; +import { StatusBoostIcon } from '@/mastodon/components/status/icons'; import { Button } from 'mastodon/components/button'; import { Icon } from 'mastodon/components/icon'; import { NavigationFocusTarget } from 'mastodon/components/navigation_focus_target'; @@ -57,7 +57,7 @@ export const BoostModal: React.FC<{
    - +
    @@ -81,7 +81,8 @@ export const BoostModal: React.FC<{ values={{ combo: ( - Shift+ + Shift+ + ), }} diff --git a/app/javascript/mastodon/features/ui/components/domain_block_modal.tsx b/app/javascript/mastodon/features/ui/components/domain_block_modal.tsx index 5e7e17809c3..0201483621c 100644 --- a/app/javascript/mastodon/features/ui/components/domain_block_modal.tsx +++ b/app/javascript/mastodon/features/ui/components/domain_block_modal.tsx @@ -2,11 +2,11 @@ import { useCallback, useEffect, useState } from 'react'; import { FormattedMessage } from 'react-intl'; +import { StatusReplyIcon } from '@/mastodon/components/status/icons'; import CampaignIcon from '@/material-icons/400-24px/campaign.svg?react'; import DomainDisabledIcon from '@/material-icons/400-24px/domain_disabled.svg?react'; import HistoryIcon from '@/material-icons/400-24px/history.svg?react'; import PersonRemoveIcon from '@/material-icons/400-24px/person_remove.svg?react'; -import ReplyIcon from '@/material-icons/400-24px/reply.svg?react'; import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react'; import { blockAccount } from 'mastodon/actions/accounts'; import { blockDomain } from 'mastodon/actions/domain_blocks'; @@ -159,7 +159,7 @@ export const DomainBlockModal: React.FC<{
  • - +
    - +
    {
  • -
    +
  • diff --git a/app/javascript/svg-icons/boost_active.svg b/app/javascript/svg-icons/boost_active.svg new file mode 100644 index 00000000000..b1ed1e5abf0 --- /dev/null +++ b/app/javascript/svg-icons/boost_active.svg @@ -0,0 +1 @@ +