Change navigation layout on small screens in web UI (#34910)
7
app/javascript/mastodon/actions/navigation.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
export const openNavigation = createAction('navigation/open');
|
||||||
|
|
||||||
|
export const closeNavigation = createAction('navigation/close');
|
||||||
|
|
||||||
|
export const toggleNavigation = createAction('navigation/toggle');
|
|
@ -9,7 +9,8 @@ import ArrowBackIcon from '@/material-icons/400-24px/arrow_back.svg?react';
|
||||||
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
|
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
|
||||||
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
||||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||||
import SettingsIcon from '@/material-icons/400-24px/settings.svg?react';
|
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 type { IconProp } from 'mastodon/components/icon';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
import { ButtonInTabsBar } from 'mastodon/features/ui/util/columns_context';
|
import { ButtonInTabsBar } from 'mastodon/features/ui/util/columns_context';
|
||||||
|
@ -238,7 +239,10 @@ export const ColumnHeader: React.FC<Props> = ({
|
||||||
onClick={handleToggleClick}
|
onClick={handleToggleClick}
|
||||||
>
|
>
|
||||||
<i className='icon-with-badge'>
|
<i className='icon-with-badge'>
|
||||||
<Icon id='sliders' icon={SettingsIcon} />
|
<Icon
|
||||||
|
id='sliders'
|
||||||
|
icon={collapsed ? UnfoldMoreIcon : UnfoldLessIcon}
|
||||||
|
/>
|
||||||
{collapseIssues && <i className='icon-with-badge__issue-badge' />}
|
{collapseIssues && <i className='icon-with-badge__issue-badge' />}
|
||||||
</i>
|
</i>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -27,6 +27,7 @@ interface Props {
|
||||||
counter?: number;
|
counter?: number;
|
||||||
href?: string;
|
href?: string;
|
||||||
ariaHidden?: boolean;
|
ariaHidden?: boolean;
|
||||||
|
ariaControls?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IconButton = forwardRef<HTMLButtonElement, Props>(
|
export const IconButton = forwardRef<HTMLButtonElement, Props>(
|
||||||
|
@ -52,6 +53,7 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(
|
||||||
overlay = false,
|
overlay = false,
|
||||||
tabIndex = 0,
|
tabIndex = 0,
|
||||||
ariaHidden = false,
|
ariaHidden = false,
|
||||||
|
ariaControls,
|
||||||
},
|
},
|
||||||
buttonRef,
|
buttonRef,
|
||||||
) => {
|
) => {
|
||||||
|
@ -153,6 +155,7 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(
|
||||||
aria-label={title}
|
aria-label={title}
|
||||||
aria-expanded={expanded}
|
aria-expanded={expanded}
|
||||||
aria-hidden={ariaHidden}
|
aria-hidden={ariaHidden}
|
||||||
|
aria-controls={ariaControls}
|
||||||
title={title}
|
title={title}
|
||||||
className={classes}
|
className={classes}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
|
|
@ -7,7 +7,7 @@ interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
icon: IconProp;
|
icon: IconProp;
|
||||||
count: number;
|
count: number;
|
||||||
issueBadge: boolean;
|
issueBadge?: boolean;
|
||||||
className: string;
|
className: string;
|
||||||
}
|
}
|
||||||
export const IconWithBadge: React.FC<Props> = ({
|
export const IconWithBadge: React.FC<Props> = ({
|
||||||
|
|
|
@ -2,34 +2,47 @@ import { useCallback } from 'react';
|
||||||
|
|
||||||
import { useIntl, defineMessages } from 'react-intl';
|
import { useIntl, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
|
||||||
|
|
||||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||||
import { cancelReplyCompose } from 'mastodon/actions/compose';
|
import { cancelReplyCompose } from 'mastodon/actions/compose';
|
||||||
import { Account } from 'mastodon/components/account';
|
import { Account } from 'mastodon/components/account';
|
||||||
import { IconButton } from 'mastodon/components/icon_button';
|
import { IconButton } from 'mastodon/components/icon_button';
|
||||||
import { me } from 'mastodon/initial_state';
|
import { me } from 'mastodon/initial_state';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
import { ActionBar } from './action_bar';
|
import { ActionBar } from './action_bar';
|
||||||
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
|
cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const NavigationBar = () => {
|
export const NavigationBar: React.FC = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const isReplying = useSelector(state => !!state.getIn(['compose', 'in_reply_to']));
|
const isReplying = useAppSelector(
|
||||||
|
(state) => !!state.compose.get('in_reply_to'),
|
||||||
|
);
|
||||||
|
|
||||||
const handleCancelClick = useCallback(() => {
|
const handleCancelClick = useCallback(() => {
|
||||||
dispatch(cancelReplyCompose());
|
dispatch(cancelReplyCompose());
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
|
if (!me) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='navigation-bar'>
|
<div className='navigation-bar'>
|
||||||
<Account id={me} minimal />
|
<Account id={me} minimal />
|
||||||
{isReplying ? <IconButton title={intl.formatMessage(messages.cancel)} iconComponent={CloseIcon} onClick={handleCancelClick} /> : <ActionBar />}
|
{isReplying ? (
|
||||||
|
<IconButton
|
||||||
|
title={intl.formatMessage(messages.cancel)}
|
||||||
|
icon=''
|
||||||
|
iconComponent={CloseIcon}
|
||||||
|
onClick={handleCancelClick}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ActionBar />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
|
@ -1,138 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { PureComponent } from 'react';
|
|
||||||
|
|
||||||
import { injectIntl, defineMessages } from 'react-intl';
|
|
||||||
|
|
||||||
import { Helmet } from 'react-helmet';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
|
|
||||||
import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react';
|
|
||||||
import LogoutIcon from '@/material-icons/400-24px/logout.svg?react';
|
|
||||||
import MenuIcon from '@/material-icons/400-24px/menu.svg?react';
|
|
||||||
import NotificationsIcon from '@/material-icons/400-24px/notifications-fill.svg?react';
|
|
||||||
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
|
|
||||||
import SettingsIcon from '@/material-icons/400-24px/settings-fill.svg?react';
|
|
||||||
import { openModal } from 'mastodon/actions/modal';
|
|
||||||
import Column from 'mastodon/components/column';
|
|
||||||
import { Icon } from 'mastodon/components/icon';
|
|
||||||
|
|
||||||
import elephantUIPlane from '../../../images/elephant_ui_plane.svg';
|
|
||||||
import { changeComposing, mountCompose, unmountCompose } from '../../actions/compose';
|
|
||||||
import { mascot } from '../../initial_state';
|
|
||||||
import { isMobile } from '../../is_mobile';
|
|
||||||
|
|
||||||
import { Search } from './components/search';
|
|
||||||
import ComposeFormContainer from './containers/compose_form_container';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
|
|
||||||
home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' },
|
|
||||||
notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' },
|
|
||||||
public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' },
|
|
||||||
community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
|
|
||||||
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
|
||||||
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
|
||||||
compose: { id: 'navigation_bar.compose', defaultMessage: 'Compose new post' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapStateToProps = (state) => ({
|
|
||||||
columns: state.getIn(['settings', 'columns']),
|
|
||||||
});
|
|
||||||
|
|
||||||
class Compose extends PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
dispatch: PropTypes.func.isRequired,
|
|
||||||
columns: ImmutablePropTypes.list.isRequired,
|
|
||||||
multiColumn: PropTypes.bool,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
const { dispatch } = this.props;
|
|
||||||
dispatch(mountCompose());
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount () {
|
|
||||||
const { dispatch } = this.props;
|
|
||||||
dispatch(unmountCompose());
|
|
||||||
}
|
|
||||||
|
|
||||||
handleLogoutClick = e => {
|
|
||||||
const { dispatch } = this.props;
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT' }));
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
onFocus = () => {
|
|
||||||
this.props.dispatch(changeComposing(true));
|
|
||||||
};
|
|
||||||
|
|
||||||
onBlur = () => {
|
|
||||||
this.props.dispatch(changeComposing(false));
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { multiColumn, intl } = this.props;
|
|
||||||
|
|
||||||
if (multiColumn) {
|
|
||||||
const { columns } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='drawer' role='region' aria-label={intl.formatMessage(messages.compose)}>
|
|
||||||
<nav className='drawer__header'>
|
|
||||||
<Link to='/getting-started' className='drawer__tab' title={intl.formatMessage(messages.start)} aria-label={intl.formatMessage(messages.start)}><Icon id='bars' icon={MenuIcon} /></Link>
|
|
||||||
{!columns.some(column => column.get('id') === 'HOME') && (
|
|
||||||
<Link to='/home' className='drawer__tab' title={intl.formatMessage(messages.home_timeline)} aria-label={intl.formatMessage(messages.home_timeline)}><Icon id='home' icon={HomeIcon} /></Link>
|
|
||||||
)}
|
|
||||||
{!columns.some(column => column.get('id') === 'NOTIFICATIONS') && (
|
|
||||||
<Link to='/notifications' className='drawer__tab' title={intl.formatMessage(messages.notifications)} aria-label={intl.formatMessage(messages.notifications)}><Icon id='bell' icon={NotificationsIcon} /></Link>
|
|
||||||
)}
|
|
||||||
{!columns.some(column => column.get('id') === 'COMMUNITY') && (
|
|
||||||
<Link to='/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)} aria-label={intl.formatMessage(messages.community)}><Icon id='users' icon={PeopleIcon} /></Link>
|
|
||||||
)}
|
|
||||||
{!columns.some(column => column.get('id') === 'PUBLIC') && (
|
|
||||||
<Link to='/public' className='drawer__tab' title={intl.formatMessage(messages.public)} aria-label={intl.formatMessage(messages.public)}><Icon id='globe' icon={PublicIcon} /></Link>
|
|
||||||
)}
|
|
||||||
<a href='/settings/preferences' className='drawer__tab' title={intl.formatMessage(messages.preferences)} aria-label={intl.formatMessage(messages.preferences)}><Icon id='cog' icon={SettingsIcon} /></a>
|
|
||||||
<a href='/auth/sign_out' className='drawer__tab' title={intl.formatMessage(messages.logout)} aria-label={intl.formatMessage(messages.logout)} onClick={this.handleLogoutClick}><Icon id='sign-out' icon={LogoutIcon} /></a>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
{multiColumn && <Search /> }
|
|
||||||
|
|
||||||
<div className='drawer__pager'>
|
|
||||||
<div className='drawer__inner' onFocus={this.onFocus}>
|
|
||||||
<ComposeFormContainer autoFocus={!isMobile(window.innerWidth)} />
|
|
||||||
|
|
||||||
<div className='drawer__inner__mastodon'>
|
|
||||||
<img alt='' draggable='false' src={mascot || elephantUIPlane} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Column onFocus={this.onFocus}>
|
|
||||||
<ComposeFormContainer />
|
|
||||||
|
|
||||||
<Helmet>
|
|
||||||
<meta name='robots' content='noindex' />
|
|
||||||
</Helmet>
|
|
||||||
</Column>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(injectIntl(Compose));
|
|
200
app/javascript/mastodon/features/compose/index.tsx
Normal file
|
@ -0,0 +1,200 @@
|
||||||
|
import { useEffect, useCallback } from 'react';
|
||||||
|
|
||||||
|
import { useIntl, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import type { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||||
|
|
||||||
|
import elephantUIPlane from '@/images/elephant_ui_plane.svg';
|
||||||
|
import EditIcon from '@/material-icons/400-24px/edit_square.svg?react';
|
||||||
|
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
|
||||||
|
import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react';
|
||||||
|
import LogoutIcon from '@/material-icons/400-24px/logout.svg?react';
|
||||||
|
import MenuIcon from '@/material-icons/400-24px/menu.svg?react';
|
||||||
|
import NotificationsIcon from '@/material-icons/400-24px/notifications-fill.svg?react';
|
||||||
|
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
|
||||||
|
import SettingsIcon from '@/material-icons/400-24px/settings-fill.svg?react';
|
||||||
|
import { mountCompose, unmountCompose } from 'mastodon/actions/compose';
|
||||||
|
import { openModal } from 'mastodon/actions/modal';
|
||||||
|
import { Column } from 'mastodon/components/column';
|
||||||
|
import { ColumnHeader } from 'mastodon/components/column_header';
|
||||||
|
import { Icon } from 'mastodon/components/icon';
|
||||||
|
import { mascot } from 'mastodon/initial_state';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
|
import { Search } from './components/search';
|
||||||
|
import ComposeFormContainer from './containers/compose_form_container';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
|
||||||
|
home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' },
|
||||||
|
notifications: {
|
||||||
|
id: 'tabs_bar.notifications',
|
||||||
|
defaultMessage: 'Notifications',
|
||||||
|
},
|
||||||
|
public: {
|
||||||
|
id: 'navigation_bar.public_timeline',
|
||||||
|
defaultMessage: 'Federated timeline',
|
||||||
|
},
|
||||||
|
community: {
|
||||||
|
id: 'navigation_bar.community_timeline',
|
||||||
|
defaultMessage: 'Local timeline',
|
||||||
|
},
|
||||||
|
preferences: {
|
||||||
|
id: 'navigation_bar.preferences',
|
||||||
|
defaultMessage: 'Preferences',
|
||||||
|
},
|
||||||
|
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
||||||
|
compose: { id: 'navigation_bar.compose', defaultMessage: 'Compose new post' },
|
||||||
|
});
|
||||||
|
|
||||||
|
type ColumnMap = ImmutableMap<'id' | 'uuid' | 'params', string>;
|
||||||
|
|
||||||
|
const Compose: React.FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const columns = useAppSelector(
|
||||||
|
(state) =>
|
||||||
|
(state.settings as ImmutableMap<string, unknown>).get(
|
||||||
|
'columns',
|
||||||
|
) as ImmutableList<ColumnMap>,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(mountCompose());
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
dispatch(unmountCompose());
|
||||||
|
};
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const handleLogoutClick = useCallback(
|
||||||
|
(e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT', modalProps: {} }));
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
[dispatch],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (multiColumn) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className='drawer'
|
||||||
|
role='region'
|
||||||
|
aria-label={intl.formatMessage(messages.compose)}
|
||||||
|
>
|
||||||
|
<nav className='drawer__header'>
|
||||||
|
<Link
|
||||||
|
to='/getting-started'
|
||||||
|
className='drawer__tab'
|
||||||
|
title={intl.formatMessage(messages.start)}
|
||||||
|
aria-label={intl.formatMessage(messages.start)}
|
||||||
|
>
|
||||||
|
<Icon id='bars' icon={MenuIcon} />
|
||||||
|
</Link>
|
||||||
|
{!columns.some((column) => column.get('id') === 'HOME') && (
|
||||||
|
<Link
|
||||||
|
to='/home'
|
||||||
|
className='drawer__tab'
|
||||||
|
title={intl.formatMessage(messages.home_timeline)}
|
||||||
|
aria-label={intl.formatMessage(messages.home_timeline)}
|
||||||
|
>
|
||||||
|
<Icon id='home' icon={HomeIcon} />
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
{!columns.some((column) => column.get('id') === 'NOTIFICATIONS') && (
|
||||||
|
<Link
|
||||||
|
to='/notifications'
|
||||||
|
className='drawer__tab'
|
||||||
|
title={intl.formatMessage(messages.notifications)}
|
||||||
|
aria-label={intl.formatMessage(messages.notifications)}
|
||||||
|
>
|
||||||
|
<Icon id='bell' icon={NotificationsIcon} />
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
{!columns.some((column) => column.get('id') === 'COMMUNITY') && (
|
||||||
|
<Link
|
||||||
|
to='/public/local'
|
||||||
|
className='drawer__tab'
|
||||||
|
title={intl.formatMessage(messages.community)}
|
||||||
|
aria-label={intl.formatMessage(messages.community)}
|
||||||
|
>
|
||||||
|
<Icon id='users' icon={PeopleIcon} />
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
{!columns.some((column) => column.get('id') === 'PUBLIC') && (
|
||||||
|
<Link
|
||||||
|
to='/public'
|
||||||
|
className='drawer__tab'
|
||||||
|
title={intl.formatMessage(messages.public)}
|
||||||
|
aria-label={intl.formatMessage(messages.public)}
|
||||||
|
>
|
||||||
|
<Icon id='globe' icon={PublicIcon} />
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
<a
|
||||||
|
href='/settings/preferences'
|
||||||
|
className='drawer__tab'
|
||||||
|
title={intl.formatMessage(messages.preferences)}
|
||||||
|
aria-label={intl.formatMessage(messages.preferences)}
|
||||||
|
>
|
||||||
|
<Icon id='cog' icon={SettingsIcon} />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href='/auth/sign_out'
|
||||||
|
className='drawer__tab'
|
||||||
|
title={intl.formatMessage(messages.logout)}
|
||||||
|
aria-label={intl.formatMessage(messages.logout)}
|
||||||
|
onClick={handleLogoutClick}
|
||||||
|
>
|
||||||
|
<Icon id='sign-out' icon={LogoutIcon} />
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<Search singleColumn={false} />
|
||||||
|
|
||||||
|
<div className='drawer__pager'>
|
||||||
|
<div className='drawer__inner'>
|
||||||
|
<ComposeFormContainer />
|
||||||
|
|
||||||
|
<div className='drawer__inner__mastodon'>
|
||||||
|
<img alt='' draggable='false' src={mascot ?? elephantUIPlane} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column
|
||||||
|
bindToDocument={!multiColumn}
|
||||||
|
label={intl.formatMessage(messages.compose)}
|
||||||
|
>
|
||||||
|
<ColumnHeader
|
||||||
|
icon='pencil'
|
||||||
|
iconComponent={EditIcon}
|
||||||
|
title={intl.formatMessage(messages.compose)}
|
||||||
|
multiColumn={multiColumn}
|
||||||
|
showBackButton
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className='scrollable'>
|
||||||
|
<ComposeFormContainer />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Helmet>
|
||||||
|
<meta name='robots' content='noindex' />
|
||||||
|
</Helmet>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
export default Compose;
|
|
@ -9,7 +9,9 @@ import ExploreIcon from '@/material-icons/400-24px/explore.svg?react';
|
||||||
import { Column } from 'mastodon/components/column';
|
import { Column } from 'mastodon/components/column';
|
||||||
import type { ColumnRef } from 'mastodon/components/column';
|
import type { ColumnRef } from 'mastodon/components/column';
|
||||||
import { ColumnHeader } from 'mastodon/components/column_header';
|
import { ColumnHeader } from 'mastodon/components/column_header';
|
||||||
|
import { SymbolLogo } from 'mastodon/components/logo';
|
||||||
import { Search } from 'mastodon/features/compose/components/search';
|
import { Search } from 'mastodon/features/compose/components/search';
|
||||||
|
import { useBreakpoint } from 'mastodon/features/ui/hooks/useBreakpoint';
|
||||||
import { useIdentity } from 'mastodon/identity_context';
|
import { useIdentity } from 'mastodon/identity_context';
|
||||||
|
|
||||||
import Links from './links';
|
import Links from './links';
|
||||||
|
@ -25,6 +27,7 @@ const Explore: React.FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
|
||||||
const { signedIn } = useIdentity();
|
const { signedIn } = useIdentity();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const columnRef = useRef<ColumnRef>(null);
|
const columnRef = useRef<ColumnRef>(null);
|
||||||
|
const logoRequired = useBreakpoint('full');
|
||||||
|
|
||||||
const handleHeaderClick = useCallback(() => {
|
const handleHeaderClick = useCallback(() => {
|
||||||
columnRef.current?.scrollTop();
|
columnRef.current?.scrollTop();
|
||||||
|
@ -38,7 +41,7 @@ const Explore: React.FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
|
||||||
>
|
>
|
||||||
<ColumnHeader
|
<ColumnHeader
|
||||||
icon={'explore'}
|
icon={'explore'}
|
||||||
iconComponent={ExploreIcon}
|
iconComponent={logoRequired ? SymbolLogo : ExploreIcon}
|
||||||
title={intl.formatMessage(messages.title)}
|
title={intl.formatMessage(messages.title)}
|
||||||
onClick={handleHeaderClick}
|
onClick={handleHeaderClick}
|
||||||
multiColumn={multiColumn}
|
multiColumn={multiColumn}
|
||||||
|
|
|
@ -31,7 +31,7 @@ import { canManageReports, canViewAdminDashboard } from 'mastodon/permissions';
|
||||||
|
|
||||||
import { me, showTrends } from '../../initial_state';
|
import { me, showTrends } from '../../initial_state';
|
||||||
import { NavigationBar } from '../compose/components/navigation_bar';
|
import { NavigationBar } from '../compose/components/navigation_bar';
|
||||||
import ColumnLink from '../ui/components/column_link';
|
import { ColumnLink } from '../ui/components/column_link';
|
||||||
import ColumnSubheading from '../ui/components/column_subheading';
|
import ColumnSubheading from '../ui/components/column_subheading';
|
||||||
|
|
||||||
import TrendsContainer from './containers/trends_container';
|
import TrendsContainer from './containers/trends_container';
|
||||||
|
|
|
@ -10,12 +10,14 @@ import { connect } from 'react-redux';
|
||||||
|
|
||||||
import CampaignIcon from '@/material-icons/400-24px/campaign.svg?react';
|
import CampaignIcon from '@/material-icons/400-24px/campaign.svg?react';
|
||||||
import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react';
|
import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react';
|
||||||
|
import { SymbolLogo } from 'mastodon/components/logo';
|
||||||
import { fetchAnnouncements, toggleShowAnnouncements } from 'mastodon/actions/announcements';
|
import { fetchAnnouncements, toggleShowAnnouncements } from 'mastodon/actions/announcements';
|
||||||
import { IconWithBadge } from 'mastodon/components/icon_with_badge';
|
import { IconWithBadge } from 'mastodon/components/icon_with_badge';
|
||||||
import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator';
|
import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator';
|
||||||
import AnnouncementsContainer from 'mastodon/features/getting_started/containers/announcements_container';
|
import AnnouncementsContainer from 'mastodon/features/getting_started/containers/announcements_container';
|
||||||
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
|
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
|
||||||
import { criticalUpdatesPending } from 'mastodon/initial_state';
|
import { criticalUpdatesPending } from 'mastodon/initial_state';
|
||||||
|
import { withBreakpoint } from 'mastodon/features/ui/hooks/useBreakpoint';
|
||||||
|
|
||||||
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
||||||
import { expandHomeTimeline } from '../../actions/timelines';
|
import { expandHomeTimeline } from '../../actions/timelines';
|
||||||
|
@ -52,6 +54,7 @@ class HomeTimeline extends PureComponent {
|
||||||
hasAnnouncements: PropTypes.bool,
|
hasAnnouncements: PropTypes.bool,
|
||||||
unreadAnnouncements: PropTypes.number,
|
unreadAnnouncements: PropTypes.number,
|
||||||
showAnnouncements: PropTypes.bool,
|
showAnnouncements: PropTypes.bool,
|
||||||
|
matchesBreakpoint: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
handlePin = () => {
|
handlePin = () => {
|
||||||
|
@ -121,7 +124,7 @@ class HomeTimeline extends PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, hasUnread, columnId, multiColumn, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props;
|
const { intl, hasUnread, columnId, multiColumn, hasAnnouncements, unreadAnnouncements, showAnnouncements, matchesBreakpoint } = this.props;
|
||||||
const pinned = !!columnId;
|
const pinned = !!columnId;
|
||||||
const { signedIn } = this.props.identity;
|
const { signedIn } = this.props.identity;
|
||||||
const banners = [];
|
const banners = [];
|
||||||
|
@ -150,7 +153,7 @@ class HomeTimeline extends PureComponent {
|
||||||
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
|
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
|
||||||
<ColumnHeader
|
<ColumnHeader
|
||||||
icon='home'
|
icon='home'
|
||||||
iconComponent={HomeIcon}
|
iconComponent={matchesBreakpoint ? SymbolLogo : HomeIcon}
|
||||||
active={hasUnread}
|
active={hasUnread}
|
||||||
title={intl.formatMessage(messages.title)}
|
title={intl.formatMessage(messages.title)}
|
||||||
onPin={this.handlePin}
|
onPin={this.handlePin}
|
||||||
|
@ -187,4 +190,4 @@ class HomeTimeline extends PureComponent {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps)(withIdentity(injectIntl(HomeTimeline)));
|
export default connect(mapStateToProps)(withBreakpoint(withIdentity(injectIntl(HomeTimeline))));
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { useRouteMatch, NavLink } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { Icon } from 'mastodon/components/icon';
|
|
||||||
|
|
||||||
const ColumnLink = ({ icon, activeIcon, iconComponent, activeIconComponent, text, to, href, method, badge, transparent, optional, ...other }) => {
|
|
||||||
const match = useRouteMatch(to);
|
|
||||||
const className = classNames('column-link', { 'column-link--transparent': transparent, 'column-link--optional': optional });
|
|
||||||
const badgeElement = typeof badge !== 'undefined' ? <span className='column-link__badge'>{badge}</span> : null;
|
|
||||||
const iconElement = (typeof icon === 'string' || iconComponent) ? <Icon id={icon} icon={iconComponent} className='column-link__icon' /> : icon;
|
|
||||||
const activeIconElement = activeIcon ?? (activeIconComponent ? <Icon id={icon} icon={activeIconComponent} className='column-link__icon' /> : iconElement);
|
|
||||||
const active = match?.isExact;
|
|
||||||
|
|
||||||
if (href) {
|
|
||||||
return (
|
|
||||||
<a href={href} className={className} data-method={method} {...other}>
|
|
||||||
{active ? activeIconElement : iconElement}
|
|
||||||
<span>{text}</span>
|
|
||||||
{badgeElement}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<NavLink to={to} className={className} exact {...other}>
|
|
||||||
{active ? activeIconElement : iconElement}
|
|
||||||
<span>{text}</span>
|
|
||||||
{badgeElement}
|
|
||||||
</NavLink>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ColumnLink.propTypes = {
|
|
||||||
icon: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
|
|
||||||
iconComponent: PropTypes.func,
|
|
||||||
activeIcon: PropTypes.node,
|
|
||||||
activeIconComponent: PropTypes.func,
|
|
||||||
text: PropTypes.string.isRequired,
|
|
||||||
to: PropTypes.string,
|
|
||||||
href: PropTypes.string,
|
|
||||||
method: PropTypes.string,
|
|
||||||
badge: PropTypes.node,
|
|
||||||
transparent: PropTypes.bool,
|
|
||||||
optional: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ColumnLink;
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { useRouteMatch, NavLink } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { Icon } from 'mastodon/components/icon';
|
||||||
|
import type { IconProp } from 'mastodon/components/icon';
|
||||||
|
|
||||||
|
export const ColumnLink: React.FC<{
|
||||||
|
icon: React.ReactNode;
|
||||||
|
iconComponent?: IconProp;
|
||||||
|
activeIcon?: React.ReactNode;
|
||||||
|
activeIconComponent?: IconProp;
|
||||||
|
isActive?: (match: unknown, location: { pathname: string }) => boolean;
|
||||||
|
text: string;
|
||||||
|
to?: string;
|
||||||
|
href?: string;
|
||||||
|
method?: string;
|
||||||
|
badge?: React.ReactNode;
|
||||||
|
transparent?: boolean;
|
||||||
|
optional?: boolean;
|
||||||
|
className?: string;
|
||||||
|
id?: string;
|
||||||
|
}> = ({
|
||||||
|
icon,
|
||||||
|
activeIcon,
|
||||||
|
iconComponent,
|
||||||
|
activeIconComponent,
|
||||||
|
text,
|
||||||
|
to,
|
||||||
|
href,
|
||||||
|
method,
|
||||||
|
badge,
|
||||||
|
transparent,
|
||||||
|
optional,
|
||||||
|
...other
|
||||||
|
}) => {
|
||||||
|
const match = useRouteMatch(to ?? '');
|
||||||
|
const className = classNames('column-link', {
|
||||||
|
'column-link--transparent': transparent,
|
||||||
|
'column-link--optional': optional,
|
||||||
|
});
|
||||||
|
const badgeElement =
|
||||||
|
typeof badge !== 'undefined' ? (
|
||||||
|
<span className='column-link__badge'>{badge}</span>
|
||||||
|
) : null;
|
||||||
|
const iconElement = iconComponent ? (
|
||||||
|
<Icon
|
||||||
|
id={typeof icon === 'string' ? icon : ''}
|
||||||
|
icon={iconComponent}
|
||||||
|
className='column-link__icon'
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
icon
|
||||||
|
);
|
||||||
|
const activeIconElement =
|
||||||
|
activeIcon ??
|
||||||
|
(activeIconComponent ? (
|
||||||
|
<Icon
|
||||||
|
id={typeof icon === 'string' ? icon : ''}
|
||||||
|
icon={activeIconComponent}
|
||||||
|
className='column-link__icon'
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
iconElement
|
||||||
|
));
|
||||||
|
const active = !!match;
|
||||||
|
|
||||||
|
if (href) {
|
||||||
|
return (
|
||||||
|
<a href={href} className={className} data-method={method} {...other}>
|
||||||
|
{active ? activeIconElement : iconElement}
|
||||||
|
<span>{text}</span>
|
||||||
|
{badgeElement}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
} else if (to) {
|
||||||
|
return (
|
||||||
|
<NavLink to={to} className={className} {...other}>
|
||||||
|
{active ? activeIconElement : iconElement}
|
||||||
|
<span>{text}</span>
|
||||||
|
{badgeElement}
|
||||||
|
</NavLink>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
|
@ -25,7 +25,7 @@ import BundleColumnError from './bundle_column_error';
|
||||||
import { ColumnLoading } from './column_loading';
|
import { ColumnLoading } from './column_loading';
|
||||||
import { ComposePanel } from './compose_panel';
|
import { ComposePanel } from './compose_panel';
|
||||||
import DrawerLoading from './drawer_loading';
|
import DrawerLoading from './drawer_loading';
|
||||||
import NavigationPanel from './navigation_panel';
|
import { NavigationPanel } from './navigation_panel';
|
||||||
|
|
||||||
const componentMap = {
|
const componentMap = {
|
||||||
'COMPOSE': Compose,
|
'COMPOSE': Compose,
|
||||||
|
@ -132,12 +132,8 @@ export default class ColumnsArea extends ImmutablePureComponent {
|
||||||
<div className='columns-area columns-area--mobile'>{children}</div>
|
<div className='columns-area columns-area--mobile'>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='columns-area__panels__pane columns-area__panels__pane--start columns-area__panels__pane--navigational'>
|
|
||||||
<div className='columns-area__panels__pane__inner'>
|
|
||||||
<NavigationPanel />
|
<NavigationPanel />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,121 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { PureComponent } from 'react';
|
|
||||||
|
|
||||||
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
|
||||||
|
|
||||||
import { Link, withRouter } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
|
|
||||||
import { openModal } from 'mastodon/actions/modal';
|
|
||||||
import { fetchServer } from 'mastodon/actions/server';
|
|
||||||
import { Avatar } from 'mastodon/components/avatar';
|
|
||||||
import { Icon } from 'mastodon/components/icon';
|
|
||||||
import { WordmarkLogo, SymbolLogo } from 'mastodon/components/logo';
|
|
||||||
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
|
|
||||||
import { registrationsOpen, me, sso_redirect } from 'mastodon/initial_state';
|
|
||||||
|
|
||||||
const Account = connect(state => ({
|
|
||||||
account: state.getIn(['accounts', me]),
|
|
||||||
}))(({ account }) => (
|
|
||||||
<Link to={`/@${account.get('acct')}`} title={account.get('acct')}>
|
|
||||||
<Avatar account={account} size={35} />
|
|
||||||
</Link>
|
|
||||||
));
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
search: { id: 'navigation_bar.search', defaultMessage: 'Search' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapStateToProps = (state) => ({
|
|
||||||
signupUrl: state.getIn(['server', 'server', 'registrations', 'url'], null) || '/auth/sign_up',
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
openClosedRegistrationsModal() {
|
|
||||||
dispatch(openModal({ modalType: 'CLOSED_REGISTRATIONS' }));
|
|
||||||
},
|
|
||||||
dispatchServer() {
|
|
||||||
dispatch(fetchServer());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
class Header extends PureComponent {
|
|
||||||
static propTypes = {
|
|
||||||
identity: identityContextPropShape,
|
|
||||||
openClosedRegistrationsModal: PropTypes.func,
|
|
||||||
location: PropTypes.object,
|
|
||||||
signupUrl: PropTypes.string.isRequired,
|
|
||||||
dispatchServer: PropTypes.func,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
const { dispatchServer } = this.props;
|
|
||||||
dispatchServer();
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { signedIn } = this.props.identity;
|
|
||||||
const { location, openClosedRegistrationsModal, signupUrl, intl } = this.props;
|
|
||||||
|
|
||||||
let content;
|
|
||||||
|
|
||||||
if (signedIn) {
|
|
||||||
content = (
|
|
||||||
<>
|
|
||||||
{location.pathname !== '/search' && <Link to='/search' className='button button-secondary' aria-label={intl.formatMessage(messages.search)}><Icon id='search' icon={SearchIcon} /></Link>}
|
|
||||||
{location.pathname !== '/publish' && <Link to='/publish' className='button button-secondary'><FormattedMessage id='compose_form.publish_form' defaultMessage='New post' /></Link>}
|
|
||||||
<Account />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
|
|
||||||
if (sso_redirect) {
|
|
||||||
content = (
|
|
||||||
<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>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
let signupButton;
|
|
||||||
|
|
||||||
if (registrationsOpen) {
|
|
||||||
signupButton = (
|
|
||||||
<a href={signupUrl} className='button'>
|
|
||||||
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
signupButton = (
|
|
||||||
<button className='button' onClick={openClosedRegistrationsModal}>
|
|
||||||
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
content = (
|
|
||||||
<>
|
|
||||||
{signupButton}
|
|
||||||
<a href='/auth/sign_in' className='button button-tertiary'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Login' /></a>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='ui__header'>
|
|
||||||
<Link to='/' className='ui__header__logo'>
|
|
||||||
<WordmarkLogo />
|
|
||||||
<SymbolLogo />
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<div className='ui__header__links'>
|
|
||||||
{content}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default injectIntl(withRouter(withIdentity(connect(mapStateToProps, mapDispatchToProps)(Header))));
|
|
|
@ -1,41 +0,0 @@
|
||||||
import { useEffect } from 'react';
|
|
||||||
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
|
|
||||||
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 './column_link';
|
|
||||||
|
|
||||||
const getOrderedLists = createSelector([state => state.get('lists')], lists => {
|
|
||||||
if (!lists) {
|
|
||||||
return lists;
|
|
||||||
}
|
|
||||||
|
|
||||||
return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title'))).take(4);
|
|
||||||
});
|
|
||||||
|
|
||||||
export const ListPanel = () => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const lists = useSelector(state => getOrderedLists(state));
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
dispatch(fetchLists());
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
if (!lists || lists.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='list-panel'>
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
{lists.map(list => (
|
|
||||||
<ColumnLink icon='list-ul' key={list.get('id')} iconComponent={ListAltIcon} activeIconComponent={ListAltActiveIcon} text={list.get('title')} to={`/lists/${list.get('id')}`} transparent />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -0,0 +1,92 @@
|
||||||
|
import { useEffect, useState, useCallback, useId } from 'react';
|
||||||
|
|
||||||
|
import { useIntl, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
|
import ArrowDropDownIcon from '@/material-icons/400-24px/arrow_drop_down.svg?react';
|
||||||
|
import ArrowLeftIcon from '@/material-icons/400-24px/arrow_left.svg?react';
|
||||||
|
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 { IconButton } from 'mastodon/components/icon_button';
|
||||||
|
import { getOrderedLists } from 'mastodon/selectors/lists';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
|
import { ColumnLink } from './column_link';
|
||||||
|
|
||||||
|
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 [expanded, setExpanded] = useState(false);
|
||||||
|
const accessibilityId = useId();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchLists());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
setExpanded((value) => !value);
|
||||||
|
}, [setExpanded]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='navigation-panel__list-panel'>
|
||||||
|
<div className='navigation-panel__list-panel__header'>
|
||||||
|
<ColumnLink
|
||||||
|
transparent
|
||||||
|
to='/lists'
|
||||||
|
icon='list-ul'
|
||||||
|
iconComponent={ListAltIcon}
|
||||||
|
activeIconComponent={ListAltActiveIcon}
|
||||||
|
text={intl.formatMessage(messages.lists)}
|
||||||
|
id={`${accessibilityId}-title`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{lists.length > 0 && (
|
||||||
|
<IconButton
|
||||||
|
icon='down'
|
||||||
|
expanded={expanded}
|
||||||
|
iconComponent={expanded ? ArrowDropDownIcon : ArrowLeftIcon}
|
||||||
|
title={intl.formatMessage(
|
||||||
|
expanded ? messages.collapse : messages.expand,
|
||||||
|
)}
|
||||||
|
onClick={handleClick}
|
||||||
|
aria-controls={`${accessibilityId}-content`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{lists.length > 0 && expanded && (
|
||||||
|
<div
|
||||||
|
className='navigation-panel__list-panel__items'
|
||||||
|
role='region'
|
||||||
|
id={`${accessibilityId}-content`}
|
||||||
|
aria-labelledby={`${accessibilityId}-title`}
|
||||||
|
>
|
||||||
|
{lists.map((list) => (
|
||||||
|
<ColumnLink
|
||||||
|
icon='list-ul'
|
||||||
|
key={list.get('id')}
|
||||||
|
iconComponent={ListAltIcon}
|
||||||
|
activeIconComponent={ListAltActiveIcon}
|
||||||
|
text={list.get('title')}
|
||||||
|
to={`/lists/${list.get('id')}`}
|
||||||
|
transparent
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,204 @@
|
||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
|
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { NavLink, useRouteMatch } from 'react-router-dom';
|
||||||
|
|
||||||
|
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
||||||
|
import HomeActiveIcon from '@/material-icons/400-24px/home-fill.svg?react';
|
||||||
|
import HomeIcon from '@/material-icons/400-24px/home.svg?react';
|
||||||
|
import MenuIcon from '@/material-icons/400-24px/menu.svg?react';
|
||||||
|
import NotificationsActiveIcon from '@/material-icons/400-24px/notifications-fill.svg?react';
|
||||||
|
import NotificationsIcon from '@/material-icons/400-24px/notifications.svg?react';
|
||||||
|
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
|
||||||
|
import { openModal } from 'mastodon/actions/modal';
|
||||||
|
import { toggleNavigation } from 'mastodon/actions/navigation';
|
||||||
|
import { fetchServer } from 'mastodon/actions/server';
|
||||||
|
import { Icon } from 'mastodon/components/icon';
|
||||||
|
import { IconWithBadge } from 'mastodon/components/icon_with_badge';
|
||||||
|
import { useIdentity } from 'mastodon/identity_context';
|
||||||
|
import { registrationsOpen, sso_redirect } from 'mastodon/initial_state';
|
||||||
|
import { selectUnreadNotificationGroupsCount } from 'mastodon/selectors/notifications';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
home: { id: 'tabs_bar.home', defaultMessage: 'Home' },
|
||||||
|
search: { id: 'tabs_bar.search', defaultMessage: 'Search' },
|
||||||
|
publish: { id: 'tabs_bar.publish', defaultMessage: 'New Post' },
|
||||||
|
notifications: {
|
||||||
|
id: 'tabs_bar.notifications',
|
||||||
|
defaultMessage: 'Notifications',
|
||||||
|
},
|
||||||
|
menu: { id: 'tabs_bar.menu', defaultMessage: 'Menu' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const IconLabelButton: React.FC<{
|
||||||
|
to: string;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
activeIcon?: React.ReactNode;
|
||||||
|
title: string;
|
||||||
|
}> = ({ to, icon, activeIcon, title }) => {
|
||||||
|
const match = useRouteMatch(to);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NavLink
|
||||||
|
className='ui__navigation-bar__item'
|
||||||
|
activeClassName='active'
|
||||||
|
to={to}
|
||||||
|
aria-label={title}
|
||||||
|
>
|
||||||
|
{match && activeIcon ? activeIcon : icon}
|
||||||
|
</NavLink>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const NotificationsButton = () => {
|
||||||
|
const count = useAppSelector(selectUnreadNotificationGroupsCount);
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconLabelButton
|
||||||
|
to='/notifications'
|
||||||
|
icon={
|
||||||
|
<IconWithBadge
|
||||||
|
id='bell'
|
||||||
|
icon={NotificationsIcon}
|
||||||
|
count={count}
|
||||||
|
className=''
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
activeIcon={
|
||||||
|
<IconWithBadge
|
||||||
|
id='bell'
|
||||||
|
icon={NotificationsActiveIcon}
|
||||||
|
count={count}
|
||||||
|
className=''
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title={intl.formatMessage(messages.notifications)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const LoginOrSignUp: React.FC = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const signupUrl = useAppSelector(
|
||||||
|
(state) =>
|
||||||
|
(state.server.getIn(['server', 'registrations', 'url'], null) as
|
||||||
|
| string
|
||||||
|
| null) ?? '/auth/sign_up',
|
||||||
|
);
|
||||||
|
|
||||||
|
const openClosedRegistrationsModal = useCallback(() => {
|
||||||
|
dispatch(openModal({ modalType: 'CLOSED_REGISTRATIONS', modalProps: {} }));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchServer());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
if (sso_redirect) {
|
||||||
|
return (
|
||||||
|
<div className='ui__navigation-bar__sign-up'>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let signupButton;
|
||||||
|
|
||||||
|
if (registrationsOpen) {
|
||||||
|
signupButton = (
|
||||||
|
<a href={signupUrl} className='button'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='sign_in_banner.create_account'
|
||||||
|
defaultMessage='Create account'
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
signupButton = (
|
||||||
|
<button className='button' onClick={openClosedRegistrationsModal}>
|
||||||
|
<FormattedMessage
|
||||||
|
id='sign_in_banner.create_account'
|
||||||
|
defaultMessage='Create account'
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='ui__navigation-bar__sign-up'>
|
||||||
|
{signupButton}
|
||||||
|
<a href='/auth/sign_in' className='button button-tertiary'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='sign_in_banner.sign_in'
|
||||||
|
defaultMessage='Login'
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NavigationBar: React.FC = () => {
|
||||||
|
const { signedIn } = useIdentity();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const open = useAppSelector((state) => state.navigation.open);
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
dispatch(toggleNavigation());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='ui__navigation-bar'>
|
||||||
|
{!signedIn && <LoginOrSignUp />}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={classNames('ui__navigation-bar__items', {
|
||||||
|
active: signedIn,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{signedIn && (
|
||||||
|
<>
|
||||||
|
<IconLabelButton
|
||||||
|
title={intl.formatMessage(messages.home)}
|
||||||
|
to='/home'
|
||||||
|
icon={<Icon id='' icon={HomeIcon} />}
|
||||||
|
activeIcon={<Icon id='' icon={HomeActiveIcon} />}
|
||||||
|
/>
|
||||||
|
<IconLabelButton
|
||||||
|
title={intl.formatMessage(messages.search)}
|
||||||
|
to='/explore'
|
||||||
|
icon={<Icon id='' icon={SearchIcon} />}
|
||||||
|
/>
|
||||||
|
<IconLabelButton
|
||||||
|
title={intl.formatMessage(messages.publish)}
|
||||||
|
to='/publish'
|
||||||
|
icon={<Icon id='' icon={AddIcon} />}
|
||||||
|
/>
|
||||||
|
<NotificationsButton />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
className={classNames('ui__navigation-bar__item', { active: open })}
|
||||||
|
onClick={handleClick}
|
||||||
|
aria-label={intl.formatMessage(messages.menu)}
|
||||||
|
>
|
||||||
|
<Icon id='' icon={MenuIcon} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,206 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Component, useEffect } from 'react';
|
|
||||||
|
|
||||||
import { defineMessages, injectIntl, useIntl } from 'react-intl';
|
|
||||||
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
|
||||||
|
|
||||||
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 ExploreActiveIcon from '@/material-icons/400-24px/explore-fill.svg?react';
|
|
||||||
import ExploreIcon from '@/material-icons/400-24px/explore.svg?react';
|
|
||||||
import ModerationIcon from '@/material-icons/400-24px/gavel.svg?react';
|
|
||||||
import HomeActiveIcon from '@/material-icons/400-24px/home-fill.svg?react';
|
|
||||||
import HomeIcon from '@/material-icons/400-24px/home.svg?react';
|
|
||||||
import ListAltActiveIcon from '@/material-icons/400-24px/list_alt-fill.svg?react';
|
|
||||||
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
|
||||||
import AdministrationIcon from '@/material-icons/400-24px/manufacturing.svg?react';
|
|
||||||
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.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 SearchIcon from '@/material-icons/400-24px/search.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 { fetchFollowRequests } from 'mastodon/actions/accounts';
|
|
||||||
import { IconWithBadge } from 'mastodon/components/icon_with_badge';
|
|
||||||
import { WordmarkLogo } from 'mastodon/components/logo';
|
|
||||||
import { NavigationPortal } from 'mastodon/components/navigation_portal';
|
|
||||||
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
|
|
||||||
import { timelinePreview, trendsEnabled } from 'mastodon/initial_state';
|
|
||||||
import { transientSingleColumn } from 'mastodon/is_mobile';
|
|
||||||
import { canManageReports, canViewAdminDashboard } from 'mastodon/permissions';
|
|
||||||
import { selectUnreadNotificationGroupsCount } from 'mastodon/selectors/notifications';
|
|
||||||
|
|
||||||
import ColumnLink from './column_link';
|
|
||||||
import DisabledAccountBanner from './disabled_account_banner';
|
|
||||||
import { ListPanel } from './list_panel';
|
|
||||||
import SignInBanner from './sign_in_banner';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
home: { id: 'tabs_bar.home', defaultMessage: 'Home' },
|
|
||||||
notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' },
|
|
||||||
explore: { id: 'explore.title', defaultMessage: 'Explore' },
|
|
||||||
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' },
|
|
||||||
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
|
|
||||||
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
|
||||||
administration: { id: 'navigation_bar.administration', defaultMessage: 'Administration' },
|
|
||||||
moderation: { id: 'navigation_bar.moderation', defaultMessage: 'Moderation' },
|
|
||||||
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' },
|
|
||||||
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' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const NotificationsLink = () => {
|
|
||||||
|
|
||||||
const count = useSelector(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 = () => {
|
|
||||||
const count = useSelector(state => state.getIn(['user_lists', 'follow_requests', 'items'])?.size ?? 0);
|
|
||||||
const intl = useIntl();
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
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)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
class NavigationPanel extends Component {
|
|
||||||
static propTypes = {
|
|
||||||
identity: identityContextPropShape,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
isFirehoseActive = (match, location) => {
|
|
||||||
return match || location.pathname.startsWith('/public');
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { intl } = this.props;
|
|
||||||
const { signedIn, disabledAccountId, permissions } = this.props.identity;
|
|
||||||
|
|
||||||
let banner = undefined;
|
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='navigation-panel'>
|
|
||||||
<div className='navigation-panel__logo'>
|
|
||||||
<Link to='/' className='column-link column-link--logo'><WordmarkLogo /></Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{banner &&
|
|
||||||
<div className='navigation-panel__banner'>
|
|
||||||
{banner}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div className='navigation-panel__menu'>
|
|
||||||
{signedIn && (
|
|
||||||
<>
|
|
||||||
<ColumnLink transparent to='/home' icon='home' iconComponent={HomeIcon} activeIconComponent={HomeActiveIcon} text={intl.formatMessage(messages.home)} />
|
|
||||||
<NotificationsLink />
|
|
||||||
<FollowRequestsLink />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{trendsEnabled ? (
|
|
||||||
<ColumnLink transparent to='/explore' icon='explore' iconComponent={ExploreIcon} activeIconComponent={ExploreActiveIcon} text={intl.formatMessage(messages.explore)} />
|
|
||||||
) : (
|
|
||||||
<ColumnLink transparent to='/search' icon='search' iconComponent={SearchIcon} text={intl.formatMessage(messages.search)} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{(signedIn || timelinePreview) && (
|
|
||||||
<ColumnLink transparent to='/public/local' isActive={this.isFirehoseActive} icon='globe' iconComponent={PublicIcon} text={intl.formatMessage(messages.firehose)} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!signedIn && (
|
|
||||||
<div className='navigation-panel__sign-in-banner'>
|
|
||||||
<hr />
|
|
||||||
{ disabledAccountId ? <DisabledAccountBanner /> : <SignInBanner /> }
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{signedIn && (
|
|
||||||
<>
|
|
||||||
<ColumnLink transparent to='/conversations' icon='at' iconComponent={AlternateEmailIcon} text={intl.formatMessage(messages.direct)} />
|
|
||||||
<ColumnLink transparent to='/bookmarks' icon='bookmarks' iconComponent={BookmarksIcon} activeIconComponent={BookmarksActiveIcon} text={intl.formatMessage(messages.bookmarks)} />
|
|
||||||
<ColumnLink transparent to='/favourites' icon='star' iconComponent={StarIcon} activeIconComponent={StarActiveIcon} text={intl.formatMessage(messages.favourites)} />
|
|
||||||
<ColumnLink transparent to='/lists' icon='list-ul' iconComponent={ListAltIcon} activeIconComponent={ListAltActiveIcon} text={intl.formatMessage(messages.lists)} />
|
|
||||||
|
|
||||||
<ListPanel />
|
|
||||||
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
<ColumnLink transparent href='/settings/preferences' icon='cog' iconComponent={SettingsIcon} text={intl.formatMessage(messages.preferences)} />
|
|
||||||
|
|
||||||
{canManageReports(permissions) && <ColumnLink optional transparent href='/admin/reports' icon='flag' iconComponent={ModerationIcon} text={intl.formatMessage(messages.moderation)} />}
|
|
||||||
{canViewAdminDashboard(permissions) && <ColumnLink optional transparent href='/admin/dashboard' icon='tachometer' iconComponent={AdministrationIcon} text={intl.formatMessage(messages.administration)} />}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className='navigation-panel__legal'>
|
|
||||||
<hr />
|
|
||||||
<ColumnLink transparent to='/about' icon='ellipsis-h' iconComponent={MoreHorizIcon} text={intl.formatMessage(messages.about)} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex-spacer' />
|
|
||||||
|
|
||||||
<NavigationPortal />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default injectIntl(withIdentity(NavigationPanel));
|
|
|
@ -0,0 +1,495 @@
|
||||||
|
import { useEffect, useCallback, 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 ExploreActiveIcon from '@/material-icons/400-24px/explore-fill.svg?react';
|
||||||
|
import ExploreIcon from '@/material-icons/400-24px/explore.svg?react';
|
||||||
|
import ModerationIcon from '@/material-icons/400-24px/gavel.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 LogoutIcon from '@/material-icons/400-24px/logout.svg?react';
|
||||||
|
import AdministrationIcon from '@/material-icons/400-24px/manufacturing.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 SearchIcon from '@/material-icons/400-24px/search.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 { fetchFollowRequests } from 'mastodon/actions/accounts';
|
||||||
|
import { openModal } from 'mastodon/actions/modal';
|
||||||
|
import { openNavigation, closeNavigation } from 'mastodon/actions/navigation';
|
||||||
|
import { Account } from 'mastodon/components/account';
|
||||||
|
import { IconButton } from 'mastodon/components/icon_button';
|
||||||
|
import { IconWithBadge } from 'mastodon/components/icon_with_badge';
|
||||||
|
import { WordmarkLogo } from 'mastodon/components/logo';
|
||||||
|
import { NavigationPortal } from 'mastodon/components/navigation_portal';
|
||||||
|
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 { canManageReports, canViewAdminDashboard } from 'mastodon/permissions';
|
||||||
|
import { selectUnreadNotificationGroupsCount } from 'mastodon/selectors/notifications';
|
||||||
|
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||||
|
|
||||||
|
import { ColumnLink } from './column_link';
|
||||||
|
import DisabledAccountBanner from './disabled_account_banner';
|
||||||
|
import { ListPanel } from './list_panel';
|
||||||
|
import SignInBanner from './sign_in_banner';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
home: { id: 'tabs_bar.home', defaultMessage: 'Home' },
|
||||||
|
notifications: {
|
||||||
|
id: 'tabs_bar.notifications',
|
||||||
|
defaultMessage: 'Notifications',
|
||||||
|
},
|
||||||
|
explore: { id: 'explore.title', defaultMessage: 'Explore' },
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
administration: {
|
||||||
|
id: 'navigation_bar.administration',
|
||||||
|
defaultMessage: 'Administration',
|
||||||
|
},
|
||||||
|
moderation: { id: 'navigation_bar.moderation', defaultMessage: 'Moderation' },
|
||||||
|
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' },
|
||||||
|
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 SearchLink: React.FC = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const showAsSearch = useBreakpoint('full');
|
||||||
|
|
||||||
|
if (!trendsEnabled || showAsSearch) {
|
||||||
|
return (
|
||||||
|
<ColumnLink
|
||||||
|
transparent
|
||||||
|
to={trendsEnabled ? '/explore' : '/search'}
|
||||||
|
icon='search'
|
||||||
|
iconComponent={SearchIcon}
|
||||||
|
text={intl.formatMessage(messages.search)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ColumnLink
|
||||||
|
transparent
|
||||||
|
to='/explore'
|
||||||
|
icon='explore'
|
||||||
|
iconComponent={ExploreIcon}
|
||||||
|
activeIconComponent={ExploreActiveIcon}
|
||||||
|
text={intl.formatMessage(messages.explore)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProfileCard: React.FC = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const handleLogoutClick = useCallback(() => {
|
||||||
|
dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT', modalProps: {} }));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
if (!me) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='navigation-bar'>
|
||||||
|
<Account id={me} minimal size={36} />
|
||||||
|
<IconButton
|
||||||
|
icon='sign-out'
|
||||||
|
iconComponent={LogoutIcon}
|
||||||
|
title={intl.formatMessage(messages.logout)}
|
||||||
|
onClick={handleLogoutClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const MENU_WIDTH = 284;
|
||||||
|
|
||||||
|
export const NavigationPanel: React.FC = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const { signedIn, disabledAccountId, permissions } = useIdentity();
|
||||||
|
const open = useAppSelector((state) => state.navigation.open);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const openable = useBreakpoint('openable');
|
||||||
|
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 isFirehoseActive = useCallback(
|
||||||
|
(match: unknown, location: { pathname: string }): boolean => {
|
||||||
|
return !!match || location.pathname.startsWith('/public');
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
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 = undefined;
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
||||||
|
<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)}
|
||||||
|
/>
|
||||||
|
<NotificationsLink />
|
||||||
|
<FollowRequestsLink />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<SearchLink />
|
||||||
|
|
||||||
|
{(signedIn || timelinePreview) && (
|
||||||
|
<ColumnLink
|
||||||
|
transparent
|
||||||
|
to='/public/local'
|
||||||
|
isActive={isFirehoseActive}
|
||||||
|
icon='globe'
|
||||||
|
iconComponent={PublicIcon}
|
||||||
|
text={intl.formatMessage(messages.firehose)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!signedIn && (
|
||||||
|
<div className='navigation-panel__sign-in-banner'>
|
||||||
|
<hr />
|
||||||
|
{disabledAccountId ? (
|
||||||
|
<DisabledAccountBanner />
|
||||||
|
) : (
|
||||||
|
<SignInBanner />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{signedIn && (
|
||||||
|
<>
|
||||||
|
<ColumnLink
|
||||||
|
transparent
|
||||||
|
to='/conversations'
|
||||||
|
icon='at'
|
||||||
|
iconComponent={AlternateEmailIcon}
|
||||||
|
text={intl.formatMessage(messages.direct)}
|
||||||
|
/>
|
||||||
|
<ColumnLink
|
||||||
|
transparent
|
||||||
|
to='/bookmarks'
|
||||||
|
icon='bookmarks'
|
||||||
|
iconComponent={BookmarksIcon}
|
||||||
|
activeIconComponent={BookmarksActiveIcon}
|
||||||
|
text={intl.formatMessage(messages.bookmarks)}
|
||||||
|
/>
|
||||||
|
<ColumnLink
|
||||||
|
transparent
|
||||||
|
to='/favourites'
|
||||||
|
icon='star'
|
||||||
|
iconComponent={StarIcon}
|
||||||
|
activeIconComponent={StarActiveIcon}
|
||||||
|
text={intl.formatMessage(messages.favourites)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ListPanel />
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<ColumnLink
|
||||||
|
transparent
|
||||||
|
href='/settings/preferences'
|
||||||
|
icon='cog'
|
||||||
|
iconComponent={SettingsIcon}
|
||||||
|
text={intl.formatMessage(messages.preferences)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{canManageReports(permissions) && (
|
||||||
|
<ColumnLink
|
||||||
|
optional
|
||||||
|
transparent
|
||||||
|
href='/admin/reports'
|
||||||
|
icon='flag'
|
||||||
|
iconComponent={ModerationIcon}
|
||||||
|
text={intl.formatMessage(messages.moderation)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{canViewAdminDashboard(permissions) && (
|
||||||
|
<ColumnLink
|
||||||
|
optional
|
||||||
|
transparent
|
||||||
|
href='/admin/dashboard'
|
||||||
|
icon='tachometer'
|
||||||
|
iconComponent={AdministrationIcon}
|
||||||
|
text={intl.formatMessage(messages.administration)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className='navigation-panel__legal'>
|
||||||
|
<hr />
|
||||||
|
<ColumnLink
|
||||||
|
transparent
|
||||||
|
to='/about'
|
||||||
|
icon='ellipsis-h'
|
||||||
|
iconComponent={InfoIcon}
|
||||||
|
text={intl.formatMessage(messages.about)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex-spacer' />
|
||||||
|
|
||||||
|
<NavigationPortal />
|
||||||
|
</div>
|
||||||
|
</animated.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
53
app/javascript/mastodon/features/ui/hooks/useBreakpoint.tsx
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
const breakpoints = {
|
||||||
|
openable: 759, // Device width at which the sidebar becomes an openable hamburger menu
|
||||||
|
full: 1174, // Device width at which all 3 columns can be displayed
|
||||||
|
};
|
||||||
|
|
||||||
|
type Breakpoint = 'openable' | 'full';
|
||||||
|
|
||||||
|
export const useBreakpoint = (breakpoint: Breakpoint) => {
|
||||||
|
const [isMatching, setIsMatching] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const mediaWatcher = window.matchMedia(
|
||||||
|
`(max-width: ${breakpoints[breakpoint]}px)`,
|
||||||
|
);
|
||||||
|
|
||||||
|
setIsMatching(mediaWatcher.matches);
|
||||||
|
|
||||||
|
const handleChange = (e: MediaQueryListEvent) => {
|
||||||
|
setIsMatching(e.matches);
|
||||||
|
};
|
||||||
|
|
||||||
|
mediaWatcher.addEventListener('change', handleChange);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mediaWatcher.removeEventListener('change', handleChange);
|
||||||
|
};
|
||||||
|
}, [breakpoint, setIsMatching]);
|
||||||
|
|
||||||
|
return isMatching;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface WithBreakpointType {
|
||||||
|
matchesBreakpoint: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withBreakpoint<P>(
|
||||||
|
Component: React.ComponentType<P & WithBreakpointType>,
|
||||||
|
breakpoint: Breakpoint = 'full',
|
||||||
|
) {
|
||||||
|
const displayName = `withMobileLayout(${Component.displayName ?? Component.name})`;
|
||||||
|
|
||||||
|
const ComponentWithBreakpoint = (props: P) => {
|
||||||
|
const matchesBreakpoint = useBreakpoint(breakpoint);
|
||||||
|
|
||||||
|
return <Component matchesBreakpoint={matchesBreakpoint} {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
ComponentWithBreakpoint.displayName = displayName;
|
||||||
|
|
||||||
|
return ComponentWithBreakpoint;
|
||||||
|
}
|
|
@ -29,7 +29,7 @@ import { expandHomeTimeline } from '../../actions/timelines';
|
||||||
import initialState, { me, owner, singleUserMode, trendsEnabled, trendsAsLanding, disableHoverCards } from '../../initial_state';
|
import initialState, { me, owner, singleUserMode, trendsEnabled, trendsAsLanding, disableHoverCards } from '../../initial_state';
|
||||||
|
|
||||||
import BundleColumnError from './components/bundle_column_error';
|
import BundleColumnError from './components/bundle_column_error';
|
||||||
import Header from './components/header';
|
import { NavigationBar } from './components/navigation_bar';
|
||||||
import { UploadArea } from './components/upload_area';
|
import { UploadArea } from './components/upload_area';
|
||||||
import { HashtagMenuController } from './components/hashtag_menu_controller';
|
import { HashtagMenuController } from './components/hashtag_menu_controller';
|
||||||
import ColumnsAreaContainer from './containers/columns_area_container';
|
import ColumnsAreaContainer from './containers/columns_area_container';
|
||||||
|
@ -603,12 +603,11 @@ class UI extends PureComponent {
|
||||||
return (
|
return (
|
||||||
<HotKeys keyMap={keyMap} handlers={handlers} ref={this.setHotkeysRef} attach={window} focused>
|
<HotKeys keyMap={keyMap} handlers={handlers} ref={this.setHotkeysRef} attach={window} focused>
|
||||||
<div className={classNames('ui', { 'is-composing': isComposing })} ref={this.setRef}>
|
<div className={classNames('ui', { 'is-composing': isComposing })} ref={this.setRef}>
|
||||||
<Header />
|
|
||||||
|
|
||||||
<SwitchingColumnsArea identity={this.props.identity} location={location} singleColumn={layout === 'mobile' || layout === 'single-column'} forceOnboarding={firstLaunch && newAccount}>
|
<SwitchingColumnsArea identity={this.props.identity} location={location} singleColumn={layout === 'mobile' || layout === 'single-column'} forceOnboarding={firstLaunch && newAccount}>
|
||||||
{children}
|
{children}
|
||||||
</SwitchingColumnsArea>
|
</SwitchingColumnsArea>
|
||||||
|
|
||||||
|
<NavigationBar />
|
||||||
{layout !== 'mobile' && <PictureInPicture />}
|
{layout !== 'mobile' && <PictureInPicture />}
|
||||||
<AlertsController />
|
<AlertsController />
|
||||||
{!disableHoverCards && <HoverCardController />}
|
{!disableHoverCards && <HoverCardController />}
|
||||||
|
|
|
@ -207,7 +207,6 @@
|
||||||
"compose_form.poll.switch_to_single": "Change poll to allow for a single choice",
|
"compose_form.poll.switch_to_single": "Change poll to allow for a single choice",
|
||||||
"compose_form.poll.type": "Style",
|
"compose_form.poll.type": "Style",
|
||||||
"compose_form.publish": "Post",
|
"compose_form.publish": "Post",
|
||||||
"compose_form.publish_form": "New post",
|
|
||||||
"compose_form.reply": "Reply",
|
"compose_form.reply": "Reply",
|
||||||
"compose_form.save_changes": "Update",
|
"compose_form.save_changes": "Update",
|
||||||
"compose_form.spoiler.marked": "Remove content warning",
|
"compose_form.spoiler.marked": "Remove content warning",
|
||||||
|
@ -579,6 +578,8 @@
|
||||||
"navigation_bar.public_timeline": "Federated timeline",
|
"navigation_bar.public_timeline": "Federated timeline",
|
||||||
"navigation_bar.search": "Search",
|
"navigation_bar.search": "Search",
|
||||||
"navigation_bar.security": "Security",
|
"navigation_bar.security": "Security",
|
||||||
|
"navigation_panel.collapse_lists": "Collapse list menu",
|
||||||
|
"navigation_panel.expand_lists": "Expand list menu",
|
||||||
"not_signed_in_indicator.not_signed_in": "You need to login to access this resource.",
|
"not_signed_in_indicator.not_signed_in": "You need to login to access this resource.",
|
||||||
"notification.admin.report": "{name} reported {target}",
|
"notification.admin.report": "{name} reported {target}",
|
||||||
"notification.admin.report_account": "{name} reported {count, plural, one {one post} other {# posts}} from {target} for {category}",
|
"notification.admin.report_account": "{name} reported {count, plural, one {one post} other {# posts}} from {target} for {category}",
|
||||||
|
@ -907,7 +908,10 @@
|
||||||
"subscribed_languages.save": "Save changes",
|
"subscribed_languages.save": "Save changes",
|
||||||
"subscribed_languages.target": "Change subscribed languages for {target}",
|
"subscribed_languages.target": "Change subscribed languages for {target}",
|
||||||
"tabs_bar.home": "Home",
|
"tabs_bar.home": "Home",
|
||||||
|
"tabs_bar.menu": "Menu",
|
||||||
"tabs_bar.notifications": "Notifications",
|
"tabs_bar.notifications": "Notifications",
|
||||||
|
"tabs_bar.publish": "New Post",
|
||||||
|
"tabs_bar.search": "Search",
|
||||||
"terms_of_service.effective_as_of": "Effective as of {date}",
|
"terms_of_service.effective_as_of": "Effective as of {date}",
|
||||||
"terms_of_service.title": "Terms of Service",
|
"terms_of_service.title": "Terms of Service",
|
||||||
"terms_of_service.upcoming_changes_on": "Upcoming changes on {date}",
|
"terms_of_service.upcoming_changes_on": "Upcoming changes on {date}",
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { markersReducer } from './markers';
|
||||||
import media_attachments from './media_attachments';
|
import media_attachments from './media_attachments';
|
||||||
import meta from './meta';
|
import meta from './meta';
|
||||||
import { modalReducer } from './modal';
|
import { modalReducer } from './modal';
|
||||||
|
import { navigationReducer } from './navigation';
|
||||||
import { notificationGroupsReducer } from './notification_groups';
|
import { notificationGroupsReducer } from './notification_groups';
|
||||||
import { notificationPolicyReducer } from './notification_policy';
|
import { notificationPolicyReducer } from './notification_policy';
|
||||||
import { notificationRequestsReducer } from './notification_requests';
|
import { notificationRequestsReducer } from './notification_requests';
|
||||||
|
@ -76,6 +77,7 @@ const reducers = {
|
||||||
history,
|
history,
|
||||||
notificationPolicy: notificationPolicyReducer,
|
notificationPolicy: notificationPolicyReducer,
|
||||||
notificationRequests: notificationRequestsReducer,
|
notificationRequests: notificationRequestsReducer,
|
||||||
|
navigation: navigationReducer,
|
||||||
};
|
};
|
||||||
|
|
||||||
// We want the root state to be an ImmutableRecord, which is an object with a defined list of keys,
|
// We want the root state to be an ImmutableRecord, which is an object with a defined list of keys,
|
||||||
|
|
28
app/javascript/mastodon/reducers/navigation.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { createReducer } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
import {
|
||||||
|
openNavigation,
|
||||||
|
closeNavigation,
|
||||||
|
toggleNavigation,
|
||||||
|
} from 'mastodon/actions/navigation';
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
open: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: State = {
|
||||||
|
open: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const navigationReducer = createReducer(initialState, (builder) => {
|
||||||
|
builder
|
||||||
|
.addCase(openNavigation, (state) => {
|
||||||
|
state.open = true;
|
||||||
|
})
|
||||||
|
.addCase(closeNavigation, (state) => {
|
||||||
|
state.open = false;
|
||||||
|
})
|
||||||
|
.addCase(toggleNavigation, (state) => {
|
||||||
|
state.open = !state.open;
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,15 +1,16 @@
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import type { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||||
import type { Map as ImmutableMap } from 'immutable';
|
|
||||||
|
|
||||||
import type { List } from 'mastodon/models/list';
|
import type { List } from 'mastodon/models/list';
|
||||||
import type { RootState } from 'mastodon/store';
|
import { createAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
export const getOrderedLists = createSelector(
|
const getLists = createAppSelector(
|
||||||
[(state: RootState) => state.lists],
|
[(state) => state.lists],
|
||||||
(lists: ImmutableMap<string, List | null>) =>
|
(lists: ImmutableMap<string, List | null>): ImmutableList<List> =>
|
||||||
lists
|
lists.toList().filter((item: List | null): item is List => !!item),
|
||||||
.toList()
|
);
|
||||||
.filter((item: List | null) => !!item)
|
|
||||||
.sort((a: List, b: List) => a.title.localeCompare(b.title))
|
export const getOrderedLists = createAppSelector(
|
||||||
.toArray(),
|
[(state) => getLists(state)],
|
||||||
|
(lists) =>
|
||||||
|
lists.sort((a: List, b: List) => a.title.localeCompare(b.title)).toArray(),
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M560-280 360-480l200-200v400Z"/></svg>
|
After Width: | Height: | Size: 135 B |
1
app/javascript/material-icons/400-24px/arrow_left.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M560-280 360-480l200-200v400Z"/></svg>
|
After Width: | Height: | Size: 135 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M360-360v-170l367-367q12-12 27-18t30-6q16 0 30.5 6t26.5 18l56 57q11 12 17 26.5t6 29.5q0 15-5.5 29.5T897-728L530-360H360Zm424-368 57-56-56-56-57 56 56 56ZM200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h357L280-563v283h282l278-278v358q0 33-23.5 56.5T760-120H200Z"/></svg>
|
After Width: | Height: | Size: 379 B |
1
app/javascript/material-icons/400-24px/edit_square.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h357l-80 80H200v560h560v-278l80-80v358q0 33-23.5 56.5T760-120H200Zm280-360ZM360-360v-170l367-367q12-12 27-18t30-6q16 0 30.5 6t26.5 18l56 57q11 12 17 26.5t6 29.5q0 15-5.5 29.5T897-728L530-360H360Zm481-424-56-56 56 56ZM440-440h56l232-232-28-28-29-28-231 231v57Zm260-260-29-28 29 28 28 28-28-28Z"/></svg>
|
After Width: | Height: | Size: 458 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m356-160-56-56 180-180 180 180-56 56-124-124-124 124Zm124-404L300-744l56-56 124 124 124-124 56 56-180 180Z"/></svg>
|
After Width: | Height: | Size: 212 B |
1
app/javascript/material-icons/400-24px/unfold_less.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m356-160-56-56 180-180 180 180-56 56-124-124-124 124Zm124-404L300-744l56-56 124 124 124-124 56 56-180 180Z"/></svg>
|
After Width: | Height: | Size: 212 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-120 300-300l58-58 122 122 122-122 58 58-180 180ZM358-598l-58-58 180-180 180 180-58 58-122-122-122 122Z"/></svg>
|
After Width: | Height: | Size: 213 B |
1
app/javascript/material-icons/400-24px/unfold_more.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-120 300-300l58-58 122 122 122-122 58 58-180 180ZM358-598l-58-58 180-180 180 180-58 58-122-122-122 122Z"/></svg>
|
After Width: | Height: | Size: 213 B |
|
@ -2644,15 +2644,13 @@ a.account__display-name {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
||||||
&__display-name {
|
&__display-name {
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
line-height: 24px;
|
line-height: 20px;
|
||||||
letter-spacing: 0.15px;
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
||||||
.display-name__account {
|
.display-name__account {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 20px;
|
font-weight: 400;
|
||||||
letter-spacing: 0.1px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2889,67 +2887,69 @@ a.account__display-name {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$ui-header-height: 55px;
|
.ui__navigation-bar {
|
||||||
$ui-header-logo-wordmark-width: 99px;
|
|
||||||
|
|
||||||
.ui__header {
|
|
||||||
display: none;
|
|
||||||
box-sizing: border-box;
|
|
||||||
height: $ui-header-height;
|
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
bottom: 0;
|
||||||
z-index: 3;
|
background: var(--background-color);
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
backdrop-filter: var(--background-filter);
|
backdrop-filter: var(--background-filter);
|
||||||
|
border-top: 1px solid var(--background-border-color);
|
||||||
&__logo {
|
z-index: 3;
|
||||||
display: inline-flex;
|
|
||||||
padding: 15px;
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-shrink: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
container: header-logo / inline-size;
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
height: $ui-header-height - 30px;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo--wordmark {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@container header-logo (min-width: #{$ui-header-logo-wordmark-width}) {
|
|
||||||
.logo--wordmark {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo--icon {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__links {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
justify-content: space-between;
|
||||||
padding: 0 10px;
|
gap: 8px;
|
||||||
overflow: hidden;
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
.button {
|
.layout-multiple-columns & {
|
||||||
flex: 0 0 auto;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-tertiary {
|
&__items {
|
||||||
flex-shrink: 1;
|
display: grid;
|
||||||
|
grid-auto-columns: minmax(0, 1fr);
|
||||||
|
grid-auto-flow: column;
|
||||||
|
padding: 0 16px;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
&__sign-up {
|
||||||
width: 22px;
|
display: flex;
|
||||||
height: 22px;
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding-inline-start: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 16px;
|
||||||
|
padding-top: 11px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
border-top: 4px solid transparent;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: $highlight-text-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
border-top-color: $ui-button-focus-outline-color;
|
||||||
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2958,13 +2958,12 @@ $ui-header-logo-wordmark-width: 99px;
|
||||||
background: var(--background-color);
|
background: var(--background-color);
|
||||||
backdrop-filter: var(--background-filter);
|
backdrop-filter: var(--background-filter);
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: $ui-header-height;
|
top: 0;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
|
|
||||||
@media screen and (min-width: $no-gap-breakpoint) {
|
@media screen and (min-width: $no-gap-breakpoint) {
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
top: 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3133,8 +3132,10 @@ $ui-header-logo-wordmark-width: 99px;
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navigation-panel__legal {
|
.navigation-panel__legal,
|
||||||
display: none;
|
.navigation-panel__compose-button,
|
||||||
|
.navigation-panel .navigation-bar {
|
||||||
|
display: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3146,7 +3147,7 @@ $ui-header-logo-wordmark-width: 99px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.columns-area__panels {
|
.columns-area__panels {
|
||||||
min-height: calc(100vh - $ui-header-height);
|
min-height: 100vh;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3164,24 +3165,14 @@ $ui-header-logo-wordmark-width: 99px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navigation-panel__sign-in-banner,
|
.navigation-panel__sign-in-banner,
|
||||||
.navigation-panel__logo,
|
|
||||||
.navigation-panel__banner,
|
.navigation-panel__banner,
|
||||||
.getting-started__trends {
|
.getting-started__trends,
|
||||||
|
.navigation-panel__logo {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.column-link__icon {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout-single-column {
|
.layout-single-column {
|
||||||
.ui__header {
|
|
||||||
display: flex;
|
|
||||||
background: var(--background-color);
|
|
||||||
border-bottom: 1px solid var(--background-border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.column > .scrollable,
|
.column > .scrollable,
|
||||||
.tabs-bar__wrapper .column-header,
|
.tabs-bar__wrapper .column-header,
|
||||||
.tabs-bar__wrapper .column-back-button,
|
.tabs-bar__wrapper .column-back-button,
|
||||||
|
@ -3205,28 +3196,62 @@ $ui-header-logo-wordmark-width: 99px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: $no-gap-breakpoint - 285px - 1px) {
|
@media screen and (width <= 759px) {
|
||||||
$sidebar-width: 55px;
|
|
||||||
|
|
||||||
.columns-area__panels__main {
|
.columns-area__panels__main {
|
||||||
width: calc(100% - $sidebar-width);
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.columns-area__panels__pane--navigational {
|
.columns-area__panels__pane--navigational {
|
||||||
min-width: $sidebar-width;
|
position: fixed;
|
||||||
|
inset-inline-end: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.columns-area__panels__pane--navigational .columns-area__panels__pane__inner {
|
||||||
|
pointer-events: auto;
|
||||||
|
background: var(--background-color);
|
||||||
|
position: fixed;
|
||||||
|
width: 284px + 70px;
|
||||||
|
inset-inline-end: -70px;
|
||||||
|
touch-action: pan-y;
|
||||||
|
|
||||||
|
.navigation-panel {
|
||||||
|
width: 284px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
&__menu {
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-height: none;
|
||||||
|
overflow: hidden;
|
||||||
|
padding-bottom: calc(65px + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
&__logo {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.columns-area__panels__pane--navigational {
|
||||||
|
transition: background 500ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.columns-area__panels__pane--overlay {
|
||||||
|
pointer-events: auto;
|
||||||
|
background: rgba($base-overlay-background, 0.5);
|
||||||
|
|
||||||
.columns-area__panels__pane__inner {
|
.columns-area__panels__pane__inner {
|
||||||
width: $sidebar-width;
|
box-shadow: var(--dropdown-shadow);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.column-link span {
|
@media screen and (width >= 760px) {
|
||||||
|
.ui__navigation-bar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-panel {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.explore__suggestions__card {
|
.explore__suggestions__card {
|
||||||
|
@ -3455,6 +3480,49 @@ $ui-header-logo-wordmark-width: 99px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__list-panel {
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-inline-end: 12px;
|
||||||
|
|
||||||
|
.column-link {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__items {
|
||||||
|
padding-inline-start: 24px + 5px;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__compose-button {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
padding-inline-start: 13px - 7px;
|
||||||
|
padding-inline-end: 13px;
|
||||||
|
gap: 5px;
|
||||||
|
margin: 12px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation-bar {
|
||||||
|
padding: 16px;
|
||||||
|
border-bottom: 1px solid var(--background-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
height: 30px;
|
height: 30px;
|
||||||
width: auto;
|
width: auto;
|
||||||
|
@ -3487,12 +3555,6 @@ $ui-header-logo-wordmark-width: 99px;
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (height <= 1040px) {
|
|
||||||
.list-panel {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.navigation-panel,
|
.navigation-panel,
|
||||||
|
@ -4336,6 +4398,10 @@ a.status-card {
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
outline: $ui-button-icon-focus-outline;
|
outline: $ui-button-icon-focus-outline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.column-header__back-button + &__title {
|
.column-header__back-button + &__title {
|
||||||
|
@ -4419,10 +4485,6 @@ a.status-card {
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $primary-text-color;
|
color: $primary-text-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-sliders {
|
|
||||||
transform: rotate(60deg);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
|
|