Redesign: Update column headers on Custom Feed pages & Followed Hashtags overview page (#40376)

This commit is contained in:
diondiondion
2026-09-03 16:33:04 +00:00
committed by GitHub
parent 2889142840
commit 373cbad1b5
8 changed files with 306 additions and 139 deletions

View File

@@ -7,7 +7,9 @@ import { isFulfilled } from '@reduxjs/toolkit';
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 TagIcon from '@/material-icons/400-24px/tag.svg?react';
import {
fetchFollowedHashtags,
@@ -97,14 +99,21 @@ const FollowedTags: React.FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
bindToDocument={!multiColumn}
label={intl.formatMessage(messages.heading)}
>
<ColumnHeader
icon='hashtag'
iconComponent={TagIcon}
title={intl.formatMessage(messages.heading)}
multiColumn={multiColumn}
showBackButton
scrollTopOnClick
/>
{isRedesignEnabled() ? (
<ColumnHeader
withBackButton
title={intl.formatMessage(messages.heading)}
/>
) : (
<LegacyColumnHeader
icon='hashtag'
iconComponent={TagIcon}
title={intl.formatMessage(messages.heading)}
multiColumn={multiColumn}
showBackButton
scrollTopOnClick
/>
)}
<ScrollableList
scrollKey='followed_tags'

View File

@@ -18,12 +18,16 @@ import { openModal } from 'mastodon/actions/modal';
import { connectListStream } from 'mastodon/actions/streaming';
import { expandListTimeline } from 'mastodon/actions/timelines';
import { Column } from '@/mastodon/components/column';
import { ColumnHeader } from '@/mastodon/components/column/header';
import { ColumnHeader as LegacyColumnHeader } from '@/mastodon/components/column/header';
import { Icon } from 'mastodon/components/icon';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
import StatusListContainer from 'mastodon/features/ui/containers/status_list_container';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import { isRedesignEnabled } from '@/mastodon/utils/environment';
import { ColumnHeader, ColumnSettingsMenu } from '@/mastodon/components/column_header';
import { MenuItem, MenuItemLink } from '@/mastodon/components/menu';
import { MultiColumnMenuItems } from '@/mastodon/components/column_header/multicolumn_settings';
const mapStateToProps = (state, props) => ({
list: state.getIn(['lists', props.params.id]),
@@ -126,36 +130,71 @@ class ListTimeline extends PureComponent {
return (
<Column bindToDocument={!multiColumn} label={title}>
<ColumnHeader
icon='list-ul'
iconComponent={ListAltIcon}
active={hasUnread}
title={title}
onPin={this.handlePin}
onMove={this.handleMove}
pinned={pinned}
multiColumn={multiColumn}
scrollTopOnClick
>
<div className='column-settings'>
<section className='column-header__links'>
<Link to={`/lists/${id}/edit`} className='text-btn column-header__setting-btn'>
<Icon id='pencil' icon={EditIcon} /> <FormattedMessage id='lists.edit' defaultMessage='Edit list' />
</Link>
{isRedesignEnabled() ? (
<ColumnHeader
title={title}
withBackButton={multiColumn && !pinned && 'auto'}
extraButtons={
<ColumnSettingsMenu
label={
<FormattedMessage id='custom_feed.options' defaultMessage='Feed Options' />
}
>
<MenuItemLink as='link' to={`/lists/${id}/edit`}>
<FormattedMessage id='custom_feed.edit' defaultMessage='Edit feed' />
</MenuItemLink>
<MenuItemLink as='link' to={`/lists/${id}/members`}>
<FormattedMessage id='custom_feed.manage_accounts' defaultMessage='Manage members' />
</MenuItemLink>
<MenuItem onClick={this.handleDeleteClick}>
<FormattedMessage id='custom_feed.delete' defaultMessage='Delete feed' />
</MenuItem>
{multiColumn &&
<MultiColumnMenuItems
withDivider
pinned={pinned}
onPin={this.handlePin}
onMove={this.handleMove}
/>
}
</ColumnSettingsMenu>
}
/>
) : (
<LegacyColumnHeader
icon='list-ul'
iconComponent={ListAltIcon}
active={hasUnread}
title={title}
onPin={this.handlePin}
onMove={this.handleMove}
pinned={pinned}
multiColumn={multiColumn}
scrollTopOnClick
>
<div className='column-settings'>
<section className='column-header__links'>
<Link to={`/lists/${id}/edit`} className='text-btn column-header__setting-btn'>
<Icon id='pencil' icon={EditIcon} /> <FormattedMessage id='lists.edit' defaultMessage='Edit list' />
</Link>
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleDeleteClick}>
<Icon id='trash' icon={DeleteIcon} /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' />
</button>
</section>
</div>
</ColumnHeader>
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleDeleteClick}>
<Icon id='trash' icon={DeleteIcon} /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' />
</button>
</section>
</div>
</LegacyColumnHeader>
)}
<StatusListContainer
trackScroll={!pinned}
scrollKey={`list_timeline-${columnId}`}
timelineId={`list:${id}`}
onLoadMore={this.handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.list' defaultMessage='There is nothing in this list yet. When members of this list post new statuses, they will appear here.' />}
emptyMessage={isRedesignEnabled
? <FormattedMessage id='empty_column.custom_feed' defaultMessage='There is nothing in this custom feed yet. When members of this feed post new statuses, they will appear here.' />
: <FormattedMessage id='empty_column.list' defaultMessage='There is nothing in this list yet. When members of this list post new statuses, they will appear here.' />
}
bindToDocument={!multiColumn}
/>

View File

@@ -4,12 +4,18 @@ import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import { PlusIcon } from '@phosphor-icons/react';
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,
ColumnHeaderButton,
} from '@/mastodon/components/column_header';
import { NotSignedInIndicator } from '@/mastodon/components/not_signed_in_indicator';
import { useIdentity } from '@/mastodon/identity_context';
import { isRedesignEnabled } from '@/mastodon/utils/environment';
import AddIcon from '@/material-icons/400-24px/add.svg?react';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
@@ -24,7 +30,12 @@ import { useAppSelector, useAppDispatch } from 'mastodon/store';
const messages = defineMessages({
heading: { id: 'column.lists', defaultMessage: 'Lists' },
heading_redesign: {
id: 'column.custom_feeds',
defaultMessage: 'Custom Feeds',
},
create: { id: 'lists.create_list', defaultMessage: 'Create list' },
create_redesign: { id: 'custom_feeds.create', defaultMessage: 'Create' },
edit: { id: 'lists.edit', defaultMessage: 'Edit list' },
delete: { id: 'lists.delete', defaultMessage: 'Delete list' },
more: { id: 'status.more', defaultMessage: 'More' },
@@ -90,45 +101,78 @@ const Lists: React.FC<{
const emptyMessage = (
<>
<span>
<FormattedMessage
id='lists.no_lists_yet'
defaultMessage='No lists yet.'
/>
<br />
<FormattedMessage
id='lists.create_a_list_to_organize'
defaultMessage='Create a new list to organize your Home feed'
/>
</span>
{isRedesignEnabled() ? (
<span>
<FormattedMessage
id='custom_feeds.no_custom_feeds_yet'
defaultMessage='No custom feeds yet.'
/>
<br />
<FormattedMessage
id='custom_feeds.create_a_list_to_organize'
defaultMessage='Create a new custom feed to organize your Following feed.'
/>
</span>
) : (
<span>
<FormattedMessage
id='lists.no_lists_yet'
defaultMessage='No lists yet.'
/>
<br />
<FormattedMessage
id='lists.create_a_list_to_organize'
defaultMessage='Create a new list to organize your Home feed'
/>
</span>
)}
<SquigglyArrow className='empty-column-indicator__arrow' />
</>
);
const title = intl.formatMessage(
isRedesignEnabled() ? messages.heading_redesign : messages.heading,
);
return (
<Column
bindToDocument={!multiColumn}
label={intl.formatMessage(messages.heading)}
>
<ColumnHeader
title={intl.formatMessage(messages.heading)}
icon='list-ul'
iconComponent={ListAltIcon}
multiColumn={multiColumn}
extraButton={
signedIn && (
<Link
<Column bindToDocument={!multiColumn} label={title}>
{isRedesignEnabled() ? (
<ColumnHeader
withBackButton
title={title}
extraButtons={
<ColumnHeaderButton
showTextOnDesktop
variant='solid'
icon={PlusIcon}
as='link'
to='/lists/new'
className='column-header__button'
title={intl.formatMessage(messages.create)}
aria-label={intl.formatMessage(messages.create)}
>
<Icon id='plus' icon={AddIcon} />
</Link>
)
}
/>
{intl.formatMessage(messages.create_redesign)}
</ColumnHeaderButton>
}
/>
) : (
<LegacyColumnHeader
title={title}
icon='list-ul'
iconComponent={ListAltIcon}
multiColumn={multiColumn}
extraButton={
signedIn && (
<Link
to='/lists/new'
className='column-header__button'
title={intl.formatMessage(messages.create)}
aria-label={intl.formatMessage(messages.create)}
>
<Icon id='plus' icon={AddIcon} />
</Link>
)
}
/>
)}
<ScrollableList
scrollKey='lists'
@@ -145,7 +189,7 @@ const Lists: React.FC<{
</ScrollableList>
<Helmet>
<title>{intl.formatMessage(messages.heading)}</title>
<title>{title}</title>
<meta name='robots' content='noindex' />
</Helmet>
</Column>

View File

@@ -7,8 +7,10 @@ import { useParams, Link } from 'react-router-dom';
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 { ColumnSearchHeader } from '@/mastodon/components/column/search_header';
import { ColumnHeader } from '@/mastodon/components/column_header';
import { isRedesignEnabled } from '@/mastodon/utils/environment';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import SquigglyArrow from '@/svg-icons/squiggly_arrow.svg?react';
import { fetchRelationships } from 'mastodon/actions/accounts';
@@ -33,7 +35,7 @@ import { useSearchAccounts } from 'mastodon/hooks/useSearchAccounts';
import { me } from 'mastodon/initial_state';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
export const messages = defineMessages({
const messagesLegacy = defineMessages({
manageMembers: {
id: 'column.list_members',
defaultMessage: 'Manage list members',
@@ -48,6 +50,21 @@ export const messages = defineMessages({
back: { id: 'column_back_button.label', defaultMessage: 'Back' },
});
const messagesRedesign = defineMessages({
manageMembers: {
id: 'custom_feeds.manage_accounts_title',
defaultMessage: 'Manage Feed Members',
},
enterSearch: {
id: 'custom_feeds.add_to_feed',
defaultMessage: 'Add to feed',
},
});
const messages = isRedesignEnabled()
? { ...messagesLegacy, ...messagesRedesign }
: messagesLegacy;
type Mode = 'remove' | 'add';
const AccountItem: React.FC<{
@@ -232,13 +249,20 @@ const ListMembers: React.FC<{
bindToDocument={!multiColumn}
label={intl.formatMessage(messages.manageMembers)}
>
<ColumnHeader
title={intl.formatMessage(messages.manageMembers)}
icon='list-ul'
iconComponent={ListAltIcon}
multiColumn={multiColumn}
showBackButton
/>
{isRedesignEnabled() ? (
<ColumnHeader
withBackButton
title={intl.formatMessage(messages.manageMembers)}
/>
) : (
<LegacyColumnHeader
title={intl.formatMessage(messages.manageMembers)}
icon='list-ul'
iconComponent={ListAltIcon}
multiColumn={multiColumn}
showBackButton
/>
)}
<ColumnSearchHeader
placeholder={intl.formatMessage(messages.placeholder)}

View File

@@ -9,9 +9,11 @@ import { isFulfilled } from '@reduxjs/toolkit';
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 { NotSignedInIndicator } from '@/mastodon/components/not_signed_in_indicator';
import { useIdentity } from '@/mastodon/identity_context';
import { isRedesignEnabled } from '@/mastodon/utils/environment';
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import { fetchList } from 'mastodon/actions/lists';
@@ -31,13 +33,57 @@ import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import type { List } from 'mastodon/models/list';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
import { messages as membersMessages } from './members';
const messages = defineMessages({
const messagesLegacy = defineMessages({
edit: { id: 'column.edit_list', defaultMessage: 'Edit list' },
create: { id: 'column.create_list', defaultMessage: 'Create list' },
manageMembers: {
id: 'column.list_members',
defaultMessage: 'Manage list members',
},
nameFieldLabel: { id: 'lists.list_name', defaultMessage: 'List name' },
showRepliesTo: {
id: 'lists.show_replies_to',
defaultMessage: 'Include replies from list members to',
},
replyPolicyList: {
id: 'lists.replies_policy.list',
defaultMessage: 'Members of the list',
},
exclusiveHint: {
id: 'lists.exclusive_hint',
defaultMessage:
'If someone is on this list, hide them in your Home feed to avoid seeing their posts twice.',
},
});
const messagesRedesign = defineMessages({
edit: { id: 'column.edit_custom_feed', defaultMessage: 'Edit Custom Feed' },
create: {
id: 'column.create_custom_feed',
defaultMessage: 'Create Custom Feed',
},
manageMembers: {
id: 'custom_feeds.manage_accounts',
defaultMessage: 'Manage feed members',
},
nameFieldLabel: { id: 'custom_feeds.feed_name', defaultMessage: 'Feed name' },
showRepliesTo: {
id: 'custom_feeds.show_replies_to',
defaultMessage: 'Include replies from feed members to',
},
replyPolicyList: {
id: 'custom_feeds.replies_policy.members',
defaultMessage: 'Members of the feed',
},
exclusiveHint: {
id: 'custom_feeds.exclusive_hint',
defaultMessage:
'If someone is in this custom feed, hide them in your Home feed to avoid seeing their posts twice.',
},
});
const messages = isRedesignEnabled() ? messagesRedesign : messagesLegacy;
const MembersLink: React.FC<{
id: string;
}> = ({ id }) => {
@@ -60,7 +106,7 @@ const MembersLink: React.FC<{
<Link to={`/lists/${id}/members`} className='app-form__link'>
<div className='app-form__link__text'>
<strong>
{intl.formatMessage(membersMessages.manageMembers)}
{intl.formatMessage(messages.manageMembers)}
<Icon id='chevron_right' icon={ChevronRightIcon} />
</strong>
<FormattedMessage
@@ -82,6 +128,7 @@ const MembersLink: React.FC<{
const NewList: React.FC<{ list?: List | null }> = ({ list }) => {
const dispatch = useAppDispatch();
const history = useHistory();
const intl = useIntl();
const {
id,
@@ -158,9 +205,7 @@ const NewList: React.FC<{ list?: List | null }> = ({ list }) => {
<TextInputField
required
maxLength={30}
label={
<FormattedMessage id='lists.list_name' defaultMessage='List name' />
}
label={intl.formatMessage(messages.nameFieldLabel)}
value={title}
onChange={handleTitleChange}
id='list_title'
@@ -169,34 +214,26 @@ const NewList: React.FC<{ list?: List | null }> = ({ list }) => {
<div className='fields-group'>
<SelectField
label={
<FormattedMessage
id='lists.show_replies_to'
defaultMessage='Include replies from list members to'
/>
}
label={intl.formatMessage(messages.showRepliesTo)}
value={repliesPolicy}
onChange={handleRepliesPolicyChange}
id='list_replies_policy'
>
<FormattedMessage
id='lists.replies_policy.none'
defaultMessage='No one'
>
{(msg) => <option value='none'>{msg}</option>}
</FormattedMessage>
<FormattedMessage
id='lists.replies_policy.list'
defaultMessage='Members of the list'
>
{(msg) => <option value='list'>{msg}</option>}
</FormattedMessage>
<FormattedMessage
id='lists.replies_policy.followed'
defaultMessage='Any followed user'
>
{(msg) => <option value='followed'>{msg}</option>}
</FormattedMessage>
<option value='none'>
<FormattedMessage
id='lists.replies_policy.none'
defaultMessage='No one'
/>
</option>
<option value='list'>
{intl.formatMessage(messages.replyPolicyList)}
</option>
<option value='followed'>
<FormattedMessage
id='lists.replies_policy.followed'
defaultMessage='Any followed user'
/>
</option>
</SelectField>
</div>
@@ -217,10 +254,7 @@ const NewList: React.FC<{ list?: List | null }> = ({ list }) => {
/>
</strong>
<span className='hint'>
<FormattedMessage
id='lists.exclusive_hint'
defaultMessage='If someone is on this list, hide them in your Home feed to avoid seeing their posts twice.'
/>
{intl.formatMessage(messages.exclusiveHint)}
</span>
</div>
@@ -265,19 +299,21 @@ const NewListWrapper: React.FC<{
}, [dispatch, signedIn, id]);
const isLoading = id && !list;
const title = intl.formatMessage(id ? messages.edit : messages.create);
return (
<Column
bindToDocument={!multiColumn}
label={intl.formatMessage(id ? messages.edit : messages.create)}
>
<ColumnHeader
title={intl.formatMessage(id ? messages.edit : messages.create)}
icon='list-ul'
iconComponent={ListAltIcon}
multiColumn={multiColumn}
showBackButton
/>
<Column bindToDocument={!multiColumn} label={title}>
{isRedesignEnabled() ? (
<ColumnHeader withBackButton title={title} />
) : (
<LegacyColumnHeader
title={title}
icon='list-ul'
iconComponent={ListAltIcon}
multiColumn={multiColumn}
showBackButton
/>
)}
<div className='scrollable'>
{!signedIn ? (
@@ -290,9 +326,7 @@ const NewListWrapper: React.FC<{
</div>
<Helmet>
<title>
{intl.formatMessage(id ? messages.edit : messages.create)}
</title>
<title>{title}</title>
<meta name='robots' content='noindex' />
</Helmet>
</Column>

View File

@@ -157,7 +157,7 @@ export const RedesignNavigationPanel: React.FC<{
label: (
<FormattedMessage
id='tabs_bar.create_custom_feed'
defaultMessage='Create feed'
defaultMessage='Create'
/>
),
link: '/lists/new',
@@ -188,21 +188,18 @@ export const RedesignNavigationPanel: React.FC<{
defaultMessage='Followed Hashtags'
/>
}
action={{
label: (
<FormattedMessage
id='tabs_bar.followed_tags_view_all'
defaultMessage='View all'
/>
),
link: '/followed_tags',
}}
>
{followedHashtags.slice(0, 4).map((tag) => (
<NavigationLink key={tag.name} to={`/tags/${tag.name}`}>
#{tag.name}
</NavigationLink>
))}
<NavigationLink key='view-all' to='/followed_tags'>
<FormattedMessage
id='tabs_bar.followed_tags_view_all'
defaultMessage='View all'
/>
</NavigationLink>
</ListSection>
)}
</ul>

View File

@@ -453,10 +453,13 @@
"column.blocks": "Blocked users",
"column.bookmarks": "Bookmarks",
"column.community": "Local timeline",
"column.create_custom_feed": "Create Custom Feed",
"column.create_list": "Create list",
"column.custom_feeds": "Custom Feeds",
"column.direct": "Private mentions",
"column.directory": "Browse profiles",
"column.domain_blocks": "Blocked domains",
"column.edit_custom_feed": "Edit Custom Feed",
"column.edit_list": "Edit list",
"column.favourites": "Favorites",
"column.firehose": "Live feeds",
@@ -666,6 +669,20 @@
"copy_icon_button.copy_this_text": "Copy link to clipboard",
"copypaste.copied": "Copied",
"copypaste.copy_to_clipboard": "Copy to clipboard",
"custom_feed.delete": "Delete feed",
"custom_feed.edit": "Edit feed",
"custom_feed.manage_accounts": "Manage members",
"custom_feed.options": "Feed Options",
"custom_feeds.add_to_feed": "Add to feed",
"custom_feeds.create": "Create",
"custom_feeds.create_a_list_to_organize": "Create a new custom feed to organize your Following feed.",
"custom_feeds.exclusive_hint": "If someone is in this custom feed, hide them in your Home feed to avoid seeing their posts twice.",
"custom_feeds.feed_name": "Feed name",
"custom_feeds.manage_accounts": "Manage feed members",
"custom_feeds.manage_accounts_title": "Manage Feed Members",
"custom_feeds.no_custom_feeds_yet": "No custom feeds yet.",
"custom_feeds.replies_policy.members": "Members of the feed",
"custom_feeds.show_replies_to": "Include replies from feed members to",
"custom_homepage.about": "About",
"custom_homepage.about_this_server": "About this server",
"custom_homepage.administered_by": "Administered by",
@@ -733,6 +750,7 @@
"empty_column.collections.featured_in_undiscoverable": "In order for people to add you to collections, you need to allow featuring in discovery experiences from <link>Preferences > Privacy and reach</link>",
"empty_column.collections_self": "You have not created any collections yet.",
"empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
"empty_column.custom_feed": "There is nothing in this custom feed yet. When members of this feed post new statuses, they will appear here.",
"empty_column.direct": "You don't have any private mentions yet. When you send or receive one, it will show up here.",
"empty_column.disabled_feed": "This feed has been disabled by your server administrators.",
"empty_column.domain_blocks": "There are no blocked domains yet.",
@@ -1419,7 +1437,7 @@
"subscribed_languages.save": "Save changes",
"subscribed_languages.target": "Change subscribed languages for {target}",
"tab_bar.messages": "Messages",
"tabs_bar.create_custom_feed": "Create feed",
"tabs_bar.create_custom_feed": "Create",
"tabs_bar.custom_feeds": "Custom Feeds",
"tabs_bar.custom_feeds_empty": "You have no custom feeds yet.",
"tabs_bar.explore": "Explore",

View File

@@ -11019,12 +11019,14 @@ noscript {
display: flex;
gap: 12px;
align-items: center;
border: 1px solid var(--color-border-primary);
border-top: 0;
border-bottom: 0;
border-inline: 1px solid var(--color-border-primary);
padding: 16px;
padding-bottom: 8px;
[data-redesign='true'] & {
border-inline: none;
}
input {
background: var(--color-bg-secondary);
border: 1px solid var(--color-border-primary);