Change links in multi-column mode so tabs are open in single-column mode (#25893)
This commit is contained in:
		
							parent
							
								
									618eb10d18
								
							
						
					
					
						commit
						663e9484e2
					
				
					 12 changed files with 77 additions and 19 deletions
				
			
		
							
								
								
									
										23
									
								
								app/javascript/mastodon/components/router.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								app/javascript/mastodon/components/router.tsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,23 @@
 | 
			
		|||
import type { PropsWithChildren } from 'react';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
 | 
			
		||||
import type { History } from 'history';
 | 
			
		||||
import { createBrowserHistory } from 'history';
 | 
			
		||||
import { Router as OriginalRouter } from 'react-router';
 | 
			
		||||
 | 
			
		||||
import { layoutFromWindow } from 'mastodon/is_mobile';
 | 
			
		||||
 | 
			
		||||
const browserHistory = createBrowserHistory();
 | 
			
		||||
const originalPush = browserHistory.push.bind(browserHistory);
 | 
			
		||||
 | 
			
		||||
browserHistory.push = (path: string, state: History.LocationState) => {
 | 
			
		||||
  if (layoutFromWindow() === 'multi-column' && !path.startsWith('/deck')) {
 | 
			
		||||
    originalPush(`/deck${path}`, state);
 | 
			
		||||
  } else {
 | 
			
		||||
    originalPush(path, state);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const Router: React.FC<PropsWithChildren> = ({ children }) => {
 | 
			
		||||
  return <OriginalRouter history={browserHistory}>{children}</OriginalRouter>;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
 | 
			
		|||
import { PureComponent } from 'react';
 | 
			
		||||
 | 
			
		||||
import { Helmet } from 'react-helmet';
 | 
			
		||||
import { BrowserRouter, Route } from 'react-router-dom';
 | 
			
		||||
import { Route } from 'react-router-dom';
 | 
			
		||||
 | 
			
		||||
import { Provider as ReduxProvider } from 'react-redux';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -12,6 +12,7 @@ import { fetchCustomEmojis } from 'mastodon/actions/custom_emojis';
 | 
			
		|||
import { hydrateStore } from 'mastodon/actions/store';
 | 
			
		||||
import { connectUserStream } from 'mastodon/actions/streaming';
 | 
			
		||||
import ErrorBoundary from 'mastodon/components/error_boundary';
 | 
			
		||||
import { Router } from 'mastodon/components/router';
 | 
			
		||||
import UI from 'mastodon/features/ui';
 | 
			
		||||
import initialState, { title as siteTitle } from 'mastodon/initial_state';
 | 
			
		||||
import { IntlProvider } from 'mastodon/locales';
 | 
			
		||||
| 
						 | 
				
			
			@ -75,11 +76,11 @@ export default class Mastodon extends PureComponent {
 | 
			
		|||
      <IntlProvider>
 | 
			
		||||
        <ReduxProvider store={store}>
 | 
			
		||||
          <ErrorBoundary>
 | 
			
		||||
            <BrowserRouter>
 | 
			
		||||
            <Router>
 | 
			
		||||
              <ScrollContext shouldUpdateScroll={this.shouldUpdateScroll}>
 | 
			
		||||
                <Route path='/' component={UI} />
 | 
			
		||||
              </ScrollContext>
 | 
			
		||||
            </BrowserRouter>
 | 
			
		||||
            </Router>
 | 
			
		||||
 | 
			
		||||
            <Helmet defaultTitle={title} titleTemplate={`%s - ${title}`} />
 | 
			
		||||
          </ErrorBoundary>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,6 +8,7 @@ import { Link } from 'react-router-dom';
 | 
			
		|||
import { WordmarkLogo } from 'mastodon/components/logo';
 | 
			
		||||
import NavigationPortal from 'mastodon/components/navigation_portal';
 | 
			
		||||
import { timelinePreview, trendsEnabled } from 'mastodon/initial_state';
 | 
			
		||||
import { transientSingleColumn } from 'mastodon/is_mobile';
 | 
			
		||||
 | 
			
		||||
import ColumnLink from './column_link';
 | 
			
		||||
import DisabledAccountBanner from './disabled_account_banner';
 | 
			
		||||
| 
						 | 
				
			
			@ -29,6 +30,7 @@ const messages = defineMessages({
 | 
			
		|||
  followsAndFollowers: { id: 'navigation_bar.follows_and_followers', defaultMessage: 'Follows and followers' },
 | 
			
		||||
  about: { id: 'navigation_bar.about', defaultMessage: 'About' },
 | 
			
		||||
  search: { id: 'navigation_bar.search', defaultMessage: 'Search' },
 | 
			
		||||
  advancedInterface: { id: 'navigation_bar.advanced_interface', defaultMessage: 'Open in advanced web interface' },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
class NavigationPanel extends Component {
 | 
			
		||||
| 
						 | 
				
			
			@ -54,6 +56,12 @@ class NavigationPanel extends Component {
 | 
			
		|||
      <div className='navigation-panel'>
 | 
			
		||||
        <div className='navigation-panel__logo'>
 | 
			
		||||
          <Link to='/' className='column-link column-link--logo'><WordmarkLogo /></Link>
 | 
			
		||||
 | 
			
		||||
          {transientSingleColumn && (
 | 
			
		||||
            <a href={`/deck${location.pathname}`} className='button button--block'>
 | 
			
		||||
              {intl.formatMessage(messages.advancedInterface)}
 | 
			
		||||
            </a>
 | 
			
		||||
          )}
 | 
			
		||||
          <hr />
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -126,11 +126,11 @@ class SwitchingColumnsArea extends PureComponent {
 | 
			
		|||
  static propTypes = {
 | 
			
		||||
    children: PropTypes.node,
 | 
			
		||||
    location: PropTypes.object,
 | 
			
		||||
    mobile: PropTypes.bool,
 | 
			
		||||
    singleColumn: PropTypes.bool,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  UNSAFE_componentWillMount () {
 | 
			
		||||
    if (this.props.mobile) {
 | 
			
		||||
    if (this.props.singleColumn) {
 | 
			
		||||
      document.body.classList.toggle('layout-single-column', true);
 | 
			
		||||
      document.body.classList.toggle('layout-multiple-columns', false);
 | 
			
		||||
    } else {
 | 
			
		||||
| 
						 | 
				
			
			@ -144,9 +144,9 @@ class SwitchingColumnsArea extends PureComponent {
 | 
			
		|||
      this.node.handleChildrenContentChange();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (prevProps.mobile !== this.props.mobile) {
 | 
			
		||||
      document.body.classList.toggle('layout-single-column', this.props.mobile);
 | 
			
		||||
      document.body.classList.toggle('layout-multiple-columns', !this.props.mobile);
 | 
			
		||||
    if (prevProps.singleColumn !== this.props.singleColumn) {
 | 
			
		||||
      document.body.classList.toggle('layout-single-column', this.props.singleColumn);
 | 
			
		||||
      document.body.classList.toggle('layout-multiple-columns', !this.props.singleColumn);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -157,16 +157,17 @@ class SwitchingColumnsArea extends PureComponent {
 | 
			
		|||
  };
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { children, mobile } = this.props;
 | 
			
		||||
    const { children, singleColumn } = this.props;
 | 
			
		||||
    const { signedIn } = this.context.identity;
 | 
			
		||||
    const pathName = this.props.location.pathname;
 | 
			
		||||
 | 
			
		||||
    let redirect;
 | 
			
		||||
 | 
			
		||||
    if (signedIn) {
 | 
			
		||||
      if (mobile) {
 | 
			
		||||
      if (singleColumn) {
 | 
			
		||||
        redirect = <Redirect from='/' to='/home' exact />;
 | 
			
		||||
      } else {
 | 
			
		||||
        redirect = <Redirect from='/' to='/getting-started' exact />;
 | 
			
		||||
        redirect = <Redirect from='/' to='/deck/getting-started' exact />;
 | 
			
		||||
      }
 | 
			
		||||
    } else if (singleUserMode && owner && initialState?.accounts[owner]) {
 | 
			
		||||
      redirect = <Redirect from='/' to={`/@${initialState.accounts[owner].username}`} exact />;
 | 
			
		||||
| 
						 | 
				
			
			@ -177,10 +178,13 @@ class SwitchingColumnsArea extends PureComponent {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <ColumnsAreaContainer ref={this.setRef} singleColumn={mobile}>
 | 
			
		||||
      <ColumnsAreaContainer ref={this.setRef} singleColumn={singleColumn}>
 | 
			
		||||
        <WrappedSwitch>
 | 
			
		||||
          {redirect}
 | 
			
		||||
 | 
			
		||||
          {singleColumn ? <Redirect from='/deck' to='/home' exact /> : null}
 | 
			
		||||
          {singleColumn && pathName.startsWith('/deck/') ? <Redirect from={pathName} to={pathName.slice(5)} /> : null}
 | 
			
		||||
 | 
			
		||||
          <WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
 | 
			
		||||
          <WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
 | 
			
		||||
          <WrappedRoute path='/about' component={About} content={children} />
 | 
			
		||||
| 
						 | 
				
			
			@ -573,7 +577,7 @@ class UI extends PureComponent {
 | 
			
		|||
        <div className={classNames('ui', { 'is-composing': isComposing })} ref={this.setRef} style={{ pointerEvents: dropdownMenuIsOpen ? 'none' : null }}>
 | 
			
		||||
          <Header />
 | 
			
		||||
 | 
			
		||||
          <SwitchingColumnsArea location={location} mobile={layout === 'mobile' || layout === 'single-column'}>
 | 
			
		||||
          <SwitchingColumnsArea location={location} singleColumn={layout === 'mobile' || layout === 'single-column'}>
 | 
			
		||||
            {children}
 | 
			
		||||
          </SwitchingColumnsArea>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,13 +11,21 @@ import BundleContainer from '../containers/bundle_container';
 | 
			
		|||
 | 
			
		||||
// Small wrapper to pass multiColumn to the route components
 | 
			
		||||
export class WrappedSwitch extends PureComponent {
 | 
			
		||||
  static contextTypes = {
 | 
			
		||||
    router: PropTypes.object,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { multiColumn, children } = this.props;
 | 
			
		||||
    const { location } = this.context.router.route;
 | 
			
		||||
 | 
			
		||||
    const decklessLocation = multiColumn && location.pathname.startsWith('/deck')
 | 
			
		||||
      ? {...location, pathname: location.pathname.slice(5)}
 | 
			
		||||
      : location;
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <Switch>
 | 
			
		||||
        {Children.map(children, child => cloneElement(child, { multiColumn }))}
 | 
			
		||||
      <Switch location={decklessLocation}>
 | 
			
		||||
        {Children.map(children, child => child ? cloneElement(child, { multiColumn }) : null)}
 | 
			
		||||
      </Switch>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -94,6 +94,13 @@ const element = document.getElementById('initial-state');
 | 
			
		|||
/** @type {InitialState | undefined} */
 | 
			
		||||
const initialState = element?.textContent && JSON.parse(element.textContent);
 | 
			
		||||
 | 
			
		||||
/** @type {string} */
 | 
			
		||||
const initialPath = document.querySelector("head meta[name=initialPath]")?.getAttribute("content") ?? '';
 | 
			
		||||
/** @type {boolean} */
 | 
			
		||||
export const hasMultiColumnPath = initialPath === '/'
 | 
			
		||||
  || initialPath === '/getting-started'
 | 
			
		||||
  || initialPath.startsWith('/deck');
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @template {keyof InitialStateMeta} K
 | 
			
		||||
 * @param {K} prop
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,19 +1,21 @@
 | 
			
		|||
import { supportsPassiveEvents } from 'detect-passive-events';
 | 
			
		||||
 | 
			
		||||
import { forceSingleColumn } from './initial_state';
 | 
			
		||||
import { forceSingleColumn, hasMultiColumnPath } from './initial_state';
 | 
			
		||||
 | 
			
		||||
const LAYOUT_BREAKPOINT = 630;
 | 
			
		||||
 | 
			
		||||
export const isMobile = (width: number) => width <= LAYOUT_BREAKPOINT;
 | 
			
		||||
 | 
			
		||||
export const transientSingleColumn = !forceSingleColumn && !hasMultiColumnPath;
 | 
			
		||||
 | 
			
		||||
export type LayoutType = 'mobile' | 'single-column' | 'multi-column';
 | 
			
		||||
export const layoutFromWindow = (): LayoutType => {
 | 
			
		||||
  if (isMobile(window.innerWidth)) {
 | 
			
		||||
    return 'mobile';
 | 
			
		||||
  } else if (forceSingleColumn) {
 | 
			
		||||
    return 'single-column';
 | 
			
		||||
  } else {
 | 
			
		||||
  } else if (!forceSingleColumn && !transientSingleColumn) {
 | 
			
		||||
    return 'multi-column';
 | 
			
		||||
  } else {
 | 
			
		||||
    return 'single-column';
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -385,6 +385,7 @@
 | 
			
		|||
  "mute_modal.hide_notifications": "Hide notifications from this user?",
 | 
			
		||||
  "mute_modal.indefinite": "Indefinite",
 | 
			
		||||
  "navigation_bar.about": "About",
 | 
			
		||||
  "navigation_bar.advanced_interface": "Open in advanced web interface",
 | 
			
		||||
  "navigation_bar.blocks": "Blocked users",
 | 
			
		||||
  "navigation_bar.bookmarks": "Bookmarks",
 | 
			
		||||
  "navigation_bar.community_timeline": "Local timeline",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -368,6 +368,7 @@
 | 
			
		|||
  "mute_modal.hide_notifications": "Masquer les notifications de cette personne ?",
 | 
			
		||||
  "mute_modal.indefinite": "Indéfinie",
 | 
			
		||||
  "navigation_bar.about": "À propos",
 | 
			
		||||
  "navigation_bar.advanced_interface": "Ouvrir dans l’interface avancée",
 | 
			
		||||
  "navigation_bar.blocks": "Comptes bloqués",
 | 
			
		||||
  "navigation_bar.bookmarks": "Marque-pages",
 | 
			
		||||
  "navigation_bar.community_timeline": "Fil public local",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,6 +3,7 @@
 | 
			
		|||
    = preload_pack_asset 'features/compose.js', crossorigin: 'anonymous'
 | 
			
		||||
    = preload_pack_asset 'features/home_timeline.js', crossorigin: 'anonymous'
 | 
			
		||||
    = preload_pack_asset 'features/notifications.js', crossorigin: 'anonymous'
 | 
			
		||||
    %meta{ name: 'initialPath', content: request.path }
 | 
			
		||||
 | 
			
		||||
  %meta{ name: 'applicationServerKey', content: Rails.configuration.x.vapid_public_key }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -30,6 +30,7 @@ Rails.application.routes.draw do
 | 
			
		|||
    /mutes
 | 
			
		||||
    /followed_tags
 | 
			
		||||
    /statuses/(*any)
 | 
			
		||||
    /deck/(*any)
 | 
			
		||||
  ).freeze
 | 
			
		||||
 | 
			
		||||
  root 'home#index'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -106,6 +106,7 @@
 | 
			
		|||
    "react-overlays": "^5.2.1",
 | 
			
		||||
    "react-redux": "^8.0.4",
 | 
			
		||||
    "react-redux-loading-bar": "^5.0.4",
 | 
			
		||||
    "react-router": "^4.3.1",
 | 
			
		||||
    "react-router-dom": "^4.1.1",
 | 
			
		||||
    "react-router-scroll-4": "^1.0.0-beta.1",
 | 
			
		||||
    "react-select": "^5.7.3",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in a new issue