Redesign: Add "custom feeds" and "followed hashtags" sections to main navigation (#40248)

This commit is contained in:
diondiondion
2026-08-24 09:34:22 +00:00
committed by GitHub
parent 45db0baa44
commit 68fb4de0d0
8 changed files with 185 additions and 8 deletions

View File

@@ -43,6 +43,8 @@
// Override default anchor styles
a.base {
text-decoration: none;
&:focus {
border-radius: var(--radius-round);
}

View File

@@ -1,5 +1,3 @@
@use '@/styles/mastodon/mixins';
.root {
display: flex;
align-items: center;

View File

@@ -3,6 +3,7 @@
.root {
position: sticky;
top: 0;
z-index: 1;
display: flex;
align-items: center;
padding: var(--space-xl) var(--space-lg);

View File

@@ -1,4 +1,4 @@
import { useCallback } from 'react';
import { useCallback, useEffect } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
@@ -6,20 +6,25 @@ import {
PenNibIcon,
HouseIcon,
MagnifyingGlassIcon,
RssSimpleIcon,
BellIcon,
ChatCircleIcon,
BookmarkSimpleIcon,
} from '@phosphor-icons/react';
import FediIcon from '@/images/icons/icon_fediverse.svg?react';
import { fetchLists } from '@/mastodon/actions/lists';
import { fetchFollowedHashtags } from '@/mastodon/actions/tags_typed';
import { useIdentity } from '@/mastodon/identity_context';
import { openNewComposer } from '@/mastodon/reducers/slices/composer';
import { getOrderedLists } from '@/mastodon/selectors/lists';
import { selectUnreadNotificationGroupsCount } from '@/mastodon/selectors/notifications';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import { NavigationAccountCardAndMenu } from './account_card_and_menu';
import { NavigationFooterLinks } from './footer_links';
import { NavigationHeader } from './header';
import { ListSection } from './list_section';
import { NavigationLink } from './navigation_link';
import classes from './styles.module.scss';
@@ -32,6 +37,32 @@ const messages = defineMessages({
},
});
function useCustomFeeds() {
const dispatch = useAppDispatch();
const customFeeds = useAppSelector((state) => getOrderedLists(state));
useEffect(() => {
void dispatch(fetchLists());
}, [dispatch]);
return {
customFeeds,
};
}
function useFollowedHashtags() {
const dispatch = useAppDispatch();
const { tags, stale } = useAppSelector((state) => state.followedTags);
useEffect(() => {
if (stale) {
void dispatch(fetchFollowedHashtags());
}
}, [dispatch, stale]);
return { followedHashtags: tags };
}
export const RedesignNavigationPanel: React.FC<{ siteName?: string }> = ({
siteName,
}) => {
@@ -46,6 +77,9 @@ export const RedesignNavigationPanel: React.FC<{ siteName?: string }> = ({
dispatch(openNewComposer({ type: 'post' }));
}, [dispatch]);
const { customFeeds } = useCustomFeeds();
const { followedHashtags } = useFollowedHashtags();
return (
<nav
className={classes.root}
@@ -75,12 +109,75 @@ export const RedesignNavigationPanel: React.FC<{ siteName?: string }> = ({
defaultMessage='Explore'
/>
</NavigationLink>
<NavigationLink to='/public/local' iconComponent={FediIcon}>
<NavigationLink
withSpaceAfter
to='/public/local'
iconComponent={FediIcon}
>
<FormattedMessage
id='tabs_bar.fediverse_feeds'
defaultMessage='Fediverse Feeds'
/>
</NavigationLink>
<ListSection
title={
<FormattedMessage
id='tabs_bar.custom_feeds'
defaultMessage='Custom feeds'
/>
}
action={{
label: (
<FormattedMessage
id='tabs_bar.create_custom_feed'
defaultMessage='Create feed'
/>
),
link: '/lists/new',
}}
emptyMessage={
<FormattedMessage
id='tabs_bar.custom_feeds_empty'
defaultMessage='You have no custom feeds yet.'
/>
}
>
{customFeeds.map((feed) => (
<NavigationLink
key={feed.id}
to={`/lists/${feed.id}`}
iconComponent={RssSimpleIcon}
>
{feed.title}
</NavigationLink>
))}
</ListSection>
{followedHashtags.length > 0 && (
<ListSection
title={
<FormattedMessage
id='navigation_bar.followed_tags'
defaultMessage='Followed hashtags'
/>
}
action={{
label: (
<FormattedMessage
id='navigation_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>
))}
</ListSection>
)}
</ul>
<footer className={classes.footer}>
<ul className={classes.footerNav}>

View File

@@ -0,0 +1,31 @@
@use '@/styles/mastodon/mixins';
.root {
&:not(:last-child) {
margin-bottom: var(--space-md);
}
}
.titleWrapper {
display: flex;
align-items: center;
padding-inline: var(--space-xs);
padding-block: calc(var(--space-xs) - var(--space-3xs));
}
.title {
@include mixins.type-label-sm;
color: var(--color-text-secondary);
cursor: default;
}
.action {
margin-inline-start: auto;
}
.emptyState {
@include mixins.type-label-sm;
padding-inline: var(--space-xs);
}

View File

@@ -0,0 +1,42 @@
import type { ReactNode } from 'react';
import { Button } from '@/mastodon/components/button/redesign';
import type { MastodonLocationDescriptor } from '@/mastodon/components/router';
import { hasReactChildren } from '@/mastodon/utils/has_react_children';
import classes from './list_section.module.scss';
export const ListSection: React.FC<{
title: ReactNode;
action?: {
label: ReactNode;
link: MastodonLocationDescriptor;
};
children: ReactNode;
emptyMessage?: ReactNode;
}> = ({ title, action, children, emptyMessage }) => {
const hasContent = hasReactChildren(children);
return (
<li className={classes.root}>
<div className={classes.titleWrapper}>
<span className={classes.title}>{title}</span>
{action && (
<Button
as='link'
to={action.link}
size='xs'
className={classes.action}
>
{action.label}
</Button>
)}
</div>
{hasContent ? (
<ul>{children}</ul>
) : (
emptyMessage && <div className={classes.emptyState}>{emptyMessage}</div>
)}
</li>
);
};

View File

@@ -12,7 +12,7 @@ import classes from './navigation_link.module.scss';
type NavigationLinkProps = {
stacked?: boolean;
iconComponent: Icon | React.FC<SVGProps<SVGSVGElement>>;
iconComponent?: Icon | React.FC<SVGProps<SVGSVGElement>>;
badgeCount?: number;
withSpaceAfter?: boolean;
} & (
@@ -55,9 +55,11 @@ export const NavigationLink: React.FC<NavigationLinkProps> = ({
{...otherProps}
className={classNames(classes.link, stacked && classes.linkStacked)}
>
<span className={classes.icon}>
<IconComp size={20} weight={isActive ? 'fill' : undefined} />
</span>
{IconComp && (
<span className={classes.icon}>
<IconComp size={20} weight={isActive ? 'fill' : undefined} />
</span>
)}
<span className={classes.label}>{children}</span>
{badgeCount > 0 && (
<Badge

View File

@@ -986,6 +986,7 @@
"navigation_bar.filters": "Muted words",
"navigation_bar.follow_requests": "Follow requests",
"navigation_bar.followed_tags": "Followed hashtags",
"navigation_bar.followed_tags_view_all": "View all",
"navigation_bar.followers_and_following": "Followers & Following",
"navigation_bar.follows_and_followers": "Follows and followers",
"navigation_bar.import_export": "Import and export",
@@ -1398,6 +1399,9 @@
"subscribed_languages.lead": "Only posts in selected languages will appear on your home and list timelines after the change. Select none to receive posts in all languages.",
"subscribed_languages.save": "Save changes",
"subscribed_languages.target": "Change subscribed languages for {target}",
"tabs_bar.create_custom_feed": "Create feed",
"tabs_bar.custom_feeds": "Custom feeds",
"tabs_bar.custom_feeds_empty": "You have no custom feeds yet.",
"tabs_bar.explore": "Explore",
"tabs_bar.fediverse_feeds": "Fediverse Feeds",
"tabs_bar.home": "Home",