diff --git a/app/javascript/mastodon/components/router.tsx b/app/javascript/mastodon/components/router.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c82711790b0f50f395d609cb8238f77388623cc7 --- /dev/null +++ b/app/javascript/mastodon/components/router.tsx @@ -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>; +}; diff --git a/app/javascript/mastodon/containers/mastodon.jsx b/app/javascript/mastodon/containers/mastodon.jsx index 4538db050d1cbc64eb8052ac0e86bd2a3145c756..59efc80570dc6cf468e2326ef35bdc95654263cf 100644 --- a/app/javascript/mastodon/containers/mastodon.jsx +++ b/app/javascript/mastodon/containers/mastodon.jsx @@ -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> diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx index dc406fa55cce59358ee83ebfa24833e493581690..ab5c78246f9eaaacfca84df6143f8b23738db30f 100644 --- a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx +++ b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx @@ -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> diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index b38acfc14dd0fca5350523bbd153984811c0cbb5..ae81a354b2146d7147621b2aad6b7b2ee61fa973 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -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> diff --git a/app/javascript/mastodon/features/ui/util/react_router_helpers.jsx b/app/javascript/mastodon/features/ui/util/react_router_helpers.jsx index 66cfee970814b1f183e43e0115e02e7cf0535ba3..99277268573cc584d71ca75356847ed91ea59b77 100644 --- a/app/javascript/mastodon/features/ui/util/react_router_helpers.jsx +++ b/app/javascript/mastodon/features/ui/util/react_router_helpers.jsx @@ -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> ); } diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js index 5ad61e1f6b87cf576492006c61825ed6b3f59ae1..67fb068432c7ce11189dc9ebd1c08a9003b59cf0 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -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 diff --git a/app/javascript/mastodon/is_mobile.ts b/app/javascript/mastodon/is_mobile.ts index 36cde21332ea1faab6daa4e96b23cf761f721d28..7f339e287bfd6165b8f2eb03042b5a43de395b5f 100644 --- a/app/javascript/mastodon/is_mobile.ts +++ b/app/javascript/mastodon/is_mobile.ts @@ -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'; } }; diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index edecaf60f395d304a245ec5ecf2515ad4132728b..8c85cb7bea9c550cdfefc2e5c223d3023ac11f04 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -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", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index 75b7890d27af5642252463cdfea8c8ab3bc558fd..13eb9762efcb7593d986b98f3236e8ce76c688e6 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -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", diff --git a/app/views/shared/_web_app.html.haml b/app/views/shared/_web_app.html.haml index 998cee9fa9e1679905a284c9568db2209aa51cc8..9a1c3dc0bf432c8973a8e4820af3181abde669d6 100644 --- a/app/views/shared/_web_app.html.haml +++ b/app/views/shared/_web_app.html.haml @@ -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 } diff --git a/config/routes.rb b/config/routes.rb index fa72d8b06546e873f8f4cf9a828632bdee1eb70d..87ee815f415704f81a53b8f15064ef4d3e74f0d5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -30,6 +30,7 @@ Rails.application.routes.draw do /mutes /followed_tags /statuses/(*any) + /deck/(*any) ).freeze root 'home#index' diff --git a/package.json b/package.json index 4f99f25f1e3eaac3c6f60ff6ab8c552369479617..b46dada7d023081a1880cc71dbdc7948bf590ebe 100644 --- a/package.json +++ b/package.json @@ -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",