mirror of
https://github.com/mastodon/mastodon.git
synced 2026-09-13 01:06:15 -05:00
Redesign: Remove old navigation in single-column mode (#40249)
This commit is contained in:
@@ -61,6 +61,7 @@
|
||||
}
|
||||
|
||||
.label {
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.list {
|
||||
|
||||
@@ -1,213 +0,0 @@
|
||||
import {
|
||||
Children,
|
||||
cloneElement,
|
||||
forwardRef,
|
||||
isValidElement,
|
||||
lazy,
|
||||
Suspense,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import type { List, Record } from 'immutable';
|
||||
|
||||
import { ColumnIndexContext } from '@/mastodon/components/column/context';
|
||||
import { useAppSelector } from '@/mastodon/store';
|
||||
import { isRedesignEnabled } from '@/mastodon/utils/environment';
|
||||
import { Footer } from 'mastodon/features/custom_homepage/components/footer';
|
||||
import { Header } from 'mastodon/features/custom_homepage/components/header';
|
||||
import { CollapsibleNavigationPanel } from 'mastodon/features/navigation_panel';
|
||||
|
||||
import { useBreakpoint } from '../hooks/useBreakpoint';
|
||||
import {
|
||||
Compose,
|
||||
Notifications,
|
||||
HomeTimeline,
|
||||
CommunityTimeline,
|
||||
PublicTimeline,
|
||||
HashtagTimeline,
|
||||
DirectTimeline,
|
||||
FavouritedStatuses,
|
||||
BookmarkedStatuses,
|
||||
ListTimeline,
|
||||
Directory,
|
||||
} from '../util/async-components';
|
||||
import { useColumnsContext } from '../util/columns_context';
|
||||
|
||||
import Bundle from './bundle';
|
||||
import { BundleColumnError } from './bundle_column_error';
|
||||
import { ColumnLoading } from './column_loading';
|
||||
import { ComposePanel, RedirectToMobileComposeIfNeeded } from './compose_panel';
|
||||
import DrawerLoading from './drawer_loading';
|
||||
|
||||
const LazyRedesignNavigationPanel = lazy(() =>
|
||||
import('@/mastodon/features/navigation_panel/redesign').then(
|
||||
({ RedesignNavigationPanel }) => ({ default: RedesignNavigationPanel }),
|
||||
),
|
||||
);
|
||||
|
||||
const componentMap = {
|
||||
COMPOSE: Compose,
|
||||
HOME: HomeTimeline,
|
||||
NOTIFICATIONS: Notifications,
|
||||
PUBLIC: PublicTimeline,
|
||||
REMOTE: PublicTimeline,
|
||||
COMMUNITY: CommunityTimeline,
|
||||
HASHTAG: HashtagTimeline,
|
||||
DIRECT: DirectTimeline,
|
||||
FAVOURITES: FavouritedStatuses,
|
||||
BOOKMARKS: BookmarkedStatuses,
|
||||
LIST: ListTimeline,
|
||||
DIRECTORY: Directory,
|
||||
} as const;
|
||||
|
||||
const TabsBarPortal = () => {
|
||||
const { setTabsBarElement } = useColumnsContext();
|
||||
|
||||
const setRef = useCallback(
|
||||
(element: HTMLDivElement | null) => {
|
||||
if (element) {
|
||||
setTabsBarElement(element);
|
||||
}
|
||||
},
|
||||
[setTabsBarElement],
|
||||
);
|
||||
|
||||
return <div id='tabs-bar__portal' ref={setRef} />;
|
||||
};
|
||||
|
||||
interface Column {
|
||||
uuid: string;
|
||||
id: keyof typeof componentMap;
|
||||
params?: null | Record<{ other?: unknown }>;
|
||||
}
|
||||
|
||||
type FetchedComponent = React.FC<{
|
||||
columnId?: string;
|
||||
multiColumn?: boolean;
|
||||
params: unknown;
|
||||
}>;
|
||||
|
||||
export const ColumnsArea = forwardRef<
|
||||
HTMLDivElement,
|
||||
{
|
||||
singleColumn?: boolean;
|
||||
minimalShell?: boolean;
|
||||
children: React.ReactElement | React.ReactElement[];
|
||||
}
|
||||
>(({ children, minimalShell, singleColumn }, ref) => {
|
||||
const renderComposePanel = !useBreakpoint('full');
|
||||
const columns = useAppSelector(
|
||||
(state) => state.settings.get('columns') as List<Record<Column>>,
|
||||
);
|
||||
const isModalOpen = useAppSelector(
|
||||
(state) => !state.modal.get('stack').isEmpty(),
|
||||
);
|
||||
|
||||
if (minimalShell) {
|
||||
return (
|
||||
<div className='columns-area__panels'>
|
||||
<div className='columns-area__panels__main'>
|
||||
<Header />
|
||||
|
||||
<div className='tabs-bar__wrapper'>
|
||||
<TabsBarPortal />
|
||||
</div>
|
||||
|
||||
<div className='columns-area columns-area--mobile'>{children}</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (singleColumn) {
|
||||
return (
|
||||
<div className='columns-area__panels'>
|
||||
<div className='columns-area__panels__pane columns-area__panels__pane--compositional'>
|
||||
<div className='columns-area__panels__pane__inner'>
|
||||
{isRedesignEnabled() ? (
|
||||
<Suspense>
|
||||
<LazyRedesignNavigationPanel />
|
||||
</Suspense>
|
||||
) : (
|
||||
<>
|
||||
{renderComposePanel && <ComposePanel />}
|
||||
<RedirectToMobileComposeIfNeeded />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main className='columns-area__panels__main'>
|
||||
<div className='tabs-bar__wrapper'>
|
||||
<TabsBarPortal />
|
||||
</div>
|
||||
|
||||
<div className='columns-area columns-area--mobile'>{children}</div>
|
||||
</main>
|
||||
|
||||
<CollapsibleNavigationPanel />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main
|
||||
className={classNames('columns-area', { unscrollable: isModalOpen })}
|
||||
ref={ref}
|
||||
tabIndex={isModalOpen ? undefined : 0}
|
||||
>
|
||||
{columns.map((column, index) => {
|
||||
const params = column.get('params')
|
||||
? column.get('params')?.toJS()
|
||||
: null;
|
||||
const other = params?.other ?? {};
|
||||
const uuid = column.get('uuid');
|
||||
const id = column.get('id');
|
||||
|
||||
return (
|
||||
<ColumnIndexContext.Provider value={index} key={uuid}>
|
||||
<Bundle
|
||||
key={uuid}
|
||||
fetchComponent={componentMap[id]}
|
||||
loading={renderLoading(id)}
|
||||
error={ErrorComponent}
|
||||
>
|
||||
{(SpecificComponent: FetchedComponent) => (
|
||||
<SpecificComponent
|
||||
columnId={uuid}
|
||||
params={params}
|
||||
multiColumn
|
||||
{...other}
|
||||
/>
|
||||
)}
|
||||
</Bundle>
|
||||
</ColumnIndexContext.Provider>
|
||||
);
|
||||
})}
|
||||
|
||||
<ColumnIndexContext.Provider value={columns.size}>
|
||||
{Children.map(children, (child) =>
|
||||
isValidElement<{ multiColumn?: boolean }>(child)
|
||||
? cloneElement(child, { multiColumn: true })
|
||||
: child,
|
||||
)}
|
||||
</ColumnIndexContext.Provider>
|
||||
</main>
|
||||
);
|
||||
});
|
||||
|
||||
ColumnsArea.displayName = 'ColumnsArea';
|
||||
|
||||
const ErrorComponent = (props: { onRetry: () => void }) => {
|
||||
return <BundleColumnError multiColumn errorType='network' {...props} />;
|
||||
};
|
||||
|
||||
const renderLoading = (columnId: string) => {
|
||||
const LoadingComponent =
|
||||
columnId === 'COMPOSE' ? <DrawerLoading /> : <ColumnLoading multiColumn />;
|
||||
return () => LoadingComponent;
|
||||
};
|
||||
@@ -0,0 +1,122 @@
|
||||
import { lazy, Suspense, useCallback } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { LoadingIndicator } from '@/mastodon/components/loading_indicator';
|
||||
import { Footer } from '@/mastodon/features/custom_homepage/components/footer';
|
||||
import { Header } from '@/mastodon/features/custom_homepage/components/header';
|
||||
import { CollapsibleNavigationPanel } from '@/mastodon/features/navigation_panel';
|
||||
import { useBreakpoint } from '@/mastodon/features/ui/hooks/useBreakpoint';
|
||||
import { useColumnsContext } from '@/mastodon/features/ui/util/columns_context';
|
||||
import { useAppSelector } from '@/mastodon/store';
|
||||
import { isRedesignEnabled } from '@/mastodon/utils/environment';
|
||||
|
||||
import {
|
||||
ComposePanel,
|
||||
RedirectToMobileComposeIfNeeded,
|
||||
} from '../compose_panel';
|
||||
|
||||
import { MultiColumnContent } from './multi_column_content';
|
||||
|
||||
const LazyColumnsAreaRedesign = lazy(() =>
|
||||
import('@/mastodon/features/ui/components/columns_area/redesign').then(
|
||||
({ ColumnsAreaRedesign }) => ({ default: ColumnsAreaRedesign }),
|
||||
),
|
||||
);
|
||||
|
||||
const TabsBarPortal = () => {
|
||||
const { setTabsBarElement } = useColumnsContext();
|
||||
|
||||
const setRef = useCallback(
|
||||
(element: HTMLDivElement | null) => {
|
||||
if (element) {
|
||||
setTabsBarElement(element);
|
||||
}
|
||||
},
|
||||
[setTabsBarElement],
|
||||
);
|
||||
|
||||
return <div id='tabs-bar__portal' ref={setRef} />;
|
||||
};
|
||||
|
||||
interface ColumnsAreaProps {
|
||||
singleColumn?: boolean;
|
||||
minimalShell?: boolean;
|
||||
children: React.ReactElement | React.ReactElement[];
|
||||
ref?: React.Ref<HTMLDivElement>;
|
||||
}
|
||||
|
||||
const ColumnsAreaLegacy: React.FC<ColumnsAreaProps> = ({
|
||||
children,
|
||||
minimalShell,
|
||||
singleColumn,
|
||||
ref,
|
||||
}) => {
|
||||
const renderComposePanel = !useBreakpoint('full');
|
||||
const isModalOpen = useAppSelector(
|
||||
(state) => !state.modal.get('stack').isEmpty(),
|
||||
);
|
||||
|
||||
if (minimalShell) {
|
||||
return (
|
||||
<div className='columns-area__panels'>
|
||||
<div className='columns-area__panels__main'>
|
||||
<Header />
|
||||
|
||||
<div className='tabs-bar__wrapper'>
|
||||
<TabsBarPortal />
|
||||
</div>
|
||||
|
||||
<div className='columns-area columns-area--mobile'>{children}</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (singleColumn) {
|
||||
return (
|
||||
<div className='columns-area__panels'>
|
||||
<div className='columns-area__panels__pane columns-area__panels__pane--compositional'>
|
||||
<div className='columns-area__panels__pane__inner'>
|
||||
{renderComposePanel && <ComposePanel />}
|
||||
<RedirectToMobileComposeIfNeeded />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main className='columns-area__panels__main'>
|
||||
<div className='tabs-bar__wrapper'>
|
||||
<TabsBarPortal />
|
||||
</div>
|
||||
|
||||
<div className='columns-area columns-area--mobile'>{children}</div>
|
||||
</main>
|
||||
|
||||
<CollapsibleNavigationPanel />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main
|
||||
className={classNames('columns-area', { unscrollable: isModalOpen })}
|
||||
ref={ref}
|
||||
tabIndex={isModalOpen ? undefined : 0}
|
||||
>
|
||||
<MultiColumnContent>{children}</MultiColumnContent>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export const ColumnsArea: React.FC<ColumnsAreaProps> = (props) => {
|
||||
if (isRedesignEnabled()) {
|
||||
return (
|
||||
<Suspense fallback={<LoadingIndicator />}>
|
||||
<LazyColumnsAreaRedesign {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
} else {
|
||||
return <ColumnsAreaLegacy {...props} />;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,109 @@
|
||||
import { Children, cloneElement, isValidElement } from 'react';
|
||||
|
||||
import type { List, Record } from 'immutable';
|
||||
|
||||
import { ColumnIndexContext } from '@/mastodon/components/column/context';
|
||||
import { useAppSelector } from '@/mastodon/store';
|
||||
|
||||
import {
|
||||
Compose,
|
||||
Notifications,
|
||||
HomeTimeline,
|
||||
CommunityTimeline,
|
||||
PublicTimeline,
|
||||
HashtagTimeline,
|
||||
DirectTimeline,
|
||||
FavouritedStatuses,
|
||||
BookmarkedStatuses,
|
||||
ListTimeline,
|
||||
Directory,
|
||||
} from '../../util/async-components';
|
||||
import Bundle from '../bundle';
|
||||
import { BundleColumnError } from '../bundle_column_error';
|
||||
import { ColumnLoading } from '../column_loading';
|
||||
import DrawerLoading from '../drawer_loading';
|
||||
|
||||
const componentMap = {
|
||||
COMPOSE: Compose,
|
||||
HOME: HomeTimeline,
|
||||
NOTIFICATIONS: Notifications,
|
||||
PUBLIC: PublicTimeline,
|
||||
REMOTE: PublicTimeline,
|
||||
COMMUNITY: CommunityTimeline,
|
||||
HASHTAG: HashtagTimeline,
|
||||
DIRECT: DirectTimeline,
|
||||
FAVOURITES: FavouritedStatuses,
|
||||
BOOKMARKS: BookmarkedStatuses,
|
||||
LIST: ListTimeline,
|
||||
DIRECTORY: Directory,
|
||||
} as const;
|
||||
|
||||
interface Column {
|
||||
uuid: string;
|
||||
id: keyof typeof componentMap;
|
||||
params?: null | Record<{ other?: unknown }>;
|
||||
}
|
||||
|
||||
type FetchedComponent = React.FC<{
|
||||
columnId?: string;
|
||||
multiColumn?: boolean;
|
||||
params: unknown;
|
||||
}>;
|
||||
|
||||
const ErrorComponent = (props: { onRetry: () => void }) => (
|
||||
<BundleColumnError multiColumn errorType='network' {...props} />
|
||||
);
|
||||
|
||||
const renderLoading = (columnId: string) => {
|
||||
const LoadingComponent =
|
||||
columnId === 'COMPOSE' ? <DrawerLoading /> : <ColumnLoading multiColumn />;
|
||||
return () => LoadingComponent;
|
||||
};
|
||||
|
||||
export const MultiColumnContent: React.FC<{
|
||||
children: React.ReactElement | React.ReactElement[];
|
||||
}> = ({ children }) => {
|
||||
const columns = useAppSelector(
|
||||
(state) => state.settings.get('columns') as List<Record<Column>>,
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{columns.map((column, index) => {
|
||||
const params = column.get('params')
|
||||
? column.get('params')?.toJS()
|
||||
: null;
|
||||
const other = params?.other ?? {};
|
||||
const uuid = column.get('uuid');
|
||||
const id = column.get('id');
|
||||
|
||||
return (
|
||||
<ColumnIndexContext.Provider value={index} key={uuid}>
|
||||
<Bundle
|
||||
key={uuid}
|
||||
fetchComponent={componentMap[id]}
|
||||
loading={renderLoading(id)}
|
||||
error={ErrorComponent}
|
||||
>
|
||||
{(SpecificComponent: FetchedComponent) => (
|
||||
<SpecificComponent
|
||||
columnId={uuid}
|
||||
params={params}
|
||||
multiColumn
|
||||
{...other}
|
||||
/>
|
||||
)}
|
||||
</Bundle>
|
||||
</ColumnIndexContext.Provider>
|
||||
);
|
||||
})}
|
||||
|
||||
<ColumnIndexContext.Provider value={columns.size}>
|
||||
{Children.map(children, (child) =>
|
||||
isValidElement<{ multiColumn?: boolean }>(child)
|
||||
? cloneElement(child, { multiColumn: true })
|
||||
: child,
|
||||
)}
|
||||
</ColumnIndexContext.Provider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
@use '@/styles/mastodon/variables' as *;
|
||||
|
||||
.root {
|
||||
--main-column-max-width: 640px;
|
||||
--navigation-min-width: 280px;
|
||||
--navigation-max-width: 360px;
|
||||
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
|
||||
@media (min-width: $no-gap-breakpoint) {
|
||||
padding-inline: var(--space-2xs);
|
||||
}
|
||||
|
||||
@media (min-width: $mobile-menu-breakpoint) {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
minmax(var(--navigation-min-width), 1fr)
|
||||
minmax(0, var(--main-column-max-width))
|
||||
minmax(0, 1fr);
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
}
|
||||
|
||||
.navigationWrapper {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
max-width: var(--navigation-max-width);
|
||||
height: 100dvh;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
|
||||
@media (max-width: ($mobile-menu-breakpoint - 1)) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 0 1 auto;
|
||||
width: 100%;
|
||||
contain: inline-size layout paint style;
|
||||
container: column / inline-size;
|
||||
color: var(--color-text-primary);
|
||||
background-color: var(--color-bg-primary);
|
||||
|
||||
@media ($mobile-menu-breakpoint < width < $no-gap-breakpoint) {
|
||||
border-inline: 1px solid var(--color-border-primary);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { CollapsibleNavigationPanel } from '@/mastodon/features/navigation_panel';
|
||||
import { RedesignNavigationPanel } from '@/mastodon/features/navigation_panel/redesign';
|
||||
import { useAppSelector } from '@/mastodon/store';
|
||||
import { Footer } from 'mastodon/features/custom_homepage/components/footer';
|
||||
import { Header } from 'mastodon/features/custom_homepage/components/header';
|
||||
|
||||
import { useBreakpoint } from '../../hooks/useBreakpoint';
|
||||
import { useColumnsContext } from '../../util/columns_context';
|
||||
|
||||
import { MultiColumnContent } from './multi_column_content';
|
||||
import classes from './redesign.module.scss';
|
||||
|
||||
const TabsBarPortal = () => {
|
||||
const { setTabsBarElement } = useColumnsContext();
|
||||
|
||||
const setRef = useCallback(
|
||||
(element: HTMLDivElement | null) => {
|
||||
if (element) {
|
||||
setTabsBarElement(element);
|
||||
}
|
||||
},
|
||||
[setTabsBarElement],
|
||||
);
|
||||
|
||||
return <div id='tabs-bar__portal' ref={setRef} />;
|
||||
};
|
||||
|
||||
export const ColumnsAreaRedesign: React.FC<{
|
||||
singleColumn?: boolean;
|
||||
minimalShell?: boolean;
|
||||
children: React.ReactElement | React.ReactElement[];
|
||||
ref?: React.Ref<HTMLDivElement>;
|
||||
}> = ({ children, minimalShell, singleColumn, ref }) => {
|
||||
const isModalOpen = useAppSelector(
|
||||
(state) => !state.modal.get('stack').isEmpty(),
|
||||
);
|
||||
const renderLegacyNavForMobile = useBreakpoint('openable');
|
||||
|
||||
if (minimalShell) {
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<div className={classes.main}>
|
||||
<Header />
|
||||
|
||||
<div className='tabs-bar__wrapper'>
|
||||
<TabsBarPortal />
|
||||
</div>
|
||||
|
||||
<div className='columns-area columns-area--mobile'>{children}</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (singleColumn) {
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<div className={classes.navigationWrapper}>
|
||||
<RedesignNavigationPanel />
|
||||
</div>
|
||||
|
||||
<main className={classes.main}>
|
||||
<div className='tabs-bar__wrapper'>
|
||||
<TabsBarPortal />
|
||||
</div>
|
||||
|
||||
<div className='columns-area columns-area--mobile'>{children}</div>
|
||||
|
||||
{renderLegacyNavForMobile && <CollapsibleNavigationPanel />}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main
|
||||
className={classNames('columns-area', { unscrollable: isModalOpen })}
|
||||
ref={ref}
|
||||
tabIndex={isModalOpen ? undefined : 0}
|
||||
>
|
||||
<MultiColumnContent>{children}</MultiColumnContent>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user