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