Change order of items in navigation panel in web UI (#35029)

This commit is contained in:
Eugen Rochko 2025-06-16 17:06:33 +02:00 committed by GitHub
commit 7c4393e719
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 625 additions and 452 deletions

View file

@ -0,0 +1,85 @@
import { useState, useCallback, useId } from 'react';
import KeyboardArrowDownIcon from '@/material-icons/400-24px/keyboard_arrow_down.svg?react';
import KeyboardArrowUpIcon from '@/material-icons/400-24px/keyboard_arrow_up.svg?react';
import type { IconProp } from 'mastodon/components/icon';
import { IconButton } from 'mastodon/components/icon_button';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { ColumnLink } from 'mastodon/features/ui/components/column_link';
export const CollapsiblePanel: React.FC<{
children: React.ReactNode[];
to: string;
title: string;
collapseTitle: string;
expandTitle: string;
icon: string;
iconComponent: IconProp;
activeIconComponent?: IconProp;
loading?: boolean;
}> = ({
children,
to,
icon,
iconComponent,
activeIconComponent,
title,
collapseTitle,
expandTitle,
loading,
}) => {
const [expanded, setExpanded] = useState(false);
const accessibilityId = useId();
const handleClick = useCallback(() => {
setExpanded((value) => !value);
}, [setExpanded]);
return (
<div className='navigation-panel__list-panel'>
<div className='navigation-panel__list-panel__header'>
<ColumnLink
transparent
to={to}
icon={icon}
iconComponent={iconComponent}
activeIconComponent={activeIconComponent}
text={title}
id={`${accessibilityId}-title`}
/>
{(loading || children.length > 0) && (
<>
<div className='navigation-panel__list-panel__header__sep' />
<IconButton
icon='down'
expanded={expanded}
iconComponent={
loading
? LoadingIndicator
: expanded
? KeyboardArrowUpIcon
: KeyboardArrowDownIcon
}
title={expanded ? collapseTitle : expandTitle}
onClick={handleClick}
aria-controls={`${accessibilityId}-content`}
/>
</>
)}
</div>
{children.length > 0 && expanded && (
<div
className='navigation-panel__list-panel__items'
role='region'
id={`${accessibilityId}-content`}
aria-labelledby={`${accessibilityId}-title`}
>
{children}
</div>
)}
</div>
);
};

View file

@ -0,0 +1,88 @@
import { useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import { openModal } from 'mastodon/actions/modal';
import {
disabledAccountId,
movedToAccountId,
domain,
} from 'mastodon/initial_state';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
export const DisabledAccountBanner: React.FC = () => {
const disabledAccount = useAppSelector((state) =>
disabledAccountId ? state.accounts.get(disabledAccountId) : undefined,
);
const movedToAccount = useAppSelector((state) =>
movedToAccountId ? state.accounts.get(movedToAccountId) : undefined,
);
const dispatch = useAppDispatch();
const handleLogOutClick = useCallback(
(e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT', modalProps: {} }));
return false;
},
[dispatch],
);
const disabledAccountLink = (
<Link to={`/@${disabledAccount?.acct}`}>
{disabledAccount?.acct}@{domain}
</Link>
);
return (
<div className='sign-in-banner'>
<p>
{movedToAccount ? (
<FormattedMessage
id='moved_to_account_banner.text'
defaultMessage='Your account {disabledAccount} is currently disabled because you moved to {movedToAccount}.'
values={{
disabledAccount: disabledAccountLink,
movedToAccount: (
<Link to={`/@${movedToAccount.acct}`}>
{movedToAccount.acct.includes('@')
? movedToAccount.acct
: `${movedToAccount.acct}@${domain}`}
</Link>
),
}}
/>
) : (
<FormattedMessage
id='disabled_account_banner.text'
defaultMessage='Your account {disabledAccount} is currently disabled.'
values={{
disabledAccount: disabledAccountLink,
}}
/>
)}
</p>
<a href='/auth/edit' className='button button--block'>
<FormattedMessage
id='disabled_account_banner.account_settings'
defaultMessage='Account settings'
/>
</a>
<button
type='button'
className='button button--block button-tertiary'
onClick={handleLogOutClick}
>
<FormattedMessage
id='confirmations.logout.confirm'
defaultMessage='Log out'
/>
</button>
</div>
);
};

View file

@ -0,0 +1,69 @@
import { useEffect, useState } from 'react';
import { useIntl, defineMessages } from 'react-intl';
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
import { apiGetFollowedTags } from 'mastodon/api/tags';
import type { ApiHashtagJSON } from 'mastodon/api_types/tags';
import { ColumnLink } from 'mastodon/features/ui/components/column_link';
import { CollapsiblePanel } from './collapsible_panel';
const messages = defineMessages({
followedTags: {
id: 'navigation_bar.followed_tags',
defaultMessage: 'Followed hashtags',
},
expand: {
id: 'navigation_panel.expand_followed_tags',
defaultMessage: 'Expand followed hashtags menu',
},
collapse: {
id: 'navigation_panel.collapse_followed_tags',
defaultMessage: 'Collapse followed hashtags menu',
},
});
export const FollowedTagsPanel: React.FC = () => {
const intl = useIntl();
const [tags, setTags] = useState<ApiHashtagJSON[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
void apiGetFollowedTags(undefined, 4)
.then(({ tags }) => {
setTags(tags);
setLoading(false);
return '';
})
.catch(() => {
setLoading(false);
});
}, [setLoading, setTags]);
return (
<CollapsiblePanel
to='/followed_tags'
icon='hashtag'
iconComponent={TagIcon}
title={intl.formatMessage(messages.followedTags)}
collapseTitle={intl.formatMessage(messages.collapse)}
expandTitle={intl.formatMessage(messages.expand)}
loading={loading}
>
{tags.map((tag) => (
<ColumnLink
icon='hashtag'
key={tag.name}
iconComponent={TagIcon}
text={`#${tag.name}`}
to={`/tags/${tag.name}`}
transparent
/>
))}
</CollapsiblePanel>
);
};

View file

@ -0,0 +1,66 @@
import { useEffect, useState } from 'react';
import { useIntl, defineMessages } from 'react-intl';
import ListAltActiveIcon from '@/material-icons/400-24px/list_alt-fill.svg?react';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import { fetchLists } from 'mastodon/actions/lists';
import { ColumnLink } from 'mastodon/features/ui/components/column_link';
import { getOrderedLists } from 'mastodon/selectors/lists';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
import { CollapsiblePanel } from './collapsible_panel';
const messages = defineMessages({
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
expand: {
id: 'navigation_panel.expand_lists',
defaultMessage: 'Expand list menu',
},
collapse: {
id: 'navigation_panel.collapse_lists',
defaultMessage: 'Collapse list menu',
},
});
export const ListPanel: React.FC = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const lists = useAppSelector((state) => getOrderedLists(state));
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
void dispatch(fetchLists()).then(() => {
setLoading(false);
return '';
});
}, [dispatch, setLoading]);
return (
<CollapsiblePanel
to='/lists'
icon='list-ul'
iconComponent={ListAltIcon}
activeIconComponent={ListAltActiveIcon}
title={intl.formatMessage(messages.lists)}
collapseTitle={intl.formatMessage(messages.collapse)}
expandTitle={intl.formatMessage(messages.expand)}
loading={loading}
>
{lists.map((list) => (
<ColumnLink
icon='list-ul'
key={list.id}
iconComponent={ListAltIcon}
activeIconComponent={ListAltActiveIcon}
text={list.title}
to={`/lists/${list.id}`}
transparent
/>
))}
</CollapsiblePanel>
);
};

