diff --git a/app/javascript/flavours/glitch/components/column_back_button.jsx b/app/javascript/flavours/glitch/components/column_back_button.jsx deleted file mode 100644 index 8d9f541049..0000000000 --- a/app/javascript/flavours/glitch/components/column_back_button.jsx +++ /dev/null @@ -1,65 +0,0 @@ -import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; -import { createPortal } from 'react-dom'; - -import { FormattedMessage } from 'react-intl'; - -import { withRouter } from 'react-router-dom'; - -import { ReactComponent as ArrowBackIcon } from '@material-symbols/svg-600/outlined/arrow_back.svg'; - -import { Icon } from 'flavours/glitch/components/icon'; -import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router'; - -export class ColumnBackButton extends PureComponent { - - static propTypes = { - multiColumn: PropTypes.bool, - onClick: PropTypes.func, - ...WithRouterPropTypes, - }; - - handleClick = () => { - const { onClick, history } = this.props; - - if (onClick) { - onClick(); - } else if (history.location?.state?.fromMastodon) { - history.goBack(); - } else { - history.push('/'); - } - }; - - render () { - const { multiColumn } = this.props; - - const component = ( - - ); - - if (multiColumn) { - return component; - } else { - // The portal container and the component may be rendered to the DOM in - // the same React render pass, so the container might not be available at - // the time `render()` is called. - const container = document.getElementById('tabs-bar__portal'); - if (container === null) { - // The container wasn't available, force a re-render so that the - // component can eventually be inserted in the container and not scroll - // with the rest of the area. - this.forceUpdate(); - return component; - } else { - return createPortal(component, container); - } - } - } - -} - -export default withRouter(ColumnBackButton); diff --git a/app/javascript/flavours/glitch/components/column_back_button.tsx b/app/javascript/flavours/glitch/components/column_back_button.tsx new file mode 100644 index 0000000000..3c71a9ce5f --- /dev/null +++ b/app/javascript/flavours/glitch/components/column_back_button.tsx @@ -0,0 +1,70 @@ +import { useCallback } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import { ReactComponent as ArrowBackIcon } from '@material-symbols/svg-600/outlined/arrow_back.svg'; + +import { Icon } from 'flavours/glitch/components/icon'; +import { ButtonInTabsBar } from 'flavours/glitch/features/ui/util/columns_context'; + +import { useAppHistory } from './router'; + +type OnClickCallback = () => void; + +function useHandleClick(onClick?: OnClickCallback) { + const history = useAppHistory(); + + return useCallback(() => { + if (onClick) { + onClick(); + } else if (history.location.state?.fromMastodon) { + history.goBack(); + } else { + history.push('/'); + } + }, [history, onClick]); +} + +export const ColumnBackButton: React.FC<{ onClick: OnClickCallback }> = ({ + onClick, +}) => { + const handleClick = useHandleClick(onClick); + + const component = ( + + ); + + return {component}; +}; + +export const ColumnBackButtonSlim: React.FC<{ onClick: OnClickCallback }> = ({ + onClick, +}) => { + const handleClick = useHandleClick(onClick); + + return ( +
+ {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */} +
+ + +
+
+ ); +}; diff --git a/app/javascript/flavours/glitch/components/column_back_button_slim.jsx b/app/javascript/flavours/glitch/components/column_back_button_slim.jsx deleted file mode 100644 index 7cd20c2237..0000000000 --- a/app/javascript/flavours/glitch/components/column_back_button_slim.jsx +++ /dev/null @@ -1,42 +0,0 @@ -import { PureComponent } from 'react'; - -import { FormattedMessage } from 'react-intl'; - -import { withRouter } from 'react-router-dom'; - -import { ReactComponent as ArrowBackIcon } from '@material-symbols/svg-600/outlined/arrow_back.svg'; - -import { Icon } from 'flavours/glitch/components/icon'; -import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router'; - -class ColumnBackButtonSlim extends PureComponent { - - static propTypes = { - ...WithRouterPropTypes, - }; - - handleClick = () => { - const { location, history } = this.props; - - // Check if there is a previous page in the app to go back to per https://stackoverflow.com/a/70532858/9703201 - // When upgrading to V6, check `location.key !== 'default'` instead per https://github.com/remix-run/history/blob/main/docs/api-reference.md#location - if (location.key) { - history.goBack(); - } else { - history.push('/'); - } - }; - - render () { - return ( -
-
- - -
-
- ); - } -} - -export default withRouter(ColumnBackButtonSlim); diff --git a/app/javascript/flavours/glitch/components/column_header.jsx b/app/javascript/flavours/glitch/components/column_header.jsx index d44f284f9b..e372ad183a 100644 --- a/app/javascript/flavours/glitch/components/column_header.jsx +++ b/app/javascript/flavours/glitch/components/column_header.jsx @@ -1,6 +1,5 @@ import PropTypes from 'prop-types'; import { PureComponent } from 'react'; -import { createPortal } from 'react-dom'; import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; @@ -15,6 +14,7 @@ import { ReactComponent as CloseIcon } from '@material-symbols/svg-600/outlined/ import { ReactComponent as TuneIcon } from '@material-symbols/svg-600/outlined/tune.svg'; import { Icon } from 'flavours/glitch/components/icon'; +import { ButtonInTabsBar } from 'flavours/glitch/features/ui/util/columns_context'; import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router'; const messages = defineMessages({ @@ -203,22 +203,12 @@ class ColumnHeader extends PureComponent { ); - if (multiColumn || placeholder) { + if (placeholder) { return component; } else { - // The portal container and the component may be rendered to the DOM in - // the same React render pass, so the container might not be available at - // the time `render()` is called. - const container = document.getElementById('tabs-bar__portal'); - if (container === null) { - // The container wasn't available, force a re-render so that the - // component can eventually be inserted in the container and not scroll - // with the rest of the area. - this.forceUpdate(); - return component; - } else { - return createPortal(component, container); - } + return ( + {component} + ); } } diff --git a/app/javascript/flavours/glitch/components/router.tsx b/app/javascript/flavours/glitch/components/router.tsx index 96cc049b1b..22f22c65cc 100644 --- a/app/javascript/flavours/glitch/components/router.tsx +++ b/app/javascript/flavours/glitch/components/router.tsx @@ -1,7 +1,7 @@ import type { PropsWithChildren } from 'react'; import React from 'react'; -import { Router as OriginalRouter } from 'react-router'; +import { Router as OriginalRouter, useHistory } from 'react-router'; import type { LocationDescriptor, @@ -16,18 +16,23 @@ interface MastodonLocationState { fromMastodon?: boolean; mastodonModalKey?: string; } -type HistoryPath = Path | LocationDescriptor; -const browserHistory = createBrowserHistory< - MastodonLocationState | undefined ->(); +type LocationState = MastodonLocationState | null | undefined; + +type HistoryPath = Path | LocationDescriptor; + +const browserHistory = createBrowserHistory(); const originalPush = browserHistory.push.bind(browserHistory); const originalReplace = browserHistory.replace.bind(browserHistory); +export function useAppHistory() { + return useHistory(); +} + function normalizePath( path: HistoryPath, - state?: MastodonLocationState, -): LocationDescriptorObject { + state?: LocationState, +): LocationDescriptorObject { const location = typeof path === 'string' ? { pathname: path } : { ...path }; if (location.state === undefined && state !== undefined) { diff --git a/app/javascript/flavours/glitch/features/blocks/index.jsx b/app/javascript/flavours/glitch/features/blocks/index.jsx index 210260c811..21b7a263f1 100644 --- a/app/javascript/flavours/glitch/features/blocks/index.jsx +++ b/app/javascript/flavours/glitch/features/blocks/index.jsx @@ -10,7 +10,7 @@ import { ReactComponent as BlockIcon } from '@material-symbols/svg-600/outlined/ import { debounce } from 'lodash'; import { fetchBlocks, expandBlocks } from '../../actions/blocks'; -import ColumnBackButtonSlim from '../../components/column_back_button_slim'; +import { ColumnBackButtonSlim } from '../../components/column_back_button'; import { LoadingIndicator } from '../../components/loading_indicator'; import ScrollableList from '../../components/scrollable_list'; import AccountContainer from '../../containers/account_container'; diff --git a/app/javascript/flavours/glitch/features/domain_blocks/index.jsx b/app/javascript/flavours/glitch/features/domain_blocks/index.jsx index 5ac1d2a71e..958083d588 100644 --- a/app/javascript/flavours/glitch/features/domain_blocks/index.jsx +++ b/app/javascript/flavours/glitch/features/domain_blocks/index.jsx @@ -12,7 +12,7 @@ import { ReactComponent as BlockIcon } from '@material-symbols/svg-600/outlined/ import { debounce } from 'lodash'; import { fetchDomainBlocks, expandDomainBlocks } from '../../actions/domain_blocks'; -import ColumnBackButtonSlim from '../../components/column_back_button_slim'; +import { ColumnBackButtonSlim } from '../../components/column_back_button'; import { LoadingIndicator } from '../../components/loading_indicator'; import ScrollableList from '../../components/scrollable_list'; import DomainContainer from '../../containers/domain_container'; diff --git a/app/javascript/flavours/glitch/features/follow_requests/index.jsx b/app/javascript/flavours/glitch/features/follow_requests/index.jsx index 8e17607fd9..7d8785e052 100644 --- a/app/javascript/flavours/glitch/features/follow_requests/index.jsx +++ b/app/javascript/flavours/glitch/features/follow_requests/index.jsx @@ -12,7 +12,7 @@ import { ReactComponent as PersonAddIcon } from '@material-symbols/svg-600/outli import { debounce } from 'lodash'; import { fetchFollowRequests, expandFollowRequests } from '../../actions/accounts'; -import ColumnBackButtonSlim from '../../components/column_back_button_slim'; +import { ColumnBackButtonSlim } from '../../components/column_back_button'; import ScrollableList from '../../components/scrollable_list'; import { me } from '../../initial_state'; import Column from '../ui/components/column'; diff --git a/app/javascript/flavours/glitch/features/getting_started_misc/index.jsx b/app/javascript/flavours/glitch/features/getting_started_misc/index.jsx index 9c14f94b0e..81b1a3732a 100644 --- a/app/javascript/flavours/glitch/features/getting_started_misc/index.jsx +++ b/app/javascript/flavours/glitch/features/getting_started_misc/index.jsx @@ -13,7 +13,7 @@ import { ReactComponent as StarIcon } from '@material-symbols/svg-600/outlined/s import { ReactComponent as VolumeOffIcon } from '@material-symbols/svg-600/outlined/volume_off.svg'; import { openModal } from 'flavours/glitch/actions/modal'; -import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim'; +import { ColumnBackButtonSlim } from 'flavours/glitch/components/column_back_button'; import Column from 'flavours/glitch/features/ui/components/column'; import ColumnLink from 'flavours/glitch/features/ui/components/column_link'; import ColumnSubheading from 'flavours/glitch/features/ui/components/column_subheading'; diff --git a/app/javascript/flavours/glitch/features/lists/index.jsx b/app/javascript/flavours/glitch/features/lists/index.jsx index b47a8c07ce..415d4c181a 100644 --- a/app/javascript/flavours/glitch/features/lists/index.jsx +++ b/app/javascript/flavours/glitch/features/lists/index.jsx @@ -12,7 +12,7 @@ import { connect } from 'react-redux'; import { ReactComponent as ListAltIcon } from '@material-symbols/svg-600/outlined/list_alt.svg'; import { fetchLists } from 'flavours/glitch/actions/lists'; -import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim'; +import { ColumnBackButtonSlim } from 'flavours/glitch/components/column_back_button'; import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator'; import ScrollableList from 'flavours/glitch/components/scrollable_list'; import Column from 'flavours/glitch/features/ui/components/column'; diff --git a/app/javascript/flavours/glitch/features/mutes/index.jsx b/app/javascript/flavours/glitch/features/mutes/index.jsx index da69bc547d..afecd72d60 100644 --- a/app/javascript/flavours/glitch/features/mutes/index.jsx +++ b/app/javascript/flavours/glitch/features/mutes/index.jsx @@ -12,7 +12,7 @@ import { ReactComponent as VolumeOffIcon } from '@material-symbols/svg-600/outli import { debounce } from 'lodash'; import { fetchMutes, expandMutes } from '../../actions/mutes'; -import ColumnBackButtonSlim from '../../components/column_back_button_slim'; +import { ColumnBackButtonSlim } from '../../components/column_back_button'; import { LoadingIndicator } from '../../components/loading_indicator'; import ScrollableList from '../../components/scrollable_list'; import AccountContainer from '../../containers/account_container'; diff --git a/app/javascript/flavours/glitch/features/onboarding/follows.jsx b/app/javascript/flavours/glitch/features/onboarding/follows.jsx index 76673cdb41..bc9dede93f 100644 --- a/app/javascript/flavours/glitch/features/onboarding/follows.jsx +++ b/app/javascript/flavours/glitch/features/onboarding/follows.jsx @@ -9,7 +9,7 @@ import { connect } from 'react-redux'; import { fetchSuggestions } from 'flavours/glitch/actions/suggestions'; import { markAsPartial } from 'flavours/glitch/actions/timelines'; import Column from 'flavours/glitch/components/column'; -import ColumnBackButton from 'flavours/glitch/components/column_back_button'; +import { ColumnBackButton } from 'flavours/glitch/components/column_back_button'; import { EmptyAccount } from 'flavours/glitch/components/empty_account'; import Account from 'flavours/glitch/containers/account_container'; @@ -25,7 +25,6 @@ class Follows extends PureComponent { dispatch: PropTypes.func.isRequired, suggestions: ImmutablePropTypes.list, isLoading: PropTypes.bool, - multiColumn: PropTypes.bool, }; componentDidMount () { @@ -39,7 +38,7 @@ class Follows extends PureComponent { } render () { - const { onBack, isLoading, suggestions, multiColumn } = this.props; + const { onBack, isLoading, suggestions } = this.props; let loadedContent; @@ -53,7 +52,7 @@ class Follows extends PureComponent { return ( - +
diff --git a/app/javascript/flavours/glitch/features/onboarding/index.jsx b/app/javascript/flavours/glitch/features/onboarding/index.jsx index 129dd0da3e..2729e760f2 100644 --- a/app/javascript/flavours/glitch/features/onboarding/index.jsx +++ b/app/javascript/flavours/glitch/features/onboarding/index.jsx @@ -48,7 +48,6 @@ class Onboarding extends ImmutablePureComponent { static propTypes = { dispatch: PropTypes.func.isRequired, account: ImmutablePropTypes.record, - multiColumn: PropTypes.bool, ...WithRouterPropTypes, }; @@ -101,14 +100,14 @@ class Onboarding extends ImmutablePureComponent { } render () { - const { account, multiColumn } = this.props; + const { account } = this.props; const { step, shareClicked } = this.state; switch(step) { case 'follows': - return ; + return ; case 'share': - return ; + return ; } return ( diff --git a/app/javascript/flavours/glitch/features/onboarding/share.jsx b/app/javascript/flavours/glitch/features/onboarding/share.jsx index 66c95b8c9b..7c35c9a492 100644 --- a/app/javascript/flavours/glitch/features/onboarding/share.jsx +++ b/app/javascript/flavours/glitch/features/onboarding/share.jsx @@ -14,7 +14,7 @@ import { ReactComponent as ContentCopyIcon } from '@material-symbols/svg-600/out import SwipeableViews from 'react-swipeable-views'; import Column from 'flavours/glitch/components/column'; -import ColumnBackButton from 'flavours/glitch/components/column_back_button'; +import { ColumnBackButton } from 'flavours/glitch/components/column_back_button'; import { Icon } from 'flavours/glitch/components/icon'; import { me, domain } from 'flavours/glitch/initial_state'; @@ -146,18 +146,17 @@ class Share extends PureComponent { static propTypes = { onBack: PropTypes.func, account: ImmutablePropTypes.record, - multiColumn: PropTypes.bool, intl: PropTypes.object, }; render () { - const { onBack, account, multiColumn, intl } = this.props; + const { onBack, account, intl } = this.props; const url = (new URL(`/@${account.get('username')}`, document.baseURI)).href; return ( - +
diff --git a/app/javascript/flavours/glitch/features/pinned_statuses/index.jsx b/app/javascript/flavours/glitch/features/pinned_statuses/index.jsx index fafcb78f1d..9b168c76ac 100644 --- a/app/javascript/flavours/glitch/features/pinned_statuses/index.jsx +++ b/app/javascript/flavours/glitch/features/pinned_statuses/index.jsx @@ -13,7 +13,7 @@ import { ReactComponent as PushPinIcon } from '@material-symbols/svg-600/outline import { getStatusList } from 'flavours/glitch/selectors'; import { fetchPinnedStatuses } from '../../actions/pin_statuses'; -import ColumnBackButtonSlim from '../../components/column_back_button_slim'; +import { ColumnBackButtonSlim } from '../../components/column_back_button'; import StatusList from '../../components/status_list'; import Column from '../ui/components/column'; diff --git a/app/javascript/flavours/glitch/features/ui/components/columns_area.jsx b/app/javascript/flavours/glitch/features/ui/components/columns_area.jsx index a37e3cadce..f3e1bfe492 100644 --- a/app/javascript/flavours/glitch/features/ui/components/columns_area.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/columns_area.jsx @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import { Children, cloneElement } from 'react'; +import { Children, cloneElement, useCallback } from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; @@ -21,6 +21,7 @@ import { ListTimeline, Directory, } from '../util/async-components'; +import { useColumnsContext } from '../util/columns_context'; import BundleColumnError from './bundle_column_error'; import { ColumnLoading } from './column_loading'; @@ -43,6 +44,17 @@ const componentMap = { 'DIRECTORY': Directory, }; +const TabsBarPortal = () => { + const {setTabsBarElement} = useColumnsContext(); + + const setRef = useCallback((element) => { + if(element) + setTabsBarElement(element); + }, [setTabsBarElement]); + + return
; +}; + export default class ColumnsArea extends ImmutablePureComponent { static propTypes = { columns: ImmutablePropTypes.list.isRequired, @@ -146,7 +158,7 @@ export default class ColumnsArea extends ImmutablePureComponent {
-
+
{children}
diff --git a/app/javascript/flavours/glitch/features/ui/index.jsx b/app/javascript/flavours/glitch/features/ui/index.jsx index fa943f579f..dc3a7ce37b 100644 --- a/app/javascript/flavours/glitch/features/ui/index.jsx +++ b/app/javascript/flavours/glitch/features/ui/index.jsx @@ -67,8 +67,8 @@ import { About, PrivacyPolicy, } from './util/async-components'; +import { ColumnsContextProvider } from './util/columns_context'; import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers'; - // Dummy import, to make sure that ends up in the application bundle. // Without this it ends up in ~8 very commonly used bundles. import '../../components/status'; @@ -188,69 +188,71 @@ class SwitchingColumnsArea extends PureComponent { } return ( - - - {redirect} + + + + {redirect} - {singleColumn ? : null} - {singleColumn && pathName.startsWith('/deck/') ? : null} - {/* Redirect old bookmarks (without /deck) with home-like routes to the advanced interface */} - {!singleColumn && pathName === '/getting-started' ? : null} - {!singleColumn && pathName === '/home' ? : null} + {singleColumn ? : null} + {singleColumn && pathName.startsWith('/deck/') ? : null} + {/* Redirect old bookmarks (without /deck) with home-like routes to the advanced interface */} + {!singleColumn && pathName === '/getting-started' ? : null} + {!singleColumn && pathName === '/home' ? : null} - - - - + + + + - - - - - - - - - - - + + + + + + + + + + + - - + + - - - - + + + + - - - - - - - - - + + + + + + + + + - {/* Legacy routes, cannot be easily factored with other routes because they share a param name */} - - - - - + {/* Legacy routes, cannot be easily factored with other routes because they share a param name */} + + + + + - - - - - - - + + + + + + + - - - + + + + ); } diff --git a/app/javascript/flavours/glitch/features/ui/util/columns_context.tsx b/app/javascript/flavours/glitch/features/ui/util/columns_context.tsx new file mode 100644 index 0000000000..e02918deb0 --- /dev/null +++ b/app/javascript/flavours/glitch/features/ui/util/columns_context.tsx @@ -0,0 +1,51 @@ +import type { ReactElement } from 'react'; +import { createContext, useContext, useMemo, useState } from 'react'; +import { createPortal } from 'react-dom'; + +export const ColumnsContext = createContext<{ + tabsBarElement: HTMLElement | null; + setTabsBarElement: (element: HTMLElement) => void; + multiColumn: boolean; +}>({ + tabsBarElement: null, + multiColumn: false, + setTabsBarElement: () => undefined, // no-op +}); + +export function useColumnsContext() { + return useContext(ColumnsContext); +} + +export const ButtonInTabsBar: React.FC<{ + children: ReactElement | string | undefined; +}> = ({ children }) => { + const { multiColumn, tabsBarElement } = useColumnsContext(); + + if (multiColumn) { + return children; + } else if (!tabsBarElement) { + return children; + } else { + return createPortal(children, tabsBarElement); + } +}; + +type ContextValue = React.ContextType; + +export const ColumnsContextProvider: React.FC< + React.PropsWithChildren<{ multiColumn: boolean }> +> = ({ multiColumn, children }) => { + const [tabsBarElement, setTabsBarElement] = + useState(null); + + const contextValue = useMemo( + () => ({ multiColumn, tabsBarElement, setTabsBarElement }), + [multiColumn, tabsBarElement], + ); + + return ( + + {children} + + ); +};