[Glitch] Convert ColumnsArea component to TS

Port 1d21d9d4c2 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
diondiondion
2026-03-03 13:48:50 +01:00
committed by Claire
parent a4b7c9f933
commit 5f710e3edc
4 changed files with 192 additions and 197 deletions

View File

@@ -1,170 +0,0 @@
import PropTypes from 'prop-types';
import { Children, cloneElement, createContext, useContext, useCallback } from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { scrollRight } from '../../../scroll';
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';
import { CollapsibleNavigationPanel } from 'flavours/glitch/features/navigation_panel';
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,
};
const TabsBarPortal = () => {
const {setTabsBarElement} = useColumnsContext();
const setRef = useCallback((element) => {
if(element)
setTabsBarElement(element);
}, [setTabsBarElement]);
return <div id='tabs-bar__portal' ref={setRef} />;
};
// Simple context to allow column children to know which column they're in
export const ColumnIndexContext = createContext(1);
/**
* @returns {number}
*/
export const useColumnIndexContext = () => useContext(ColumnIndexContext);
export default class ColumnsArea extends ImmutablePureComponent {
static propTypes = {
columns: ImmutablePropTypes.list.isRequired,
isModalOpen: PropTypes.bool.isRequired,
singleColumn: PropTypes.bool,
children: PropTypes.node,
};
// Corresponds to (max-width: $no-gap-breakpoint - 1px) in SCSS
mediaQuery = 'matchMedia' in window && window.matchMedia('(max-width: 1174px)');
state = {
renderComposePanel: !(this.mediaQuery && this.mediaQuery.matches),
};
componentDidMount() {
if (this.mediaQuery) {
if (this.mediaQuery.addEventListener) {
this.mediaQuery.addEventListener('change', this.handleLayoutChange);
} else {
this.mediaQuery.addListener(this.handleLayoutChange);
}
this.setState({ renderComposePanel: !this.mediaQuery.matches });
}
this.isRtlLayout = document.getElementsByTagName('body')[0].classList.contains('rtl');
}
componentWillUnmount () {
if (this.mediaQuery) {
if (this.mediaQuery.removeEventListener) {
this.mediaQuery.removeEventListener('change', this.handleLayoutChange);
} else {
this.mediaQuery.removeListener(this.handleLayoutChange);
}
}
}
handleChildrenContentChange() {
if (!this.props.singleColumn) {
const modifier = this.isRtlLayout ? -1 : 1;
scrollRight(this.node, (this.node.scrollWidth - window.innerWidth) * modifier);
}
}
handleLayoutChange = (e) => {
this.setState({ renderComposePanel: !e.matches });
};
setRef = (node) => {
this.node = node;
};
renderLoading = columnId => () => {
return columnId === 'COMPOSE' ? <DrawerLoading /> : <ColumnLoading multiColumn />;
};
renderError = (props) => {
return <BundleColumnError multiColumn errorType='network' {...props} />;
};
render () {
const { columns, children, singleColumn, isModalOpen } = this.props;
const { renderComposePanel } = this.state;
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>
<div className='columns-area__panels__main'>
<div className='tabs-bar__wrapper'><TabsBarPortal /></div>
<div className='columns-area columns-area--mobile'>{children}</div>
</div>
<CollapsibleNavigationPanel />
</div>
);
}
return (
<div className={`columns-area ${ isModalOpen ? 'unscrollable' : '' }`} ref={this.setRef}>
{columns.map((column, index) => {
const params = column.get('params', null) === null ? null : column.get('params').toJS();
const other = params && params.other ? params.other : {};
return (
<ColumnIndexContext.Provider value={index} key={column.get('uuid')}>
<Bundle fetchComponent={componentMap[column.get('id')]} loading={this.renderLoading(column.get('id'))} error={this.renderError}>
{SpecificComponent => <SpecificComponent columnId={column.get('uuid')} params={params} multiColumn {...other} />}
</Bundle>
</ColumnIndexContext.Provider>
);
})}
<ColumnIndexContext.Provider value={columns.size}>
{Children.map(children, child => cloneElement(child, { multiColumn: true }))}
</ColumnIndexContext.Provider>
</div>
);
}
}

View File