View file

@ -0,0 +1,117 @@
import { useMemo } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import { openModal } from 'mastodon/actions/modal';
import { Dropdown } from 'mastodon/components/dropdown_menu';
import { Icon } from 'mastodon/components/icon';
import { useIdentity } from 'mastodon/identity_context';
import type { MenuItem } from 'mastodon/models/dropdown_menu';
import { canManageReports, canViewAdminDashboard } from 'mastodon/permissions';
import { useAppDispatch } from 'mastodon/store';
const messages = defineMessages({
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
domainBlocks: {
id: 'navigation_bar.domain_blocks',
defaultMessage: 'Blocked domains',
},
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' },
administration: {
id: 'navigation_bar.administration',
defaultMessage: 'Administration',
},
moderation: { id: 'navigation_bar.moderation', defaultMessage: 'Moderation' },
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
automatedDeletion: {
id: 'navigation_bar.automated_deletion',
defaultMessage: 'Automated post deletion',
},
accountSettings: {
id: 'navigation_bar.account_settings',
defaultMessage: 'Password and security',
},
importExport: {
id: 'navigation_bar.import_export',
defaultMessage: 'Import and export',
},
privacyAndReach: {
id: 'navigation_bar.privacy_and_reach',
defaultMessage: 'Privacy and reach',
},
});
export const MoreLink: React.FC = () => {
const intl = useIntl();
const { permissions } = useIdentity();
const dispatch = useAppDispatch();
const menu = useMemo(() => {
const arr: MenuItem[] = [
{ text: intl.formatMessage(messages.filters), href: '/filters' },
{ text: intl.formatMessage(messages.mutes), to: '/mutes' },
{ text: intl.formatMessage(messages.blocks), to: '/blocks' },
{
text: intl.formatMessage(messages.domainBlocks),
to: '/domain_blocks',
},
];
arr.push(
null,
{
href: '/settings/privacy',
text: intl.formatMessage(messages.privacyAndReach),
},
{
href: '/statuses_cleanup',
text: intl.formatMessage(messages.automatedDeletion),
},
{
href: '/auth/edit',
text: intl.formatMessage(messages.accountSettings),
},
{
href: '/settings/export',
text: intl.formatMessage(messages.importExport),
},
);
if (canManageReports(permissions)) {
arr.push(null, {
href: '/admin/reports',
text: intl.formatMessage(messages.moderation),
});
}
if (canViewAdminDashboard(permissions)) {
arr.push({
href: '/admin/dashboard',
text: intl.formatMessage(messages.administration),
});
}
const handleLogoutClick = () => {
dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT', modalProps: {} }));
};
arr.push(null, {
text: intl.formatMessage(messages.logout),
action: handleLogoutClick,
});
return arr;
}, [intl, dispatch, permissions]);
return (
<Dropdown items={menu}>
<button className='column-link column-link--transparent'>
<Icon id='' icon={MoreHorizIcon} className='column-link__icon' />
<FormattedMessage id='navigation_bar.more' defaultMessage='More' />
</button>
</Dropdown>
);
};

