[Glitch] Use a context to propagate column-related Props, and remove forceUpdate usage

Port 537442853f to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
Renaud Chaput 2023-10-26 13:00:10 +02:00 committed by Claire
parent 04f786abd0
commit ca17c2d35a
18 changed files with 225 additions and 205 deletions

View file

@ -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 = (
<button onClick={this.handleClick} className='column-back-button'>
<Icon id='chevron-left' icon={ArrowBackIcon} className='column-back-button__icon' />
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
</button>
);
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);

View file

@ -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 = (
<button onClick={handleClick} className='column-back-button'>
<Icon
id='chevron-left'
icon={ArrowBackIcon}
className='column-back-button__icon'
/>
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
</button>
);
return <ButtonInTabsBar>{component}</ButtonInTabsBar>;
};
export const ColumnBackButtonSlim: React.FC<{ onClick: OnClickCallback }> = ({
onClick,
}) => {
const handleClick = useHandleClick(onClick);
return (
<div className='column-back-button--slim'>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
<div
role='button'
tabIndex={0}
onClick={handleClick}
className='column-back-button column-back-button--slim-button'
>
<Icon
id='chevron-left'
icon={ArrowBackIcon}
className='column-back-button__icon'
/>
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
</div>
</div>
);
};

View file

@ -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 (
<div className='column-back-button--slim'>
<div role='button' tabIndex={0} onClick={this.handleClick} className='column-back-button column-back-button--slim-button'>
<Icon id='chevron-left' icon={ArrowBackIcon} className='column-back-button__icon' />
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
</div>
</div>
);
}
}
export default withRouter(ColumnBackButtonSlim);

View file

@ -1,6 +1,5 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { PureComponent } from 'react'; import { PureComponent } from 'react';
import { createPortal } from 'react-dom';
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; 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 { ReactComponent as TuneIcon } from '@material-symbols/svg-600/outlined/tune.svg';
import { Icon } from 'flavours/glitch/components/icon'; 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'; import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
const messages = defineMessages({ const messages = defineMessages({
@ -203,22 +203,12 @@ class ColumnHeader extends PureComponent {
</div> </div>
); );
if (multiColumn || placeholder) { if (placeholder) {
return component; return component;
} else { } else {
// The portal container and the component may be rendered to the DOM in return (<ButtonInTabsBar>
// the same React render pass, so the container might not be available at {component}
// the time `render()` is called. </ButtonInTabsBar>);
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);
}
} }
} }

View file

