From 1d21d9d4c2266eea50b422fe1e2c6c8e0c793712 Mon Sep 17 00:00:00 2001 From: diondiondion Date: Tue, 3 Mar 2026 13:48:50 +0100 Subject: [PATCH] Convert `ColumnsArea` component to TS (#38031) --- .../features/ui/components/columns_area.jsx | 170 ----------------- .../features/ui/components/columns_area.tsx | 177 ++++++++++++++++++ .../ui/containers/columns_area_container.js | 10 - app/javascript/mastodon/features/ui/index.jsx | 19 +- 4 files changed, 192 insertions(+), 184 deletions(-) delete mode 100644 app/javascript/mastodon/features/ui/components/columns_area.jsx create mode 100644 app/javascript/mastodon/features/ui/components/columns_area.tsx delete mode 100644 app/javascript/mastodon/features/ui/containers/columns_area_container.js diff --git a/app/javascript/mastodon/features/ui/components/columns_area.jsx b/app/javascript/mastodon/features/ui/components/columns_area.jsx deleted file mode 100644 index 5609a52b31..0000000000 --- a/app/javascript/mastodon/features/ui/components/columns_area.jsx +++ /dev/null @@ -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 'mastodon/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
; -}; - -// 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' ? : ; - }; - - renderError = (props) => { - return ; - }; - - render () { - const { columns, children, singleColumn, isModalOpen } = this.props; - const { renderComposePanel } = this.state; - - if (singleColumn) { - return ( -
-
-
- {renderComposePanel && } - -
-
- -
-
-
{children}
-
- - -
- ); - } - - return ( -
- {columns.map((column, index) => { - const params = column.get('params', null) === null ? null : column.get('params').toJS(); - const other = params && params.other ? params.other : {}; - - return ( - - - {SpecificComponent => } - - - ); - })} - - - {Children.map(children, child => cloneElement(child, { multiColumn: true }))} - -
- ); - } - -} diff --git a/app/javascript/mastodon/features/ui/components/columns_area.tsx b/app/javascript/mastodon/features/ui/components/columns_area.tsx new file mode 100644 index 0000000000..6861410367 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/columns_area.tsx @@ -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 '@/mastodon/store'; +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 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
; +}; + +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> }>).get( + 'columns', + ), + ); + const isModalOpen = useAppSelector( + (state) => !state.modal.get('stack').isEmpty(), + ); + + if (singleColumn) { + return ( +
+
+
+ {renderComposePanel && } + +
+
+ +
+
+ +
+
{children}
+
+ + +
+ ); + } + + 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 ( + + + {(SpecificComponent: FetchedComponent) => ( + + )} + + + ); + })} + + + {Children.map(children, (child) => + cloneElement(child, { multiColumn: true }), + )} + +
+ ); +}); + +ColumnsArea.displayName = 'ColumnsArea'; + +const ErrorComponent = (props: { onRetry: () => void }) => { + return ; +}; + +const renderLoading = (columnId: string) => { + const LoadingComponent = + columnId === 'COMPOSE' ? : ; + return () => LoadingComponent; +}; diff --git a/app/javascript/mastodon/features/ui/containers/columns_area_container.js b/app/javascript/mastodon/features/ui/containers/columns_area_container.js deleted file mode 100644 index f8473d38ba..0000000000 --- a/app/javascript/mastodon/features/ui/containers/columns_area_container.js +++ /dev/null @@ -1,10 +0,0 @@ -import { connect } from 'react-redux'; - -import ColumnsArea from '../components/columns_area'; - -const mapStateToProps = state => ({ - columns: state.getIn(['settings', 'columns']), - isModalOpen: !!state.get('modal').modalType, -}); - -export default connect(mapStateToProps, null, null, { forwardRef: true })(ColumnsArea); diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index b46f61745e..55bc8f9901 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -10,6 +10,7 @@ import { connect } from 'react-redux'; import { debounce } from 'lodash'; +import { scrollRight } from '../../scroll'; import { focusApp, unfocusApp, changeLayout } from 'mastodon/actions/app'; import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'mastodon/actions/markers'; import { fetchNotifications } from 'mastodon/actions/notification_groups'; @@ -34,7 +35,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 { @@ -125,7 +126,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) { @@ -134,6 +135,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; @@ -181,7 +192,7 @@ class SwitchingColumnsArea extends PureComponent { return ( - + {redirect} @@ -261,7 +272,7 @@ class SwitchingColumnsArea extends PureComponent { } - + ); }