View file

@ -0,0 +1,105 @@
import { useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import { openModal } from 'mastodon/actions/modal';
import { registrationsOpen, sso_redirect } from 'mastodon/initial_state';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
export const SignInBanner: React.FC = () => {
const dispatch = useAppDispatch();
const openClosedRegistrationsModal = useCallback(
() =>
dispatch(
openModal({ modalType: 'CLOSED_REGISTRATIONS', modalProps: {} }),
),
[dispatch],
);
let signupButton: React.ReactNode;
const signupUrl = useAppSelector(
(state) =>
(state.server.getIn(['server', 'registrations', 'url'], null) as
| string
| null) ?? '/auth/sign_up',
);
if (sso_redirect) {
return (
<div className='sign-in-banner'>
<p>
<strong>
<FormattedMessage
id='sign_in_banner.mastodon_is'
defaultMessage="Mastodon is the best way to keep up with what's happening."
/>
</strong>
</p>
<p>
<FormattedMessage
id='sign_in_banner.follow_anyone'
defaultMessage='Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.'
/>
</p>
<a
href={sso_redirect}
data-method='post'
className='button button--block button-tertiary'
>
<FormattedMessage
id='sign_in_banner.sso_redirect'
defaultMessage='Login or Register'
/>
</a>
</div>
);
}
if (registrationsOpen) {
signupButton = (
<a href={signupUrl} className='button button--block'>
<FormattedMessage
id='sign_in_banner.create_account'
defaultMessage='Create account'
/>
</a>
);
} else {
signupButton = (
<button
className='button button--block'
onClick={openClosedRegistrationsModal}
>
<FormattedMessage
id='sign_in_banner.create_account'
defaultMessage='Create account'
/>
</button>
);
}
return (
<div className='sign-in-banner'>
<p>
<strong>
<FormattedMessage
id='sign_in_banner.mastodon_is'
defaultMessage="Mastodon is the best way to keep up with what's happening."
/>
</strong>
</p>
<p>
<FormattedMessage
id='sign_in_banner.follow_anyone'
defaultMessage='Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.'
/>
</p>
{signupButton}
<a href='/auth/sign_in' className='button button--block button-tertiary'>
<FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Login' />
</a>
</div>
);
};

View file

@ -0,0 +1,57 @@
import { useEffect } from 'react';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import type { List as ImmutableList, Map as ImmutableMap } from 'immutable';
import { fetchTrendingHashtags } from 'mastodon/actions/trends';
import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag';
import { showTrends } from 'mastodon/initial_state';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
export const Trends: React.FC = () => {
const dispatch = useAppDispatch();
const trends = useAppSelector(
(state) =>
state.trends.getIn(['tags', 'items']) as ImmutableList<
ImmutableMap<string, unknown>
>,
);
useEffect(() => {
dispatch(fetchTrendingHashtags());
const refreshInterval = setInterval(() => {
dispatch(fetchTrendingHashtags());
}, 900 * 1000);
return () => {
clearInterval(refreshInterval);
};
}, [dispatch]);
if (!showTrends || trends.isEmpty()) {
return null;
}
return (
<div className='navigation-panel__portal'>
<div className='getting-started__trends'>
<h4>
<Link to={'/explore/tags'}>
<FormattedMessage
id='trends.trending_now'
defaultMessage='Trending now'
/>
</Link>
</h4>
{trends.take(4).map((hashtag) => (
<Hashtag key={hashtag.get('name') as string} hashtag={hashtag} />
))}
</div>
</div>
);
};

View file

@ -0,0 +1,446 @@
import { useEffect, useRef } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import classNames from 'classnames';
import { Link, useLocation } from 'react-router-dom';
import type { Map as ImmutableMap } from 'immutable';
import { animated, useSpring } from '@react-spring/web';
import { useDrag } from '@use-gesture/react';
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 HomeActiveIcon from '@/material-icons/400-24px/home-fill.svg?react';
import HomeIcon from '@/material-icons/400-24px/home.svg?react';
import InfoIcon from '@/material-icons/400-24px/info.svg?react';
import NotificationsActiveIcon from '@/material-icons/400-24px/notifications-fill.svg?react';
import NotificationsIcon from '@/material-icons/400-24px/notifications.svg?react';
import PersonAddActiveIcon from '@/material-icons/400-24px/person_add-fill.svg?react';
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';
import { Account } from 'mastodon/components/account';
import { IconWithBadge } from 'mastodon/components/icon_with_badge';
import { WordmarkLogo } from 'mastodon/components/logo';
import { Search } from 'mastodon/features/compose/components/search';
import { ColumnLink } from 'mastodon/features/ui/components/column_link';
import { useBreakpoint } from 'mastodon/features/ui/hooks/useBreakpoint';
import { useIdentity } from 'mastodon/identity_context';
import { timelinePreview, trendsEnabled, me } from 'mastodon/initial_state';
import { transientSingleColumn } from 'mastodon/is_mobile';
import { selectUnreadNotificationGroupsCount } from 'mastodon/selectors/notifications';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
import { DisabledAccountBanner } from './components/disabled_account_banner';
import { FollowedTagsPanel } from './components/followed_tags_panel';
import { ListPanel } from './components/list_panel';
import { MoreLink } from './components/more_link';
import { SignInBanner } from './components/sign_in_banner';
import { Trends } from './components/trends';
const messages = defineMessages({
home: { id: 'tabs_bar.home', defaultMessage: 'Home' },
notifications: {
id: 'tabs_bar.notifications',
defaultMessage: 'Notifications',
},
explore: { id: 'explore.title', defaultMessage: 'Trending' },
firehose: { id: 'column.firehose', defaultMessage: 'Live feeds' },
direct: { id: 'navigation_bar.direct', defaultMessage: 'Private mentions' },
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favorites' },
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
preferences: {
id: 'navigation_bar.preferences',
defaultMessage: 'Preferences',
},
followsAndFollowers: {
id: 'navigation_bar.follows_and_followers',
defaultMessage: 'Follows and followers',
},
about: { id: 'navigation_bar.about', defaultMessage: 'About' },
search: { id: 'navigation_bar.search', defaultMessage: 'Search' },
searchTrends: {
id: 'navigation_bar.search_trends',
defaultMessage: 'Search / Trending',
},
advancedInterface: {
id: 'navigation_bar.advanced_interface',
defaultMessage: 'Open in advanced web interface',
},
openedInClassicInterface: {
id: 'navigation_bar.opened_in_classic_interface',
defaultMessage:
'Posts, accounts, and other specific pages are opened by default in the classic web interface.',
},
followRequests: {
id: 'navigation_bar.follow_requests',
defaultMessage: 'Follow requests',
},
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
compose: { id: 'tabs_bar.publish', defaultMessage: 'New Post' },
});
const NotificationsLink = () => {
const count = useAppSelector(selectUnreadNotificationGroupsCount);
const intl = useIntl();
return (
<ColumnLink
key='notifications'
transparent
to='/notifications'
icon={
<IconWithBadge
id='bell'
icon={NotificationsIcon}
count={count}
className='column-link__icon'
/>
}
activeIcon={
<IconWithBadge
id='bell'
icon={NotificationsActiveIcon}
count={count}
className='column-link__icon'
/>
}
text={intl.formatMessage(messages.notifications)}
/>
);
};
const FollowRequestsLink: React.FC = () => {
const intl = useIntl();
const count = useAppSelector(
(state) =>
(
state.user_lists.getIn(['follow_requests', 'items']) as
| ImmutableMap<string, unknown>
| undefined
)?.size ?? 0,
);
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(fetchFollowRequests());
}, [dispatch]);
if (count === 0) {
return null;
}
return (
<ColumnLink
transparent
to='/follow_requests'
icon={
<IconWithBadge
id='user-plus'
icon={PersonAddIcon}
count={count}
className='column-link__icon'
/>
}
activeIcon={
<IconWithBadge
id='user-plus'
icon={PersonAddActiveIcon}
count={count}
className='column-link__icon'
/>
}
text={intl.formatMessage(messages.followRequests)}
/>
);
};
const ProfileCard: React.FC = () => {
if (!me) {
return null;
}
return (
<div className='navigation-bar'>
<Account id={me} minimal size={36} />
</div>
);
};
const isFirehoseActive = (
match: unknown,
{ pathname }: { pathname: string },
) => {
return !!match || pathname.startsWith('/public');
};
const MENU_WIDTH = 284;
export const NavigationPanel: React.FC = () => {
const intl = useIntl();
const { signedIn, disabledAccountId } = useIdentity();
const open = useAppSelector((state) => state.navigation.open);
const dispatch = useAppDispatch();
const openable = useBreakpoint('openable');
const showSearch = useBreakpoint('full');
const location = useLocation();
const overlayRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
dispatch(closeNavigation());
}, [dispatch, location]);
useEffect(() => {
const handleDocumentClick = (e: MouseEvent) => {
if (overlayRef.current && e.target === overlayRef.current) {
dispatch(closeNavigation());
}
};
const handleDocumentKeyUp = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
dispatch(closeNavigation());
}
};
document.addEventListener('click', handleDocumentClick);
document.addEventListener('keyup', handleDocumentKeyUp);
return () => {
document.removeEventListener('click', handleDocumentClick);
document.removeEventListener('keyup', handleDocumentKeyUp);
};
}, [dispatch]);
const [{ x }, spring] = useSpring(
() => ({
x: open ? 0 : MENU_WIDTH,
onRest: {
x({ value }: { value: number }) {
if (value === 0) {
dispatch(openNavigation());
} else if (value > 0) {
dispatch(closeNavigation());
}
},
},
}),
[open],
);
const bind = useDrag(
({ last, offset: [ox], velocity: [vx], direction: [dx], cancel }) => {
if (ox < -70) {
cancel();
}
if (last) {
if (ox > MENU_WIDTH / 2 || (vx > 0.5 && dx > 0)) {
void spring.start({ x: MENU_WIDTH });
} else {
void spring.start({ x: 0 });
}
} else {
void spring.start({ x: ox, immediate: true });
}
},
{
from: () => [x.get(), 0],
filterTaps: true,
bounds: { left: 0 },
rubberband: true,
},
);
const previouslyFocusedElementRef = useRef<HTMLElement | null>();
useEffect(() => {
if (open) {
const firstLink = document.querySelector<HTMLAnchorElement>(
'.navigation-panel__menu .column-link',
);
previouslyFocusedElementRef.current =
document.activeElement as HTMLElement;
firstLink?.focus();
} else {
previouslyFocusedElementRef.current?.focus();
}
}, [open]);
let banner: React.ReactNode;
if (transientSingleColumn) {
banner = (
<div className='switch-to-advanced'>
{intl.formatMessage(messages.openedInClassicInterface)}{' '}
<a
href={`/deck${location.pathname}`}
className='switch-to-advanced__toggle'
>
{intl.formatMessage(messages.advancedInterface)}
</a>
</div>
);
}
const showOverlay = openable && open;
return (
<div
className={classNames(
'columns-area__panels__pane columns-area__panels__pane--start columns-area__panels__pane--navigational',
{ 'columns-area__panels__pane--overlay': showOverlay },
)}
ref={overlayRef}
>
<animated.div
className='columns-area__panels__pane__inner'
{...bind()}
style={openable ? { x } : undefined}
>
<div className='navigation-panel'>
<div className='navigation-panel__logo'>
<Link to='/' className='column-link column-link--logo'>
<WordmarkLogo />
</Link>
</div>
{showSearch && <Search singleColumn />}
<ProfileCard />
{banner && <div className='navigation-panel__banner'>{banner}</div>}
<div className='navigation-panel__menu'>
{signedIn && (
<>
<ColumnLink
to='/publish'
icon='plus'
iconComponent={AddIcon}
activeIconComponent={AddIcon}
text={intl.formatMessage(messages.compose)}
className='button navigation-panel__compose-button'
/>
<ColumnLink
transparent
to='/home'
icon='home'
iconComponent={HomeIcon}
activeIconComponent={HomeActiveIcon}
text={intl.formatMessage(messages.home)}
/>
</>
)}
{trendsEnabled && (
<ColumnLink
transparent
to='/explore'
icon='explore'
iconComponent={TrendingUpIcon}
text={intl.formatMessage(messages.explore)}
/>
)}
{(signedIn || timelinePreview) && (
<ColumnLink
transparent
to='/public/local'
icon='globe'
iconComponent={PublicIcon}
isActive={isFirehoseActive}
text={intl.formatMessage(messages.firehose)}
/>
)}
{signedIn && (
<>
<NotificationsLink />
<FollowRequestsLink />
<hr />
<ListPanel />
<FollowedTagsPanel />
<ColumnLink
transparent
to='/favourites'
icon='star'
iconComponent={StarIcon}
activeIconComponent={StarActiveIcon}
text={intl.formatMessage(messages.favourites)}
/>
<ColumnLink
transparent
to='/bookmarks'
icon='bookmarks'
iconComponent={BookmarksIcon}
activeIconComponent={BookmarksActiveIcon}
text={intl.formatMessage(messages.bookmarks)}
/>
<ColumnLink
transparent
to='/conversations'
icon='at'
iconComponent={AlternateEmailIcon}
text={intl.formatMessage(messages.direct)}
/>
<hr />
<ColumnLink
transparent
href='/settings/preferences'
icon='cog'
iconComponent={SettingsIcon}
text={intl.formatMessage(messages.preferences)}
/>
<MoreLink />
</>
)}
<div className='navigation-panel__legal'>
<ColumnLink
transparent
to='/about'
icon='ellipsis-h'
iconComponent={InfoIcon}
text={intl.formatMessage(messages.about)}
/>
</div>
{!signedIn && (
<div className='navigation-panel__sign-in-banner'>
<hr />
{disabledAccountId ? (
<DisabledAccountBanner />
) : (
<SignInBanner />
)}
</div>
)}
</div>
<div className='flex-spacer' />
<Trends />
</div>
</animated.div>
</div>
);
};