@@ -0,0 +1,177 @@
import {
Children,
cloneElement,
createContext,
forwardRef,
useCallback,
useContext,
} from 'react';
import classNames from 'classnames';
import type { List, Record } from 'immutable';
import { useAppSelector } from '@/flavours/glitch/store';
import { CollapsibleNavigationPanel } from 'flavours/glitch/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 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} />;
};
export const ColumnIndexContext = createContext(1);
export const useColumnIndexContext = () => useContext(ColumnIndexContext);
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;
children: React.ReactElement | React.ReactElement[];
}
>(({ children, singleColumn }, ref) => {
const renderComposePanel = !useBreakpoint('full');
const columns = useAppSelector((state) =>
(state.settings as Record<{ columns: List<Record<Column>> }>).get(
'columns',
),
);
const isModalOpen = useAppSelector(
(state) => !state.modal.get('stack').isEmpty(),
);
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>
<div className='columns-area__panels__main'>
<div className='tabs-bar__wrapper'>
<TabsBarPortal />
</div>
<div className='columns-area columns-area--mobile'>{children}</div>
</div>
<CollapsibleNavigationPanel />
</div>
);
}
return (
<div
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) =>
cloneElement(child, { multiColumn: true }),
)}
</ColumnIndexContext.Provider>
</div>
);
});
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;
};

View File

@@ -1,23 +0,0 @@
import { connect } from 'react-redux';
import { openModal } from 'flavours/glitch/actions/modal';
import ColumnsArea from '../components/columns_area';
const mapStateToProps = state => ({
columns: state.getIn(['settings', 'columns']),
isModalOpen: !!state.get('modal').modalType,
});
const mapDispatchToProps = dispatch => ({
openSettings (e) {
e.preventDefault();
e.stopPropagation();
dispatch(openModal({
modalType: 'SETTINGS',
modalProps: {},
}));
},
});
export default connect(mapStateToProps, mapDispatchToProps, null, { forwardRef: true })(ColumnsArea);

View File

@@ -11,6 +11,7 @@ import { connect } from 'react-redux';
import Favico from 'favico.js';
import { debounce } from 'lodash';
import { scrollRight } from '../../scroll';
import { focusApp, unfocusApp, changeLayout } from 'flavours/glitch/actions/app';
import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'flavours/glitch/actions/markers';
import { fetchNotifications } from 'flavours/glitch/actions/notification_groups';
@@ -37,7 +38,7 @@ import BundleColumnError from './components/bundle_column_error';
import { NavigationBar } from './components/navigation_bar';
import { UploadArea } from './components/upload_area';
import { HashtagMenuController } from './components/hashtag_menu_controller';
import ColumnsAreaContainer from './containers/columns_area_container';
import { ColumnsArea } from './components/columns_area';
import LoadingBarContainer from './containers/loading_bar_container';
import ModalContainer from './containers/modal_container';
import {
@@ -133,7 +134,7 @@ class SwitchingColumnsArea extends PureComponent {
componentDidUpdate (prevProps) {
if (![this.props.location.pathname, '/'].includes(prevProps.location.pathname)) {
this.node.handleChildrenContentChange();
this.handleChildrenContentChange();
}
if (prevProps.singleColumn !== this.props.singleColumn) {
@@ -142,6 +143,16 @@ class SwitchingColumnsArea extends PureComponent {
}
}
handleChildrenContentChange() {
if (!this.props.singleColumn) {
const isRtlLayout = document.getElementsByTagName('body')[0]
?.classList.contains('rtl');
const modifier = isRtlLayout ? -1 : 1;
scrollRight(this.node, (this.node.scrollWidth - window.innerWidth) * modifier);
}
}
setRef = c => {
if (c) {
this.node = c;
@@ -189,7 +200,7 @@ class SwitchingColumnsArea extends PureComponent {
return (
<ColumnsContextProvider multiColumn={!singleColumn}>
<ColumnsAreaContainer ref={this.setRef} singleColumn={singleColumn}>
<ColumnsArea ref={this.setRef} singleColumn={singleColumn}>
<WrappedSwitch>
{redirect}
@@ -269,7 +280,7 @@ class SwitchingColumnsArea extends PureComponent {
}
<Route component={BundleColumnError} />
</WrappedSwitch>
</ColumnsAreaContainer>
</ColumnsArea>
</ColumnsContextProvider>
);
}