@ -1,7 +1,7 @@
import type { PropsWithChildren } from 'react'; import type { PropsWithChildren } from 'react';
import React from 'react'; import React from 'react';
import { Router as OriginalRouter } from 'react-router'; import { Router as OriginalRouter, useHistory } from 'react-router';
import type { import type {
LocationDescriptor, LocationDescriptor,
@ -16,18 +16,23 @@ interface MastodonLocationState {
fromMastodon?: boolean; fromMastodon?: boolean;
mastodonModalKey?: string; mastodonModalKey?: string;
} }
type HistoryPath = Path | LocationDescriptor<MastodonLocationState>;
const browserHistory = createBrowserHistory< type LocationState = MastodonLocationState | null | undefined;
MastodonLocationState | undefined
>(); type HistoryPath = Path | LocationDescriptor<LocationState>;
const browserHistory = createBrowserHistory<LocationState>();
const originalPush = browserHistory.push.bind(browserHistory); const originalPush = browserHistory.push.bind(browserHistory);
const originalReplace = browserHistory.replace.bind(browserHistory); const originalReplace = browserHistory.replace.bind(browserHistory);
export function useAppHistory() {
return useHistory<LocationState>();
}
function normalizePath( function normalizePath(
path: HistoryPath, path: HistoryPath,
state?: MastodonLocationState, state?: LocationState,
): LocationDescriptorObject<MastodonLocationState> { ): LocationDescriptorObject<LocationState> {
const location = typeof path === 'string' ? { pathname: path } : { ...path }; const location = typeof path === 'string' ? { pathname: path } : { ...path };
if (location.state === undefined && state !== undefined) { if (location.state === undefined && state !== undefined) {

View file

@ -10,7 +10,7 @@ import { ReactComponent as BlockIcon } from '@material-symbols/svg-600/outlined/
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { fetchBlocks, expandBlocks } from '../../actions/blocks'; 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 { LoadingIndicator } from '../../components/loading_indicator';
import ScrollableList from '../../components/scrollable_list'; import ScrollableList from '../../components/scrollable_list';
import AccountContainer from '../../containers/account_container'; import AccountContainer from '../../containers/account_container';

View file

@ -12,7 +12,7 @@ import { ReactComponent as BlockIcon } from '@material-symbols/svg-600/outlined/
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { fetchDomainBlocks, expandDomainBlocks } from '../../actions/domain_blocks'; 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 { LoadingIndicator } from '../../components/loading_indicator';
import ScrollableList from '../../components/scrollable_list'; import ScrollableList from '../../components/scrollable_list';
import DomainContainer from '../../containers/domain_container'; import DomainContainer from '../../containers/domain_container';

View file

@ -12,7 +12,7 @@ import { ReactComponent as PersonAddIcon } from '@material-symbols/svg-600/outli
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { fetchFollowRequests, expandFollowRequests } from '../../actions/accounts'; 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 ScrollableList from '../../components/scrollable_list';
import { me } from '../../initial_state'; import { me } from '../../initial_state';
import Column from '../ui/components/column'; import Column from '../ui/components/column';

View file

@ -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 { ReactComponent as VolumeOffIcon } from '@material-symbols/svg-600/outlined/volume_off.svg';
import { openModal } from 'flavours/glitch/actions/modal'; 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 Column from 'flavours/glitch/features/ui/components/column';
import ColumnLink from 'flavours/glitch/features/ui/components/column_link'; import ColumnLink from 'flavours/glitch/features/ui/components/column_link';
import ColumnSubheading from 'flavours/glitch/features/ui/components/column_subheading'; import ColumnSubheading from 'flavours/glitch/features/ui/components/column_subheading';

View file

@ -12,7 +12,7 @@ import { connect } from 'react-redux';
import { ReactComponent as ListAltIcon } from '@material-symbols/svg-600/outlined/list_alt.svg'; import { ReactComponent as ListAltIcon } from '@material-symbols/svg-600/outlined/list_alt.svg';
import { fetchLists } from 'flavours/glitch/actions/lists'; 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 { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
import ScrollableList from 'flavours/glitch/components/scrollable_list'; import ScrollableList from 'flavours/glitch/components/scrollable_list';
import Column from 'flavours/glitch/features/ui/components/column'; import Column from 'flavours/glitch/features/ui/components/column';

View file

@ -12,7 +12,7 @@ import { ReactComponent as VolumeOffIcon } from '@material-symbols/svg-600/outli
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { fetchMutes, expandMutes } from '../../actions/mutes'; 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 { LoadingIndicator } from '../../components/loading_indicator';
import ScrollableList from '../../components/scrollable_list'; import ScrollableList from '../../components/scrollable_list';
import AccountContainer from '../../containers/account_container'; import AccountContainer from '../../containers/account_container';

View file

@ -9,7 +9,7 @@ import { connect } from 'react-redux';
import { fetchSuggestions } from 'flavours/glitch/actions/suggestions'; import { fetchSuggestions } from 'flavours/glitch/actions/suggestions';
import { markAsPartial } from 'flavours/glitch/actions/timelines'; import { markAsPartial } from 'flavours/glitch/actions/timelines';
import Column from 'flavours/glitch/components/column'; 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 { EmptyAccount } from 'flavours/glitch/components/empty_account';
import Account from 'flavours/glitch/containers/account_container'; import Account from 'flavours/glitch/containers/account_container';
@ -25,7 +25,6 @@ class Follows extends PureComponent {
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
suggestions: ImmutablePropTypes.list, suggestions: ImmutablePropTypes.list,
isLoading: PropTypes.bool, isLoading: PropTypes.bool,
multiColumn: PropTypes.bool,
}; };
componentDidMount () { componentDidMount () {
@ -39,7 +38,7 @@ class Follows extends PureComponent {
} }
render () { render () {
const { onBack, isLoading, suggestions, multiColumn } = this.props; const { onBack, isLoading, suggestions } = this.props;
let loadedContent; let loadedContent;
@ -53,7 +52,7 @@ class Follows extends PureComponent {
return ( return (
<Column> <Column>
<ColumnBackButton multiColumn={multiColumn} onClick={onBack} /> <ColumnBackButton onClick={onBack} />
<div className='scrollable privacy-policy'> <div className='scrollable privacy-policy'>
<div className='column-title'> <div className='column-title'>

View file

@ -48,7 +48,6 @@ class Onboarding extends ImmutablePureComponent {
static propTypes = { static propTypes = {
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
account: ImmutablePropTypes.record, account: ImmutablePropTypes.record,
multiColumn: PropTypes.bool,
...WithRouterPropTypes, ...WithRouterPropTypes,
}; };
@ -101,14 +100,14 @@ class Onboarding extends ImmutablePureComponent {
} }
render () { render () {
const { account, multiColumn } = this.props; const { account } = this.props;
const { step, shareClicked } = this.state; const { step, shareClicked } = this.state;
switch(step) { switch(step) {
case 'follows': case 'follows':
return <Follows onBack={this.handleBackClick} multiColumn={multiColumn} />; return <Follows onBack={this.handleBackClick} />;
case 'share': case 'share':
return <Share onBack={this.handleBackClick} multiColumn={multiColumn} />; return <Share onBack={this.handleBackClick} />;
} }
return ( return (

View file

@ -14,7 +14,7 @@ import { ReactComponent as ContentCopyIcon } from '@material-symbols/svg-600/out
import SwipeableViews from 'react-swipeable-views'; import SwipeableViews from 'react-swipeable-views';
import Column from 'flavours/glitch/components/column'; 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 { Icon } from 'flavours/glitch/components/icon';
import { me, domain } from 'flavours/glitch/initial_state'; import { me, domain } from 'flavours/glitch/initial_state';
@ -146,18 +146,17 @@ class Share extends PureComponent {
static propTypes = { static propTypes = {
onBack: PropTypes.func, onBack: PropTypes.func,
account: ImmutablePropTypes.record, account: ImmutablePropTypes.record,
multiColumn: PropTypes.bool,
intl: PropTypes.object, intl: PropTypes.object,
}; };
render () { render () {
const { onBack, account, multiColumn, intl } = this.props; const { onBack, account, intl } = this.props;
const url = (new URL(`/@${account.get('username')}`, document.baseURI)).href; const url = (new URL(`/@${account.get('username')}`, document.baseURI)).href;
return ( return (
<Column> <Column>
<ColumnBackButton multiColumn={multiColumn} onClick={onBack} /> <ColumnBackButton onClick={onBack} />
<div className='scrollable privacy-policy'> <div className='scrollable privacy-policy'>
<div className='column-title'> <div className='column-title'>

View file

@ -13,7 +13,7 @@ import { ReactComponent as PushPinIcon } from '@material-symbols/svg-600/outline
import { getStatusList } from 'flavours/glitch/selectors'; import { getStatusList } from 'flavours/glitch/selectors';
import { fetchPinnedStatuses } from '../../actions/pin_statuses'; 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 StatusList from '../../components/status_list';
import Column from '../ui/components/column'; import Column from '../ui/components/column';

View file

@ -1,5 +1,5 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Children, cloneElement } from 'react'; import { Children, cloneElement, useCallback } from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
@ -21,6 +21,7 @@ import {
ListTimeline, ListTimeline,
Directory, Directory,
} from '../util/async-components'; } from '../util/async-components';
import { useColumnsContext } from '../util/columns_context';
import BundleColumnError from './bundle_column_error'; import BundleColumnError from './bundle_column_error';
import { ColumnLoading } from './column_loading'; import { ColumnLoading } from './column_loading';
@ -43,6 +44,17 @@ const componentMap = {
'DIRECTORY': Directory, 'DIRECTORY': Directory,
}; };
const TabsBarPortal = () => {
const {setTabsBarElement} = useColumnsContext();
const setRef = useCallback((element) => {
if(element)
setTabsBarElement(element);
}, [setTabsBarElement]);
return <div id='tabs-bar__portal' ref={setRef} />;
};
export default class ColumnsArea extends ImmutablePureComponent { export default class ColumnsArea extends ImmutablePureComponent {
static propTypes = { static propTypes = {
columns: ImmutablePropTypes.list.isRequired, columns: ImmutablePropTypes.list.isRequired,
@ -146,7 +158,7 @@ export default class ColumnsArea extends ImmutablePureComponent {
</div> </div>
<div className='columns-area__panels__main'> <div className='columns-area__panels__main'>
<div className='tabs-bar__wrapper'><div id='tabs-bar__portal' /></div> <div className='tabs-bar__wrapper'><TabsBarPortal /></div>
<div className='columns-area columns-area--mobile'>{children}</div> <div className='columns-area columns-area--mobile'>{children}</div>
</div> </div>

View file

@ -67,8 +67,8 @@ import {
About, About,
PrivacyPolicy, PrivacyPolicy,
} from './util/async-components'; } from './util/async-components';
import { ColumnsContextProvider } from './util/columns_context';
import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers'; import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
// Dummy import, to make sure that <Status /> ends up in the application bundle. // Dummy import, to make sure that <Status /> ends up in the application bundle.
// Without this it ends up in ~8 very commonly used bundles. // Without this it ends up in ~8 very commonly used bundles.
import '../../components/status'; import '../../components/status';
@ -188,6 +188,7 @@ class SwitchingColumnsArea extends PureComponent {
} }
return ( return (
<ColumnsContextProvider multiColumn={!singleColumn}>
<ColumnsAreaContainer ref={this.setRef} singleColumn={singleColumn}> <ColumnsAreaContainer ref={this.setRef} singleColumn={singleColumn}>
<WrappedSwitch> <WrappedSwitch>
{redirect} {redirect}
@ -251,6 +252,7 @@ class SwitchingColumnsArea extends PureComponent {
<Route component={BundleColumnError} /> <Route component={BundleColumnError} />
</WrappedSwitch> </WrappedSwitch>
</ColumnsAreaContainer> </ColumnsAreaContainer>
</ColumnsContextProvider>
); );
} }

View file

@ -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<typeof ColumnsContext>;
export const ColumnsContextProvider: React.FC<
React.PropsWithChildren<{ multiColumn: boolean }>
> = ({ multiColumn, children }) => {
const [tabsBarElement, setTabsBarElement] =
useState<ContextValue['tabsBarElement']>(null);
const contextValue = useMemo<ContextValue>(
() => ({ multiColumn, tabsBarElement, setTabsBarElement }),
[multiColumn, tabsBarElement],
);
return (
<ColumnsContext.Provider value={contextValue}>
{children}
</ColumnsContext.Provider>
);
};