From 5fad7bd58a9c2541938f63e73a6ebdbfd0045903 Mon Sep 17 00:00:00 2001 From: Stanislas Signoud Date: Thu, 13 Jul 2023 17:18:09 +0200 Subject: [PATCH] Change links in multi-column mode so tabs are open in single-column mode (#25893) --- app/javascript/mastodon/components/router.tsx | 23 ++++++++++++++++++ .../mastodon/containers/mastodon.jsx | 7 +++--- .../ui/components/navigation_panel.jsx | 8 +++++++ app/javascript/mastodon/features/ui/index.jsx | 24 +++++++++++-------- .../features/ui/util/react_router_helpers.jsx | 12 ++++++++-- app/javascript/mastodon/initial_state.js | 7 ++++++ app/javascript/mastodon/is_mobile.ts | 10 ++++---- app/javascript/mastodon/locales/en.json | 1 + app/javascript/mastodon/locales/fr.json | 1 + app/views/shared/_web_app.html.haml | 1 + config/routes.rb | 1 + package.json | 1 + 12 files changed, 77 insertions(+), 19 deletions(-) create mode 100644 app/javascript/mastodon/components/router.tsx diff --git a/app/javascript/mastodon/components/router.tsx b/app/javascript/mastodon/components/router.tsx new file mode 100644 index 0000000000..c82711790b --- /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 = ({ children }) => { + return {children}; +}; diff --git a/app/javascript/mastodon/containers/mastodon.jsx b/app/javascript/mastodon/containers/mastodon.jsx index 4538db050d..59efc80570 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 { - + - + diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx index dc406fa55c..ab5c78246f 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 {
+ + {transientSingleColumn && ( + + {intl.formatMessage(messages.advancedInterface)} + + )}
diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index b38acfc14d..ae81a354b2 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 = ; } else { - redirect = ; + redirect = ; } } else if (singleUserMode && owner && initialState?.accounts[owner]) { redirect = ; @@ -177,10 +178,13 @@ class SwitchingColumnsArea extends PureComponent { } return ( - + {redirect} + {singleColumn ? : null} + {singleColumn && pathName.startsWith('/deck/') ? : null} + @@ -573,7 +577,7 @@ class UI extends PureComponent {
- + {children} 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 66cfee9708..9927726857 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 ( - - {Children.map(children, child => cloneElement(child, { multiColumn }))} + + {Children.map(children, child => child ? cloneElement(child, { multiColumn }) : null)} ); } diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js index 5ad61e1f6b..67fb068432 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 36cde21332..7f339e287b 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 edecaf60f3..8c85cb7bea 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 75b7890d27..13eb9762ef 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 998cee9fa9..9a1c3dc0bf 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 fa72d8b065..87ee815f41 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 4f99f25f1e..b46dada7d0 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",