From 3bd8929d5003bafa58763a64eedec42ff6bce285 Mon Sep 17 00:00:00 2001 From: diondiondion Date: Tue, 1 Sep 2026 16:35:59 +0200 Subject: [PATCH] Redesign: Add new column header to "Saved Posts" and "Post Likes" pages (#40345) --- .../components/column/back_button.tsx | 4 +- .../mastodon/components/column/header.tsx | 4 +- .../components/column_header/index.tsx | 33 ++-- .../column_header/styles.module.scss | 2 +- .../features/bookmarked_statuses/index.tsx | 35 +++-- .../mastodon/features/favourites/index.jsx | 104 ------------- .../mastodon/features/favourites/index.tsx | 142 ++++++++++++++++++ .../columns_area/redesign.module.scss | 35 ++++- .../ui/components/columns_area/redesign.tsx | 16 +- .../features/ui/util/columns_context.tsx | 2 +- app/javascript/mastodon/locales/en.json | 2 + 11 files changed, 226 insertions(+), 153 deletions(-) delete mode 100644 app/javascript/mastodon/features/favourites/index.jsx create mode 100644 app/javascript/mastodon/features/favourites/index.tsx diff --git a/app/javascript/mastodon/components/column/back_button.tsx b/app/javascript/mastodon/components/column/back_button.tsx index 72fc0443b5f..8050ce95110 100644 --- a/app/javascript/mastodon/components/column/back_button.tsx +++ b/app/javascript/mastodon/components/column/back_button.tsx @@ -5,7 +5,7 @@ import { FormattedMessage } from 'react-intl'; import ArrowBackIcon from '@/material-icons/400-24px/arrow_back.svg?react'; import { Icon } from 'mastodon/components/icon'; import { getColumnSkipLinkId } from 'mastodon/features/ui/components/skip_links'; -import { ButtonInTabsBar } from 'mastodon/features/ui/util/columns_context'; +import { RenderIntoTabsBarPortal } from 'mastodon/features/ui/util/columns_context'; import { useAppHistory } from '../router'; @@ -49,5 +49,5 @@ export const ColumnBackButton: React.FC<{ onClick?: OnClickCallback }> = ({ ); - return {component}; + return {component}; }; diff --git a/app/javascript/mastodon/components/column/header.tsx b/app/javascript/mastodon/components/column/header.tsx index dd8d8fca36a..ecf5008e94a 100644 --- a/app/javascript/mastodon/components/column/header.tsx +++ b/app/javascript/mastodon/components/column/header.tsx @@ -13,7 +13,7 @@ import UnfoldLessIcon from '@/material-icons/400-24px/unfold_less.svg?react'; import UnfoldMoreIcon from '@/material-icons/400-24px/unfold_more.svg?react'; import type { IconProp } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon'; -import { ButtonInTabsBar } from 'mastodon/features/ui/util/columns_context'; +import { RenderIntoTabsBarPortal } from 'mastodon/features/ui/util/columns_context'; import { useIdentity } from 'mastodon/identity_context'; import { getColumnSkipLinkId } from '../../features/ui/components/skip_links'; @@ -340,6 +340,6 @@ export const ColumnHeader: React.FC = ({ if (placeholder) { return component; } else { - return {component}; + return {component}; } }; diff --git a/app/javascript/mastodon/components/column_header/index.tsx b/app/javascript/mastodon/components/column_header/index.tsx index 616a1c512d5..ea4c6da49f8 100644 --- a/app/javascript/mastodon/components/column_header/index.tsx +++ b/app/javascript/mastodon/components/column_header/index.tsx @@ -9,6 +9,7 @@ import { ArrowLeftIcon, ListIcon } from '@phosphor-icons/react'; import { openNavigation } from '@/mastodon/actions/navigation'; import { getColumnSkipLinkId } from '@/mastodon/features/ui/components/skip_links'; import { useBreakpoint } from '@/mastodon/features/ui/hooks/useBreakpoint'; +import { RenderIntoTabsBarPortal } from '@/mastodon/features/ui/util/columns_context'; import { useAppDispatch } from '@/mastodon/store'; import { hasReactChildren } from '@/mastodon/utils/has_react_children'; @@ -38,21 +39,23 @@ export const ColumnHeader: React.FC = ({ const columnIndex = useColumnIndexContext(); return ( -
- {withBackButton ? : } - - - - {hasReactChildren(extraButtons) && ( -
{extraButtons}
- )} -
+ +
+ {withBackButton ? : } + + + + {hasReactChildren(extraButtons) && ( +
{extraButtons}
+ )} +
+
); }; diff --git a/app/javascript/mastodon/components/column_header/styles.module.scss b/app/javascript/mastodon/components/column_header/styles.module.scss index 54a601f0226..315e9c70111 100644 --- a/app/javascript/mastodon/components/column_header/styles.module.scss +++ b/app/javascript/mastodon/components/column_header/styles.module.scss @@ -18,7 +18,7 @@ color: var(--color-text-primary); background: linear-gradient( to bottom, - var(--color-bg-primary) 50%, + var(--color-bg-primary) var(--column-header-gradient-start, 55%), transparent ); } diff --git a/app/javascript/mastodon/features/bookmarked_statuses/index.tsx b/app/javascript/mastodon/features/bookmarked_statuses/index.tsx index 33d93010bf4..3ac7830c8f6 100644 --- a/app/javascript/mastodon/features/bookmarked_statuses/index.tsx +++ b/app/javascript/mastodon/features/bookmarked_statuses/index.tsx @@ -5,7 +5,9 @@ import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; import { Helmet } from '@unhead/react/helmet'; import { Column } from '@/mastodon/components/column'; -import { ColumnHeader } from '@/mastodon/components/column/header'; +import { ColumnHeader as LegacyColumnHeader } from '@/mastodon/components/column/header'; +import { ColumnHeader } from '@/mastodon/components/column_header'; +import { isRedesignEnabled } from '@/mastodon/utils/environment'; import BookmarksIcon from '@/material-icons/400-24px/bookmarks-fill.svg?react'; import { fetchBookmarkedStatuses, @@ -18,6 +20,7 @@ import { useAppDispatch, useAppSelector } from 'mastodon/store'; const messages = defineMessages({ heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' }, + headingRedesign: { id: 'column.saved_posts', defaultMessage: 'Saved Posts' }, }); const Bookmarks: React.FC<{ @@ -74,16 +77,20 @@ const Bookmarks: React.FC<{ bindToDocument={!multiColumn} label={intl.formatMessage(messages.heading)} > - + {isRedesignEnabled() ? ( + + ) : ( + + )} - {intl.formatMessage(messages.heading)} + + {isRedesignEnabled() + ? intl.formatMessage(messages.headingRedesign) + : intl.formatMessage(messages.heading)} + diff --git a/app/javascript/mastodon/features/favourites/index.jsx b/app/javascript/mastodon/features/favourites/index.jsx deleted file mode 100644 index 0a82ecfc3bb..00000000000 --- a/app/javascript/mastodon/features/favourites/index.jsx +++ /dev/null @@ -1,104 +0,0 @@ -import PropTypes from 'prop-types'; - -import { defineMessages, FormattedMessage } from 'react-intl'; - -import { Helmet } from '@unhead/react/helmet'; - -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { connect } from 'react-redux'; - -import { debounce } from 'lodash'; - -import RefreshIcon from '@/material-icons/400-24px/refresh.svg?react'; -import { fetchFavourites, expandFavourites } from 'mastodon/actions/interactions'; -import { Account } from 'mastodon/components/account'; -import { Column } from 'mastodon/components/column'; -import { ColumnHeader } from '@/mastodon/components/column/header'; -import { Icon } from 'mastodon/components/icon'; -import { injectIntl } from '@/mastodon/components/intl'; -import { LoadingIndicator } from 'mastodon/components/loading_indicator'; -import ScrollableList from 'mastodon/components/scrollable_list'; - -const messages = defineMessages({ - refresh: { id: 'refresh', defaultMessage: 'Refresh' }, -}); - -const mapStateToProps = (state, props) => ({ - accountIds: state.getIn(['user_lists', 'favourited_by', props.params.statusId, 'items']), - hasMore: !!state.getIn(['user_lists', 'favourited_by', props.params.statusId, 'next']), - isLoading: state.getIn(['user_lists', 'favourited_by', props.params.statusId, 'isLoading'], true), -}); - -class Favourites extends ImmutablePureComponent { - - static propTypes = { - params: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - accountIds: ImmutablePropTypes.list, - hasMore: PropTypes.bool, - isLoading: PropTypes.bool, - multiColumn: PropTypes.bool, - intl: PropTypes.object.isRequired, - }; - - componentDidMount () { - if (!this.props.accountIds) { - this.props.dispatch(fetchFavourites(this.props.params.statusId)); - } - } - - handleRefresh = () => { - this.props.dispatch(fetchFavourites(this.props.params.statusId)); - }; - - handleLoadMore = debounce(() => { - this.props.dispatch(expandFavourites(this.props.params.statusId)); - }, 300, { leading: true }); - - render () { - const { intl, accountIds, hasMore, isLoading, multiColumn } = this.props; - - if (!accountIds) { - return ( - - - - ); - } - - const emptyMessage = ; - - return ( - - - )} - /> - - - {accountIds.map(id => - , - )} - - - - - - - ); - } - -} - -export default connect(mapStateToProps)(injectIntl(Favourites)); diff --git a/app/javascript/mastodon/features/favourites/index.tsx b/app/javascript/mastodon/features/favourites/index.tsx new file mode 100644 index 00000000000..5937ab56e05 --- /dev/null +++ b/app/javascript/mastodon/features/favourites/index.tsx @@ -0,0 +1,142 @@ +import { useCallback, useEffect } from 'react'; + +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; + +import { useParams } from 'react-router-dom'; + +import type { List as ImmutableList } from 'immutable'; + +import { ArrowClockwiseIcon } from '@phosphor-icons/react'; +import { Helmet } from '@unhead/react/helmet'; +import { useDebouncedCallback } from 'use-debounce'; + +import { ColumnHeader as LegacyColumnHeader } from '@/mastodon/components/column/header'; +import { + ColumnHeader, + ColumnHeaderButton, +} from '@/mastodon/components/column_header'; +import { useAppDispatch, useAppSelector } from '@/mastodon/store'; +import { isRedesignEnabled } from '@/mastodon/utils/environment'; +import RefreshIcon from '@/material-icons/400-24px/refresh.svg?react'; +import { + fetchFavourites, + expandFavourites, +} from 'mastodon/actions/interactions'; +import { Account } from 'mastodon/components/account'; +import { Column } from 'mastodon/components/column'; +import { Icon } from 'mastodon/components/icon'; +import { LoadingIndicator } from 'mastodon/components/loading_indicator'; +import ScrollableList from 'mastodon/components/scrollable_list'; + +const messages = defineMessages({ + title: { id: 'status.likes_title', defaultMessage: 'Post Likes' }, + refresh: { id: 'refresh', defaultMessage: 'Refresh' }, +}); + +const Favourites: React.FC<{ multiColumn?: boolean }> = ({ multiColumn }) => { + const dispatch = useAppDispatch(); + const intl = useIntl(); + + const { statusId } = useParams<{ statusId?: string }>(); + + const { accountIds, hasMore, isLoading } = useAppSelector((state) => ({ + accountIds: state.user_lists.getIn(['favourited_by', statusId, 'items']) as + | ImmutableList + | undefined, + hasMore: !!state.user_lists.getIn(['favourited_by', statusId, 'next']), + isLoading: state.user_lists.getIn( + ['favourited_by', statusId, 'isLoading'], + true, + ) as boolean, + })); + + useEffect(() => { + if (!accountIds) { + dispatch(fetchFavourites(statusId)); + } + }, [accountIds, dispatch, statusId]); + + const handleRefresh = useCallback(() => { + dispatch(fetchFavourites(statusId)); + }, [dispatch, statusId]); + + const handleLoadMore = useDebouncedCallback( + () => { + dispatch(expandFavourites(statusId)); + }, + 300, + { + leading: true, + }, + ); + + if (!accountIds) { + return ( + + + + ); + } + + const emptyMessage = ( + + ); + + return ( + + {isRedesignEnabled() ? ( + + {intl.formatMessage(messages.refresh)} + + } + /> + ) : ( + + + + } + /> + )} + + + {accountIds.map((id) => ( + + ))} + + + + + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export default Favourites; diff --git a/app/javascript/mastodon/features/ui/components/columns_area/redesign.module.scss b/app/javascript/mastodon/features/ui/components/columns_area/redesign.module.scss index 108bb1010b9..d1f77e35130 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area/redesign.module.scss +++ b/app/javascript/mastodon/features/ui/components/columns_area/redesign.module.scss @@ -51,20 +51,24 @@ container: column / inline-size; color: var(--color-text-primary); background-color: var(--color-bg-primary); - - @media ($mobile-menu-breakpoint < width < $no-gap-breakpoint) { - border-inline: 1px solid var(--color-border-primary); - } } .columnHeader { - background: var(--color-bg-blend-solid); + // Make bottom half of column header transparent to make the + // overlay gradient of the nested `ColumnHeader` component work + --column-header-gradient-start: 55%; + + background: linear-gradient( + to bottom, + var(--color-bg-blend-solid) var(--column-header-gradient-start), + transparent var(--column-header-gradient-start) + ); position: sticky; top: 0; z-index: 2; border-top: 0; - @media (width >= $no-gap-breakpoint) { + @media (width >= $mobile-menu-breakpoint) { border-top: 10px solid var(--color-bg-blend-solid); } @@ -74,3 +78,22 @@ background: var(--color-bg-primary); } } + +.content { + width: 100%; + height: 100%; + margin: 0 auto; + + :global(.column) { + width: 100%; + height: 100%; + padding: 0; + } + + :global(.scrollable) { + contain: content; + overflow: visible; + border: none; + border-radius: 0; + } +} diff --git a/app/javascript/mastodon/features/ui/components/columns_area/redesign.tsx b/app/javascript/mastodon/features/ui/components/columns_area/redesign.tsx index d948e430e15..5f838295f96 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area/redesign.tsx +++ b/app/javascript/mastodon/features/ui/components/columns_area/redesign.tsx @@ -15,7 +15,7 @@ import { useColumnsContext } from '../../util/columns_context'; import { MultiColumnContent } from './multi_column_content'; import classes from './redesign.module.scss'; -const TabsBarPortal = () => { +const TabsBarPortal: React.FC> = (props) => { const { setTabsBarElement } = useColumnsContext(); const setRef = useCallback( @@ -27,7 +27,7 @@ const TabsBarPortal = () => { [setTabsBarElement], ); - return
; + return
; }; export const ColumnsAreaRedesign: React.FC<{ @@ -47,11 +47,9 @@ export const ColumnsAreaRedesign: React.FC<{
-
- -
+ -
{children}
+
{children}
@@ -68,11 +66,9 @@ export const ColumnsAreaRedesign: React.FC<{ {isMobile ? : }
-
- -
+ -
{children}
+
{children}
); diff --git a/app/javascript/mastodon/features/ui/util/columns_context.tsx b/app/javascript/mastodon/features/ui/util/columns_context.tsx index e02918deb00..7ea8794c48b 100644 --- a/app/javascript/mastodon/features/ui/util/columns_context.tsx +++ b/app/javascript/mastodon/features/ui/util/columns_context.tsx @@ -16,7 +16,7 @@ export function useColumnsContext() { return useContext(ColumnsContext); } -export const ButtonInTabsBar: React.FC<{ +export const RenderIntoTabsBarPortal: React.FC<{ children: ReactElement | string | undefined; }> = ({ children }) => { const { multiColumn, tabsBarElement } = useColumnsContext(); diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 8f0f8664015..d81c0977a84 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -470,6 +470,7 @@ "column.other_collections": "Collections by {name}", "column.pins": "Pinned posts", "column.public": "Federated timeline", + "column.saved_posts": "Saved Posts", "column.your_collections": "Your Collections", "column_back_button.label": "Back", "column_header.hide_settings": "Hide settings", @@ -1335,6 +1336,7 @@ "status.filter": "Filter this post", "status.history.created": "{name} created {date}", "status.history.edited": "{name} edited {date}", + "status.likes_title": "Post Likes", "status.load_more": "Load more", "status.media.open": "Click to open", "status.media.show": "Click to show",