Use a context to propagate column-related Props, and remove `forceUpdate` usage (#27548)
parent
3ca974e101
commit
537442853f
|
@ -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 'mastodon/components/icon';
|
|
||||||
import { WithRouterPropTypes } from 'mastodon/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);
|
|
|
@ -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 'mastodon/components/icon';
|
||||||
|
import { ButtonInTabsBar } from 'mastodon/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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,20 +0,0 @@
|
||||||
import { FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
import { ReactComponent as ArrowBackIcon } from '@material-symbols/svg-600/outlined/arrow_back.svg';
|
|
||||||
|
|
||||||
import { Icon } from 'mastodon/components/icon';
|
|
||||||
|
|
||||||
import { ColumnBackButton } from './column_back_button';
|
|
||||||
|
|
||||||
export default class ColumnBackButtonSlim extends ColumnBackButton {
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
|
import { ButtonInTabsBar } from 'mastodon/features/ui/util/columns_context';
|
||||||
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
|
import { WithRouterPropTypes } from 'mastodon/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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { lookupAccount, fetchAccount } from 'mastodon/actions/accounts';
|
import { lookupAccount, fetchAccount } from 'mastodon/actions/accounts';
|
||||||
import { openModal } from 'mastodon/actions/modal';
|
import { openModal } from 'mastodon/actions/modal';
|
||||||
import ColumnBackButton from 'mastodon/components/column_back_button';
|
import { ColumnBackButton } from 'mastodon/components/column_back_button';
|
||||||
import { LoadMore } from 'mastodon/components/load_more';
|
import { LoadMore } from 'mastodon/components/load_more';
|
||||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||||
import ScrollContainer from 'mastodon/containers/scroll_container';
|
import ScrollContainer from 'mastodon/containers/scroll_container';
|
||||||
|
@ -203,7 +203,7 @@ class AccountGallery extends ImmutablePureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column>
|
<Column>
|
||||||
<ColumnBackButton multiColumn={multiColumn} />
|
<ColumnBackButton />
|
||||||
|
|
||||||
<ScrollContainer scrollKey='account_gallery'>
|
<ScrollContainer scrollKey='account_gallery'>
|
||||||
<div className='scrollable scrollable--flex' onScroll={this.handleScroll}>
|
<div className='scrollable scrollable--flex' onScroll={this.handleScroll}>
|
||||||
|
|
|
@ -16,7 +16,7 @@ import { getAccountHidden } from 'mastodon/selectors';
|
||||||
import { lookupAccount, fetchAccount } from '../../actions/accounts';
|
import { lookupAccount, fetchAccount } from '../../actions/accounts';
|
||||||
import { fetchFeaturedTags } from '../../actions/featured_tags';
|
import { fetchFeaturedTags } from '../../actions/featured_tags';
|
||||||
import { expandAccountFeaturedTimeline, expandAccountTimeline, connectTimeline, disconnectTimeline } from '../../actions/timelines';
|
import { expandAccountFeaturedTimeline, expandAccountTimeline, connectTimeline, disconnectTimeline } from '../../actions/timelines';
|
||||||
import ColumnBackButton from '../../components/column_back_button';
|
import { ColumnBackButton } from '../../components/column_back_button';
|
||||||
import { LoadingIndicator } from '../../components/loading_indicator';
|
import { LoadingIndicator } from '../../components/loading_indicator';
|
||||||
import StatusList from '../../components/status_list';
|
import StatusList from '../../components/status_list';
|
||||||
import Column from '../ui/components/column';
|
import Column from '../ui/components/column';
|
||||||
|
@ -184,7 +184,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column>
|
<Column>
|
||||||
<ColumnBackButton multiColumn={multiColumn} />
|
<ColumnBackButton />
|
||||||
|
|
||||||
<StatusList
|
<StatusList
|
||||||
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs={forceEmptyState} tagged={this.props.params.tagged} />}
|
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs={forceEmptyState} tagged={this.props.params.tagged} />}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -19,7 +19,7 @@ import {
|
||||||
fetchFollowers,
|
fetchFollowers,
|
||||||
expandFollowers,
|
expandFollowers,
|
||||||
} from '../../actions/accounts';
|
} from '../../actions/accounts';
|
||||||
import ColumnBackButton from '../../components/column_back_button';
|
import { ColumnBackButton } 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';
|
||||||
|
@ -147,7 +147,7 @@ class Followers extends ImmutablePureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column>
|
<Column>
|
||||||
<ColumnBackButton multiColumn={multiColumn} />
|
<ColumnBackButton />
|
||||||
|
|
||||||
<ScrollableList
|
<ScrollableList
|
||||||
scrollKey='followers'
|
scrollKey='followers'
|
||||||
|
|
|
@ -19,7 +19,7 @@ import {
|
||||||
fetchFollowing,
|
fetchFollowing,
|
||||||
expandFollowing,
|
expandFollowing,
|
||||||
} from '../../actions/accounts';
|
} from '../../actions/accounts';
|
||||||
import ColumnBackButton from '../../components/column_back_button';
|
import { ColumnBackButton } 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';
|
||||||
|
@ -147,7 +147,7 @@ class Following extends ImmutablePureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column>
|
<Column>
|
||||||
<ColumnBackButton multiColumn={multiColumn} />
|
<ColumnBackButton />
|
||||||
|
|
||||||
<ScrollableList
|
<ScrollableList
|
||||||
scrollKey='following'
|
scrollKey='following'
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { connect } from 'react-redux';
|
||||||
import { fetchSuggestions } from 'mastodon/actions/suggestions';
|
import { fetchSuggestions } from 'mastodon/actions/suggestions';
|
||||||
import { markAsPartial } from 'mastodon/actions/timelines';
|
import { markAsPartial } from 'mastodon/actions/timelines';
|
||||||
import Column from 'mastodon/components/column';
|
import Column from 'mastodon/components/column';
|
||||||
import ColumnBackButton from 'mastodon/components/column_back_button';
|
import { ColumnBackButton } from 'mastodon/components/column_back_button';
|
||||||
import { EmptyAccount } from 'mastodon/components/empty_account';
|
import { EmptyAccount } from 'mastodon/components/empty_account';
|
||||||
import Account from 'mastodon/containers/account_container';
|
import Account from 'mastodon/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'>
|
||||||
|
|
|
@ -47,7 +47,6 @@ class Onboarding extends ImmutablePureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
account: ImmutablePropTypes.map,
|
account: ImmutablePropTypes.map,
|
||||||
multiColumn: PropTypes.bool,
|
|
||||||
...WithRouterPropTypes,
|
...WithRouterPropTypes,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -100,14 +99,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 (
|
||||||
|
|
|
@ -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 'mastodon/components/column';
|
import Column from 'mastodon/components/column';
|
||||||
import ColumnBackButton from 'mastodon/components/column_back_button';
|
import { ColumnBackButton } from 'mastodon/components/column_back_button';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
import { me, domain } from 'mastodon/initial_state';
|
import { me, domain } from 'mastodon/initial_state';
|
||||||
|
|
||||||
|
@ -146,18 +146,17 @@ class Share extends PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onBack: PropTypes.func,
|
onBack: PropTypes.func,
|
||||||
account: ImmutablePropTypes.map,
|
account: ImmutablePropTypes.map,
|
||||||
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'>
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { ReactComponent as PushPinIcon } from '@material-symbols/svg-600/outline
|
||||||
import { getStatusList } from 'mastodon/selectors';
|
import { getStatusList } from 'mastodon/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';
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -64,8 +64,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';
|
||||||
|
@ -179,68 +179,70 @@ class SwitchingColumnsArea extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ColumnsAreaContainer ref={this.setRef} singleColumn={singleColumn}>
|
<ColumnsContextProvider multiColumn={!singleColumn}>
|
||||||
<WrappedSwitch>
|
<ColumnsAreaContainer ref={this.setRef} singleColumn={singleColumn}>
|
||||||
{redirect}
|
<WrappedSwitch>
|
||||||
|
{redirect}
|
||||||
|
|
||||||
{singleColumn ? <Redirect from='/deck' to='/home' exact /> : null}
|
{singleColumn ? <Redirect from='/deck' to='/home' exact /> : null}
|
||||||
{singleColumn && pathName.startsWith('/deck/') ? <Redirect from={pathName} to={pathName.slice(5)} /> : null}
|
{singleColumn && pathName.startsWith('/deck/') ? <Redirect from={pathName} to={pathName.slice(5)} /> : null}
|
||||||
{/* Redirect old bookmarks (without /deck) with home-like routes to the advanced interface */}
|
{/* Redirect old bookmarks (without /deck) with home-like routes to the advanced interface */}
|
||||||
{!singleColumn && pathName === '/getting-started' ? <Redirect from='/getting-started' to='/deck/getting-started' exact /> : null}
|
{!singleColumn && pathName === '/getting-started' ? <Redirect from='/getting-started' to='/deck/getting-started' exact /> : null}
|
||||||
{!singleColumn && pathName === '/home' ? <Redirect from='/home' to='/deck/getting-started' exact /> : null}
|
{!singleColumn && pathName === '/home' ? <Redirect from='/home' to='/deck/getting-started' exact /> : null}
|
||||||
|
|
||||||
<WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
|
<WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
|
||||||
<WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
|
<WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
|
||||||
<WrappedRoute path='/about' component={About} content={children} />
|
<WrappedRoute path='/about' component={About} content={children} />
|
||||||
<WrappedRoute path='/privacy-policy' component={PrivacyPolicy} content={children} />
|
<WrappedRoute path='/privacy-policy' component={PrivacyPolicy} content={children} />
|
||||||
|
|
||||||
<WrappedRoute path={['/home', '/timelines/home']} component={HomeTimeline} content={children} />
|
<WrappedRoute path={['/home', '/timelines/home']} component={HomeTimeline} content={children} />
|
||||||
<Redirect from='/timelines/public' to='/public' exact />
|
<Redirect from='/timelines/public' to='/public' exact />
|
||||||
<Redirect from='/timelines/public/local' to='/public/local' exact />
|
<Redirect from='/timelines/public/local' to='/public/local' exact />
|
||||||
<WrappedRoute path='/public' exact component={Firehose} componentParams={{ feedType: 'public' }} content={children} />
|
<WrappedRoute path='/public' exact component={Firehose} componentParams={{ feedType: 'public' }} content={children} />
|
||||||
<WrappedRoute path='/public/local' exact component={Firehose} componentParams={{ feedType: 'community' }} content={children} />
|
<WrappedRoute path='/public/local' exact component={Firehose} componentParams={{ feedType: 'community' }} content={children} />
|
||||||
<WrappedRoute path='/public/remote' exact component={Firehose} componentParams={{ feedType: 'public:remote' }} content={children} />
|
<WrappedRoute path='/public/remote' exact component={Firehose} componentParams={{ feedType: 'public:remote' }} content={children} />
|
||||||
<WrappedRoute path={['/conversations', '/timelines/direct']} component={DirectTimeline} content={children} />
|
<WrappedRoute path={['/conversations', '/timelines/direct']} component={DirectTimeline} content={children} />
|
||||||
<WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} />
|
<WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} />
|
||||||
<WrappedRoute path='/lists/:id' component={ListTimeline} content={children} />
|
<WrappedRoute path='/lists/:id' component={ListTimeline} content={children} />
|
||||||
<WrappedRoute path='/notifications' component={Notifications} content={children} />
|
<WrappedRoute path='/notifications' component={Notifications} content={children} />
|
||||||
<WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} />
|
<WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} />
|
||||||
|
|
||||||
<WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} />
|
<WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} />
|
||||||
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
|
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
|
||||||
|
|
||||||
<WrappedRoute path='/start' exact component={Onboarding} content={children} />
|
<WrappedRoute path='/start' exact component={Onboarding} content={children} />
|
||||||
<WrappedRoute path='/directory' component={Directory} content={children} />
|
<WrappedRoute path='/directory' component={Directory} content={children} />
|
||||||
<WrappedRoute path={['/explore', '/search']} component={Explore} content={children} />
|
<WrappedRoute path={['/explore', '/search']} component={Explore} content={children} />
|
||||||
<WrappedRoute path={['/publish', '/statuses/new']} component={Compose} content={children} />
|
<WrappedRoute path={['/publish', '/statuses/new']} component={Compose} content={children} />
|
||||||
|
|
||||||
<WrappedRoute path={['/@:acct', '/accounts/:id']} exact component={AccountTimeline} content={children} />
|
<WrappedRoute path={['/@:acct', '/accounts/:id']} exact component={AccountTimeline} content={children} />
|
||||||
<WrappedRoute path='/@:acct/tagged/:tagged?' exact component={AccountTimeline} content={children} />
|
<WrappedRoute path='/@:acct/tagged/:tagged?' exact component={AccountTimeline} content={children} />
|
||||||
<WrappedRoute path={['/@:acct/with_replies', '/accounts/:id/with_replies']} component={AccountTimeline} content={children} componentParams={{ withReplies: true }} />
|
<WrappedRoute path={['/@:acct/with_replies', '/accounts/:id/with_replies']} component={AccountTimeline} content={children} componentParams={{ withReplies: true }} />
|
||||||
<WrappedRoute path={['/accounts/:id/followers', '/users/:acct/followers', '/@:acct/followers']} component={Followers} content={children} />
|
<WrappedRoute path={['/accounts/:id/followers', '/users/:acct/followers', '/@:acct/followers']} component={Followers} content={children} />
|
||||||
<WrappedRoute path={['/accounts/:id/following', '/users/:acct/following', '/@:acct/following']} component={Following} content={children} />
|
<WrappedRoute path={['/accounts/:id/following', '/users/:acct/following', '/@:acct/following']} component={Following} content={children} />
|
||||||
<WrappedRoute path={['/@:acct/media', '/accounts/:id/media']} component={AccountGallery} content={children} />
|
<WrappedRoute path={['/@:acct/media', '/accounts/:id/media']} component={AccountGallery} content={children} />
|
||||||
<WrappedRoute path='/@:acct/:statusId' exact component={Status} content={children} />
|
<WrappedRoute path='/@:acct/:statusId' exact component={Status} content={children} />
|
||||||
<WrappedRoute path='/@:acct/:statusId/reblogs' component={Reblogs} content={children} />
|
<WrappedRoute path='/@:acct/:statusId/reblogs' component={Reblogs} content={children} />
|
||||||
<WrappedRoute path='/@:acct/:statusId/favourites' component={Favourites} content={children} />
|
<WrappedRoute path='/@:acct/:statusId/favourites' component={Favourites} content={children} />
|
||||||
|
|
||||||
{/* 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 */}
|
||||||
<WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} />
|
<WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} />
|
||||||
<WrappedRoute path='/timelines/list/:id' component={ListTimeline} content={children} />
|
<WrappedRoute path='/timelines/list/:id' component={ListTimeline} content={children} />
|
||||||
<WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} />
|
<WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} />
|
||||||
<WrappedRoute path='/statuses/:statusId/reblogs' component={Reblogs} content={children} />
|
<WrappedRoute path='/statuses/:statusId/reblogs' component={Reblogs} content={children} />
|
||||||
<WrappedRoute path='/statuses/:statusId/favourites' component={Favourites} content={children} />
|
<WrappedRoute path='/statuses/:statusId/favourites' component={Favourites} content={children} />
|
||||||
|
|
||||||
<WrappedRoute path='/follow_requests' component={FollowRequests} content={children} />
|
<WrappedRoute path='/follow_requests' component={FollowRequests} content={children} />
|
||||||
<WrappedRoute path='/blocks' component={Blocks} content={children} />
|
<WrappedRoute path='/blocks' component={Blocks} content={children} />
|
||||||
<WrappedRoute path='/domain_blocks' component={DomainBlocks} content={children} />
|
<WrappedRoute path='/domain_blocks' component={DomainBlocks} content={children} />
|
||||||
<WrappedRoute path='/followed_tags' component={FollowedTags} content={children} />
|
<WrappedRoute path='/followed_tags' component={FollowedTags} content={children} />
|
||||||
<WrappedRoute path='/mutes' component={Mutes} content={children} />
|
<WrappedRoute path='/mutes' component={Mutes} content={children} />
|
||||||
<WrappedRoute path='/lists' component={Lists} content={children} />
|
<WrappedRoute path='/lists' component={Lists} content={children} />
|
||||||
|
|
||||||
<Route component={BundleColumnError} />
|
<Route component={BundleColumnError} />
|
||||||
</WrappedSwitch>
|
</WrappedSwitch>
|
||||||
</ColumnsAreaContainer>
|
</ColumnsAreaContainer>
|
||||||
|
</ColumnsContextProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
Loading…
Reference in New Issue