diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2021-07-01 12:57:12 +0200 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2021-07-01 12:57:12 +0200 |
| commit | bc19f9d15e30ee3b6cea233c60995b9a70368af0 (patch) | |
| tree | a05e106ba78eb1c9a9f856af9b6498acd4db35dd /gui/src | |
| parent | d962e59af7ebe3980686324d430ff1854a832445 (diff) | |
| parent | d8ef9a4b86d402d3d4191c0275f6f1007f3eeb1b (diff) | |
| download | mullvadvpn-bc19f9d15e30ee3b6cea233c60995b9a70368af0.tar.xz mullvadvpn-bc19f9d15e30ee3b6cea233c60995b9a70368af0.zip | |
Merge branch 'rework-history-and-transitions'
Diffstat (limited to 'gui/src')
22 files changed, 207 insertions, 298 deletions
diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx index 9e1555e4ec..36e49c7c77 100644 --- a/gui/src/renderer/app.tsx +++ b/gui/src/renderer/app.tsx @@ -23,7 +23,7 @@ import log, { ConsoleOutput } from '../shared/logging'; import { IRelayListPair, LaunchApplicationResult } from '../shared/ipc-schema'; import consumePromise from '../shared/promise'; import { Scheduler } from '../shared/scheduler'; -import History from './lib/history'; +import History, { transitions } from './lib/history'; import { loadTranslations } from './lib/load-translations'; import { @@ -621,14 +621,27 @@ export default class AppRenderer { } private resetNavigation() { + const pathname = this.history.location.pathname; + if (this.connectedToDaemon) { if (this.settings.accountToken) { - this.history.resetWithIfDifferent('/main'); + const transition = + pathname === '/login' || pathname === '/' ? transitions.push : transitions.dismiss; + this.history.reset('/main', transition); } else { - this.history.resetWithIfDifferent('/login'); + const transition = + pathname === '/main' + ? transitions.pop + : pathname === '/' + ? transitions.push + : transitions.none; + + this.history.reset('/login', transition); } } else { - this.history.resetWithIfDifferent('/'); + const transition = + pathname === '/login' || pathname === '/main' ? transitions.pop : transitions.dismiss; + this.history.reset('/', transition); } } diff --git a/gui/src/renderer/components/ExpiredAccountAddTime.tsx b/gui/src/renderer/components/ExpiredAccountAddTime.tsx index a9b83af6a6..24c335a551 100644 --- a/gui/src/renderer/components/ExpiredAccountAddTime.tsx +++ b/gui/src/renderer/components/ExpiredAccountAddTime.tsx @@ -1,6 +1,5 @@ import React, { useCallback } from 'react'; import { useSelector } from 'react-redux'; -import { useHistory } from 'react-router'; import { sprintf } from 'sprintf-js'; import styled from 'styled-components'; import { links, colors } from '../../config.json'; @@ -8,7 +7,7 @@ import { formatRelativeDate } from '../../shared/date-helper'; import { messages } from '../../shared/gettext'; import { useAppContext } from '../context'; import useActions from '../lib/actionsHook'; -import History from '../lib/history'; +import { transitions, useHistory } from '../lib/history'; import account from '../redux/account/actions'; import { IReduxState } from '../redux/store'; import * as AppButton from './AppButton'; @@ -87,7 +86,7 @@ export function VoucherInput() { }, [history]); const navigateBack = useCallback(() => { - history.goBack(); + history.pop(); }, [history]); return ( @@ -252,7 +251,7 @@ function HeaderBar() { function useFinishedCallback() { const { loggedIn } = useActions(account); - const history = useHistory() as History; + const history = useHistory(); const isNewAccount = useSelector( (state: IReduxState) => state.account.status.type === 'ok' && state.account.status.method === 'new_account', @@ -264,7 +263,7 @@ function useFinishedCallback() { loggedIn(); } - history.resetWith('/main'); + history.reset('/main', undefined, transitions.push); }, [isNewAccount, loggedIn, history]); return callback; diff --git a/gui/src/renderer/components/HeaderBar.tsx b/gui/src/renderer/components/HeaderBar.tsx index b89811913e..9f6d4b82a3 100644 --- a/gui/src/renderer/components/HeaderBar.tsx +++ b/gui/src/renderer/components/HeaderBar.tsx @@ -1,10 +1,10 @@ import React, { useCallback } from 'react'; import { useSelector } from 'react-redux'; -import { useHistory } from 'react-router'; import styled from 'styled-components'; import { colors } from '../../config.json'; import { TunnelState } from '../../shared/daemon-rpc-types'; import { messages } from '../../shared/gettext'; +import { useHistory } from '../lib/history'; import { IReduxState } from '../redux/store'; import { FocusFallback } from './Focus'; import ImageView from './ImageView'; @@ -103,7 +103,7 @@ export function HeaderBarSettingsButton() { const history = useHistory(); const openSettings = useCallback(() => { - history.push('/settings'); + history.show('/settings'); }, [history]); return ( diff --git a/gui/src/renderer/components/KeyboardNavigation.tsx b/gui/src/renderer/components/KeyboardNavigation.tsx index 3aefd7c684..94b5097389 100644 --- a/gui/src/renderer/components/KeyboardNavigation.tsx +++ b/gui/src/renderer/components/KeyboardNavigation.tsx @@ -1,18 +1,17 @@ import React, { useCallback, useEffect } from 'react'; -import { useHistory } from 'react-router'; -import History from '../lib/history'; +import { useHistory } from '../lib/history'; interface IKeyboardNavigationProps { children: React.ReactElement; } export default function KeyboardNavigation(props: IKeyboardNavigationProps) { - const history = useHistory() as History; + const history = useHistory(); const handleKeyDown = useCallback( (event: KeyboardEvent) => { if (event.key === 'Escape') { - history.reset(); + history.dismiss(true); } }, [history.reset], diff --git a/gui/src/renderer/components/LinuxSplitTunnelingSettings.tsx b/gui/src/renderer/components/LinuxSplitTunnelingSettings.tsx index cb7c497011..fea2fe23db 100644 --- a/gui/src/renderer/components/LinuxSplitTunnelingSettings.tsx +++ b/gui/src/renderer/components/LinuxSplitTunnelingSettings.tsx @@ -1,5 +1,4 @@ import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; -import { useHistory } from 'react-router'; import { sprintf } from 'sprintf-js'; import styled from 'styled-components'; import { colors } from '../../config.json'; @@ -21,6 +20,7 @@ import { TitleBarItem, } from './NavigationBar'; import SettingsHeader, { HeaderSubTitle, HeaderTitle } from './SettingsHeader'; +import { useHistory } from '../lib/history'; const StyledPageCover = styled.div({}, (props: { show: boolean }) => ({ position: 'absolute', @@ -154,7 +154,7 @@ export default function LinuxSplitTunnelingSettings() { <NavigationContainer> <NavigationBar> <NavigationItems> - <BackBarItem action={history.goBack}> + <BackBarItem action={history.pop}> { // TRANSLATORS: Back button in navigation bar messages.pgettext('navigation-bar', 'Advanced') diff --git a/gui/src/renderer/components/MainView.tsx b/gui/src/renderer/components/MainView.tsx index 9698321940..3891a6e48b 100644 --- a/gui/src/renderer/components/MainView.tsx +++ b/gui/src/renderer/components/MainView.tsx @@ -1,10 +1,10 @@ import React, { useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; -import { useHistory } from 'react-router'; import { hasExpired } from '../../shared/account-expiry'; import { IReduxState } from '../redux/store'; import ConnectPage from '../containers/ConnectPage'; import ExpiredAccountErrorViewContainer from '../containers/ExpiredAccountErrorViewContainer'; +import { useHistory } from '../lib/history'; export default function MainView() { const history = useHistory(); diff --git a/gui/src/renderer/components/NavigationBar.tsx b/gui/src/renderer/components/NavigationBar.tsx index 746fc6faa4..f9674e1131 100644 --- a/gui/src/renderer/components/NavigationBar.tsx +++ b/gui/src/renderer/components/NavigationBar.tsx @@ -1,9 +1,9 @@ import React, { useCallback, useContext, useLayoutEffect, useRef, useState } from 'react'; import { useSelector } from 'react-redux'; -import { useHistory } from 'react-router'; import { colors } from '../../config.json'; import { messages } from '../../shared/gettext'; import useActions from '../lib/actionsHook'; +import { useHistory } from '../lib/history'; import { useCombinedRefs } from '../lib/utilityHooks'; import { IReduxState } from '../redux/store'; import userInterface from '../redux/userinterface/actions'; diff --git a/gui/src/renderer/components/TransitionContainer.tsx b/gui/src/renderer/components/TransitionContainer.tsx index 5e12acbb56..1d177319c2 100644 --- a/gui/src/renderer/components/TransitionContainer.tsx +++ b/gui/src/renderer/components/TransitionContainer.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import styled from 'styled-components'; -import { ITransitionGroupProps } from '../transitions'; +import { ITransitionSpecification } from '../lib/history'; interface ITransitioningViewProps { viewId: string; @@ -10,10 +10,10 @@ type TransitioningView = React.ReactElement<ITransitioningViewProps>; interface ITransitionQueueItem { view: TransitioningView; - transition: ITransitionGroupProps; + transition: ITransitionSpecification; } -interface IProps extends ITransitionGroupProps { +interface IProps extends ITransitionSpecification { children: TransitioningView; onTransitionEnd: () => void; } diff --git a/gui/src/renderer/containers/AccountPage.tsx b/gui/src/renderer/containers/AccountPage.tsx index accd4287b2..985161c838 100644 --- a/gui/src/renderer/containers/AccountPage.tsx +++ b/gui/src/renderer/containers/AccountPage.tsx @@ -1,10 +1,10 @@ import { connect } from 'react-redux'; -import { RouteComponentProps, withRouter } from 'react-router'; import { links } from '../../config.json'; import consumePromise from '../../shared/promise'; import Account from '../components/Account'; import withAppContext, { IAppContext } from '../context'; +import { IHistoryProps, withHistory } from '../lib/history'; import { IReduxState, ReduxDispatch } from '../redux/store'; const mapStateToProps = (state: IReduxState) => ({ @@ -13,16 +13,16 @@ const mapStateToProps = (state: IReduxState) => ({ expiryLocale: state.userInterface.locale, isOffline: state.connection.isBlocked, }); -const mapDispatchToProps = (_dispatch: ReduxDispatch, props: RouteComponentProps & IAppContext) => { +const mapDispatchToProps = (_dispatch: ReduxDispatch, props: IHistoryProps & IAppContext) => { return { onLogout: () => { consumePromise(props.app.logout()); }, onClose: () => { - props.history.goBack(); + props.history.pop(); }, onBuyMore: () => props.app.openLinkWithAuth(links.purchase), }; }; -export default withAppContext(withRouter(connect(mapStateToProps, mapDispatchToProps)(Account))); +export default withAppContext(withHistory(connect(mapStateToProps, mapDispatchToProps)(Account))); diff --git a/gui/src/renderer/containers/AdvancedSettingsPage.tsx b/gui/src/renderer/containers/AdvancedSettingsPage.tsx index 0aaa4ee3f4..a9e603718b 100644 --- a/gui/src/renderer/containers/AdvancedSettingsPage.tsx +++ b/gui/src/renderer/containers/AdvancedSettingsPage.tsx @@ -1,5 +1,4 @@ import { connect } from 'react-redux'; -import { RouteComponentProps, withRouter } from 'react-router'; import { BridgeState, IDnsOptions, @@ -11,6 +10,7 @@ import RelaySettingsBuilder from '../../shared/relay-settings-builder'; import AdvancedSettings from '../components/AdvancedSettings'; import withAppContext, { IAppContext } from '../context'; +import { IHistoryProps, withHistory } from '../lib/history'; import { RelaySettingsRedux } from '../redux/settings/reducers'; import { IReduxState, ReduxDispatch } from '../redux/store'; @@ -56,10 +56,10 @@ const mapRelaySettingsToProtocolAndPort = (relaySettings: RelaySettingsRedux) => } }; -const mapDispatchToProps = (_dispatch: ReduxDispatch, props: RouteComponentProps & IAppContext) => { +const mapDispatchToProps = (_dispatch: ReduxDispatch, props: IHistoryProps & IAppContext) => { return { onClose: () => { - props.history.goBack(); + props.history.pop(); }, setOpenVpnRelayProtocolAndPort: async (protocol?: RelayProtocol, port?: number) => { const relayUpdate = RelaySettingsBuilder.normal() @@ -169,5 +169,5 @@ const mapDispatchToProps = (_dispatch: ReduxDispatch, props: RouteComponentProps }; export default withAppContext( - withRouter(connect(mapStateToProps, mapDispatchToProps)(AdvancedSettings)), + withHistory(connect(mapStateToProps, mapDispatchToProps)(AdvancedSettings)), ); diff --git a/gui/src/renderer/containers/ConnectPage.tsx b/gui/src/renderer/containers/ConnectPage.tsx index 99882bf8b1..d7197095f1 100644 --- a/gui/src/renderer/containers/ConnectPage.tsx +++ b/gui/src/renderer/containers/ConnectPage.tsx @@ -1,10 +1,10 @@ import { connect } from 'react-redux'; -import { RouteComponentProps, withRouter } from 'react-router'; import { sprintf } from 'sprintf-js'; import { messages } from '../../shared/gettext'; import log from '../../shared/logging'; import Connect from '../components/Connect'; import withAppContext, { IAppContext } from '../context'; +import { IHistoryProps, withHistory } from '../lib/history'; import { IRelayLocationRedux, RelaySettingsRedux } from '../redux/settings/reducers'; import { IReduxState, ReduxDispatch } from '../redux/store'; @@ -71,10 +71,10 @@ const mapStateToProps = (state: IReduxState) => { }; }; -const mapDispatchToProps = (_dispatch: ReduxDispatch, props: RouteComponentProps & IAppContext) => { +const mapDispatchToProps = (_dispatch: ReduxDispatch, props: IHistoryProps & IAppContext) => { return { onSelectLocation: () => { - props.history.push('/select-location'); + props.history.show('/select-location'); }, onConnect: async () => { try { @@ -100,4 +100,4 @@ const mapDispatchToProps = (_dispatch: ReduxDispatch, props: RouteComponentProps }; }; -export default withAppContext(withRouter(connect(mapStateToProps, mapDispatchToProps)(Connect))); +export default withAppContext(withHistory(connect(mapStateToProps, mapDispatchToProps)(Connect))); diff --git a/gui/src/renderer/containers/ExpiredAccountErrorViewContainer.tsx b/gui/src/renderer/containers/ExpiredAccountErrorViewContainer.tsx index 6297a17f42..372f9607c9 100644 --- a/gui/src/renderer/containers/ExpiredAccountErrorViewContainer.tsx +++ b/gui/src/renderer/containers/ExpiredAccountErrorViewContainer.tsx @@ -1,7 +1,7 @@ import { connect } from 'react-redux'; -import { RouteComponentProps, withRouter } from 'react-router'; import log from '../../shared/logging'; import ExpiredAccountErrorView from '../components/ExpiredAccountErrorView'; +import { IHistoryProps, withHistory } from '../lib/history'; import withAppContext, { IAppContext } from '../context'; import { IReduxState, ReduxDispatch } from '../redux/store'; @@ -13,7 +13,7 @@ const mapStateToProps = (state: IReduxState) => ({ isBlocked: state.connection.isBlocked, blockWhenDisconnected: state.settings.blockWhenDisconnected, }); -const mapDispatchToProps = (_dispatch: ReduxDispatch, props: RouteComponentProps & IAppContext) => { +const mapDispatchToProps = (_dispatch: ReduxDispatch, props: IHistoryProps & IAppContext) => { return { onExternalLinkWithAuth: (url: string) => props.app.openLinkWithAuth(url), onDisconnect: async () => { @@ -37,5 +37,5 @@ const mapDispatchToProps = (_dispatch: ReduxDispatch, props: RouteComponentProps }; export default withAppContext( - withRouter(connect(mapStateToProps, mapDispatchToProps)(ExpiredAccountErrorView)), + withHistory(connect(mapStateToProps, mapDispatchToProps)(ExpiredAccountErrorView)), ); diff --git a/gui/src/renderer/containers/PreferencesPage.tsx b/gui/src/renderer/containers/PreferencesPage.tsx index 3bd4a20e44..671ba33c26 100644 --- a/gui/src/renderer/containers/PreferencesPage.tsx +++ b/gui/src/renderer/containers/PreferencesPage.tsx @@ -1,10 +1,10 @@ import { connect } from 'react-redux'; -import { RouteComponentProps, withRouter } from 'react-router'; import { IDnsOptions } from '../../shared/daemon-rpc-types'; import log from '../../shared/logging'; import consumePromise from '../../shared/promise'; import Preferences from '../components/Preferences'; import withAppContext, { IAppContext } from '../context'; +import { IHistoryProps, withHistory } from '../lib/history'; import { IReduxState, ReduxDispatch } from '../redux/store'; const mapStateToProps = (state: IReduxState) => ({ @@ -20,10 +20,10 @@ const mapStateToProps = (state: IReduxState) => ({ dns: state.settings.dns, }); -const mapDispatchToProps = (_dispatch: ReduxDispatch, props: RouteComponentProps & IAppContext) => { +const mapDispatchToProps = (_dispatch: ReduxDispatch, props: IHistoryProps & IAppContext) => { return { onClose: () => { - props.history.goBack(); + props.history.pop(); }, setEnableSystemNotifications: (flag: boolean) => { props.app.setEnableSystemNotifications(flag); @@ -60,5 +60,5 @@ const mapDispatchToProps = (_dispatch: ReduxDispatch, props: RouteComponentProps }; export default withAppContext( - withRouter(connect(mapStateToProps, mapDispatchToProps)(Preferences)), + withHistory(connect(mapStateToProps, mapDispatchToProps)(Preferences)), ); diff --git a/gui/src/renderer/containers/SelectLanguagePage.tsx b/gui/src/renderer/containers/SelectLanguagePage.tsx index 5c073c22e1..1bd21c84dd 100644 --- a/gui/src/renderer/containers/SelectLanguagePage.tsx +++ b/gui/src/renderer/containers/SelectLanguagePage.tsx @@ -1,26 +1,26 @@ import { connect } from 'react-redux'; -import { RouteComponentProps, withRouter } from 'react-router'; import SelectLanguage from '../components/SelectLanguage'; import withAppContext, { IAppContext } from '../context'; +import { IHistoryProps, withHistory } from '../lib/history'; import { IReduxState, ReduxDispatch } from '../redux/store'; const mapStateToProps = (state: IReduxState) => ({ preferredLocale: state.settings.guiSettings.preferredLocale, }); -const mapDispatchToProps = (_dispatch: ReduxDispatch, props: RouteComponentProps & IAppContext) => { +const mapDispatchToProps = (_dispatch: ReduxDispatch, props: IHistoryProps & IAppContext) => { return { preferredLocalesList: props.app.getPreferredLocaleList(), async setPreferredLocale(locale: string) { await props.app.setPreferredLocale(locale); - props.history.goBack(); + props.history.pop(); }, onClose() { - props.history.goBack(); + props.history.pop(); }, }; }; export default withAppContext( - withRouter(connect(mapStateToProps, mapDispatchToProps)(SelectLanguage)), + withHistory(connect(mapStateToProps, mapDispatchToProps)(SelectLanguage)), ); diff --git a/gui/src/renderer/containers/SelectLocationPage.tsx b/gui/src/renderer/containers/SelectLocationPage.tsx index fbad19285b..b3aab7eb69 100644 --- a/gui/src/renderer/containers/SelectLocationPage.tsx +++ b/gui/src/renderer/containers/SelectLocationPage.tsx @@ -1,5 +1,4 @@ import { connect } from 'react-redux'; -import { RouteComponentProps, withRouter } from 'react-router'; import { bindActionCreators } from 'redux'; import BridgeSettingsBuilder from '../../shared/bridge-settings-builder'; import { LiftedConstraint, RelayLocation } from '../../shared/daemon-rpc-types'; @@ -7,6 +6,7 @@ import log from '../../shared/logging'; import RelaySettingsBuilder from '../../shared/relay-settings-builder'; import SelectLocation from '../components/SelectLocation'; import withAppContext, { IAppContext } from '../context'; +import { IHistoryProps, withHistory } from '../lib/history'; import { IReduxState, ReduxDispatch } from '../redux/store'; import userInterfaceActions from '../redux/userinterface/actions'; import { LocationScope } from '../redux/userinterface/reducers'; @@ -40,17 +40,17 @@ const mapStateToProps = (state: IReduxState) => { allowBridgeSelection, }; }; -const mapDispatchToProps = (dispatch: ReduxDispatch, props: RouteComponentProps & IAppContext) => { +const mapDispatchToProps = (dispatch: ReduxDispatch, props: IHistoryProps & IAppContext) => { const userInterface = bindActionCreators(userInterfaceActions, dispatch); return { - onClose: () => props.history.goBack(), + onClose: () => props.history.dismiss(), onChangeLocationScope: (scope: LocationScope) => { userInterface.setLocationScope(scope); }, onSelectExitLocation: async (relayLocation: RelayLocation) => { // dismiss the view first - props.history.goBack(); + props.history.dismiss(); try { const relayUpdate = RelaySettingsBuilder.normal().location.fromRaw(relayLocation).build(); @@ -63,7 +63,7 @@ const mapDispatchToProps = (dispatch: ReduxDispatch, props: RouteComponentProps }, onSelectBridgeLocation: async (bridgeLocation: RelayLocation) => { // dismiss the view first - props.history.goBack(); + props.history.dismiss(); try { await props.app.updateBridgeSettings( @@ -75,7 +75,7 @@ const mapDispatchToProps = (dispatch: ReduxDispatch, props: RouteComponentProps }, onSelectClosestToExit: async () => { // dismiss the view first - props.history.goBack(); + props.history.dismiss(); try { await props.app.updateBridgeSettings(new BridgeSettingsBuilder().location.any().build()); @@ -87,5 +87,5 @@ const mapDispatchToProps = (dispatch: ReduxDispatch, props: RouteComponentProps }; export default withAppContext( - withRouter(connect(mapStateToProps, mapDispatchToProps)(SelectLocation)), + withHistory(connect(mapStateToProps, mapDispatchToProps)(SelectLocation)), ); diff --git a/gui/src/renderer/containers/SettingsPage.tsx b/gui/src/renderer/containers/SettingsPage.tsx index 6985fd927c..97ff917e7e 100644 --- a/gui/src/renderer/containers/SettingsPage.tsx +++ b/gui/src/renderer/containers/SettingsPage.tsx @@ -1,7 +1,7 @@ import { connect } from 'react-redux'; -import { RouteComponentProps, withRouter } from 'react-router'; import Settings from '../components/Settings'; import withAppContext, { IAppContext } from '../context'; +import { IHistoryProps, withHistory } from '../lib/history'; import { IReduxState, ReduxDispatch } from '../redux/store'; const mapStateToProps = (state: IReduxState, props: IAppContext) => ({ @@ -16,10 +16,10 @@ const mapStateToProps = (state: IReduxState, props: IAppContext) => ({ suggestedIsBeta: state.version.suggestedIsBeta ?? false, isOffline: state.connection.isBlocked, }); -const mapDispatchToProps = (_dispatch: ReduxDispatch, props: RouteComponentProps & IAppContext) => { +const mapDispatchToProps = (_dispatch: ReduxDispatch, props: IHistoryProps & IAppContext) => { return { onQuit: () => props.app.quit(), - onClose: () => props.history.goBack(), + onClose: () => props.history.dismiss(), onViewSelectLanguage: () => props.history.push('/settings/language'), onViewAccount: () => props.history.push('/settings/account'), onViewSupport: () => props.history.push('/settings/support'), @@ -29,4 +29,4 @@ const mapDispatchToProps = (_dispatch: ReduxDispatch, props: RouteComponentProps }; }; -export default withAppContext(withRouter(connect(mapStateToProps, mapDispatchToProps)(Settings))); +export default withAppContext(withHistory(connect(mapStateToProps, mapDispatchToProps)(Settings))); diff --git a/gui/src/renderer/containers/SupportPage.tsx b/gui/src/renderer/containers/SupportPage.tsx index 1df4827223..a4bdbd4ac3 100644 --- a/gui/src/renderer/containers/SupportPage.tsx +++ b/gui/src/renderer/containers/SupportPage.tsx @@ -1,9 +1,9 @@ import { connect } from 'react-redux'; -import { RouteComponentProps, withRouter } from 'react-router'; import { bindActionCreators } from 'redux'; import consumePromise from '../../shared/promise'; import Support from '../components/Support'; import withAppContext, { IAppContext } from '../context'; +import { IHistoryProps, withHistory } from '../lib/history'; import { IReduxState, ReduxDispatch } from '../redux/store'; import supportActions from '../redux/support/actions'; @@ -16,12 +16,12 @@ const mapStateToProps = (state: IReduxState) => ({ suggestedIsBeta: state.version.suggestedIsBeta ?? false, }); -const mapDispatchToProps = (dispatch: ReduxDispatch, props: IAppContext & RouteComponentProps) => { +const mapDispatchToProps = (dispatch: ReduxDispatch, props: IAppContext & IHistoryProps) => { const { saveReportForm, clearReportForm } = bindActionCreators(supportActions, dispatch); return { onClose() { - props.history.goBack(); + props.history.pop(); }, viewLog(id: string) { consumePromise(props.app.viewLog(id)); @@ -34,4 +34,4 @@ const mapDispatchToProps = (dispatch: ReduxDispatch, props: IAppContext & RouteC }; }; -export default withAppContext(withRouter(connect(mapStateToProps, mapDispatchToProps)(Support))); +export default withAppContext(withHistory(connect(mapStateToProps, mapDispatchToProps)(Support))); diff --git a/gui/src/renderer/containers/WireguardKeysPage.tsx b/gui/src/renderer/containers/WireguardKeysPage.tsx index e07e4718f2..b0c25e06a9 100644 --- a/gui/src/renderer/containers/WireguardKeysPage.tsx +++ b/gui/src/renderer/containers/WireguardKeysPage.tsx @@ -1,8 +1,8 @@ import { connect } from 'react-redux'; -import { RouteComponentProps, withRouter } from 'react-router'; import { links } from '../../config.json'; import WireguardKeys from '../components/WireguardKeys'; import withAppContext, { IAppContext } from '../context'; +import { IHistoryProps, withHistory } from '../lib/history'; import { IWgKey } from '../redux/settings/reducers'; import { IReduxState, ReduxDispatch } from '../redux/store'; @@ -12,9 +12,9 @@ const mapStateToProps = (state: IReduxState) => ({ tunnelState: state.connection.status, windowFocused: state.userInterface.windowFocused, }); -const mapDispatchToProps = (_dispatch: ReduxDispatch, props: RouteComponentProps & IAppContext) => { +const mapDispatchToProps = (_dispatch: ReduxDispatch, props: IHistoryProps & IAppContext) => { return { - onClose: () => props.history.goBack(), + onClose: () => props.history.pop(), onGenerateKey: () => props.app.generateWireguardKey(), onReplaceKey: (oldKey: IWgKey) => props.app.replaceWireguardKey(oldKey), onVerifyKey: (publicKey: IWgKey) => props.app.verifyWireguardKey(publicKey), @@ -23,5 +23,5 @@ const mapDispatchToProps = (_dispatch: ReduxDispatch, props: RouteComponentProps }; export default withAppContext( - withRouter(connect(mapStateToProps, mapDispatchToProps)(WireguardKeys)), + withHistory(connect(mapStateToProps, mapDispatchToProps)(WireguardKeys)), ); diff --git a/gui/src/renderer/lib/history.ts b/gui/src/renderer/lib/history.tsx index 3f98508ce1..13968e29b9 100644 --- a/gui/src/renderer/lib/history.ts +++ b/gui/src/renderer/lib/history.tsx @@ -1,9 +1,46 @@ import { Location, Action, LocationDescriptor } from 'history'; +import React from 'react'; +import { RouteComponentProps, useHistory as useReactRouterHistory, withRouter } from 'react-router'; + +export interface ITransitionSpecification { + name: string; + duration: number; +} + +interface ITransitionMap { + [name: string]: ITransitionSpecification; +} + +/** + * Transition descriptors + */ +export const transitions: ITransitionMap = { + show: { + name: 'slide-up', + duration: 450, + }, + dismiss: { + name: 'slide-down', + duration: 450, + }, + push: { + name: 'push', + duration: 450, + }, + pop: { + name: 'pop', + duration: 450, + }, + none: { + name: '', + duration: 0, + }, +}; type LocationListener<S = unknown> = ( location: Location<S>, action: Action, - entries: Location<S>[], + transition: ITransitionSpecification, ) => void; // It currently isn't possible to implement this correctly with support for a generic state. State @@ -33,80 +70,90 @@ export default class History { } public push = (nextLocation: LocationDescriptor<S>, nextState?: S) => { - const affectedEntries = [this.entries[this.index]]; - const location = this.createLocation(nextLocation, nextState); - this.lastAction = 'PUSH'; - this.index += 1; - this.entries.splice(this.index, this.entries.length - this.index, location); - this.notify(affectedEntries); + this.pushImpl(nextLocation, nextState); + this.notify(transitions.push); }; - public replace = (nextLocation: LocationDescriptor<S>, nextState?: S) => { - const affectedEntries = [this.entries[this.index]]; - this.entries[this.index] = this.createLocation(nextLocation, nextState); - this.lastAction = 'REPLACE'; - this.notify(affectedEntries); - }; - - public go = (n: number) => { - if (this.canGo(n)) { - const nextIndex = this.index + n; - const affectedEntries = - this.index < nextIndex - ? this.entries.slice(this.index, nextIndex) - : this.entries.slice(nextIndex + 1, this.index + 1); - - this.index = nextIndex; - this.lastAction = 'POP'; - this.notify(affectedEntries); + public pop = () => { + if (this.popImpl()) { + this.notify(transitions.pop); } }; - public goBack = () => this.go(-1); - public goForward = () => this.go(1); + public show = (nextLocation: LocationDescriptor<S>, nextState?: S) => { + this.pushImpl(nextLocation, nextState); + this.notify(transitions.show); + }; - public reset = () => { - const affectedEntries = this.entries.slice(1); - this.lastAction = 'POP'; - this.index = 0; - this.notify(affectedEntries); + public dismiss = (all?: boolean) => { + if (this.popImpl(all ? this.index : 1)) { + this.notify(transitions.dismiss); + } }; - public resetWith = (nextLocation: LocationDescriptor<S>, nextState?: S) => { - const affectedEntries = [...this.entries]; - this.entries = [this.createLocation(nextLocation, nextState)]; + public reset = ( + nextLocation: LocationDescriptor<S>, + transition?: ITransitionSpecification, + nextState?: S, + ) => { + const location = this.createLocation(nextLocation, nextState); this.lastAction = 'REPLACE'; this.index = 0; - this.notify(affectedEntries); - }; + this.entries = [location]; - public resetWithIfDifferent = (nextLocation: LocationDescriptor<S>, nextState?: S) => { - const location = this.createLocation(nextLocation, nextState); - if (this.entries[0].pathname !== location.pathname) { - this.resetWith(nextLocation, nextState); - } + this.notify(transition ?? transitions.none); }; + public listen(callback: LocationListener<S>) { + this.listeners.push(callback); + return () => (this.listeners = this.listeners.filter((listener) => listener !== callback)); + } + public canGo(n: number) { const nextIndex = this.index + n; return nextIndex >= 0 && nextIndex < this.entries.length; } - public listen(callback: LocationListener<S>) { - this.listeners.push(callback); - return () => (this.listeners = this.listeners.filter((listener) => listener !== callback)); + public block(): never { + throw Error('Not implemented'); } - - public block(): () => void { + public replace(): never { throw Error('Not implemented'); } - - public createHref(): string { + public go(): never { + throw Error('Not implemented'); + } + public goBack(): never { + throw Error('Not implemented'); + } + public goForward(): never { + throw Error('Not implemented'); + } + public createHref(): never { throw Error('Not implemented'); } - private notify(affectedEntries: Location<S>[]) { - this.listeners.forEach((listener) => listener(this.location, this.action, affectedEntries)); + private pushImpl(nextLocation: LocationDescriptor<S>, nextState?: S) { + const location = this.createLocation(nextLocation, nextState); + this.lastAction = 'PUSH'; + this.index += 1; + this.entries.splice(this.index, this.entries.length - this.index, location); + } + + private popImpl(n = 1): boolean { + if (this.canGo(-n)) { + this.lastAction = 'POP'; + this.index -= n; + this.entries = this.entries.slice(0, this.index + 1); + + return true; + } else { + return false; + } + } + + private notify(transition: ITransitionSpecification) { + this.listeners.forEach((listener) => listener(this.location, this.action, transition)); } private createLocation(location: LocationDescriptor<S>, state?: S): Location<S> { @@ -133,3 +180,19 @@ export default class History { return Math.random().toString(36).substr(8); } } + +export function useHistory(): History { + return useReactRouterHistory() as History; +} + +export interface IHistoryProps { + history: History; +} + +export function withHistory<P>(BaseComponent: React.ComponentType<P & IHistoryProps>) { + return withRouter((props: P & RouteComponentProps) => { + const history = props.history as History; + const mergedProps = ({ ...props, history } as unknown) as P & IHistoryProps; + return <BaseComponent {...mergedProps} />; + }); +} diff --git a/gui/src/renderer/lib/transition-rule.ts b/gui/src/renderer/lib/transition-rule.ts deleted file mode 100644 index 91d5cd6d4c..0000000000 --- a/gui/src/renderer/lib/transition-rule.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Action } from 'history'; - -export interface ITransitionDescriptor { - name: string; - duration: number; -} - -export interface ITransitionFork { - forward: ITransitionDescriptor; - backward: ITransitionDescriptor; -} - -export interface ITransitionMatch { - direction: 'forward' | 'backward'; - descriptor: ITransitionDescriptor; -} - -export default class TransitionRule { - constructor(private from: string | null, private to: string, private fork: ITransitionFork) {} - - public match( - fromRoute: string | null, - toRoute: string, - action?: Action, - ): ITransitionMatch | null { - if (action !== 'POP' && (!this.from || this.from === fromRoute) && this.to === toRoute) { - return { - direction: 'forward', - descriptor: this.fork.forward, - }; - } - - if (action !== 'PUSH' && (!this.from || this.from === toRoute) && this.to === fromRoute) { - return { - direction: 'backward', - descriptor: this.fork.backward, - }; - } - - return null; - } -} diff --git a/gui/src/renderer/routes.tsx b/gui/src/renderer/routes.tsx index fe49469092..68605821d4 100644 --- a/gui/src/renderer/routes.tsx +++ b/gui/src/renderer/routes.tsx @@ -1,6 +1,6 @@ import { Action } from 'history'; import * as React from 'react'; -import { Route, RouteComponentProps, Switch, withRouter } from 'react-router'; +import { Route, Switch } from 'react-router'; import Launch from './components/Launch'; import KeyboardNavigation from './components/KeyboardNavigation'; import MainView from './components/MainView'; @@ -17,8 +17,7 @@ import SelectLocationPage from './containers/SelectLocationPage'; import SettingsPage from './containers/SettingsPage'; import SupportPage from './containers/SupportPage'; import WireguardKeysPage from './containers/WireguardKeysPage'; -import History from './lib/history'; -import { getTransitionProps } from './transitions'; +import { IHistoryProps, ITransitionSpecification, transitions, withHistory } from './lib/history'; import { SetupFinished, TimeAdded, @@ -27,36 +26,35 @@ import { } from './components/ExpiredAccountAddTime'; interface IAppRoutesState { - previousLocation?: RouteComponentProps['location']; - currentLocation: RouteComponentProps['location']; + currentLocation: IHistoryProps['history']['location']; + transition: ITransitionSpecification; action?: Action; } -class AppRoutes extends React.Component<RouteComponentProps, IAppRoutesState> { +class AppRoutes extends React.Component<IHistoryProps, IAppRoutesState> { private unobserveHistory?: () => void; private focusRef = React.createRef<IFocusHandle>(); - constructor(props: RouteComponentProps) { + constructor(props: IHistoryProps) { super(props); this.state = { - currentLocation: props.location, + currentLocation: props.history.location, + transition: transitions.none, }; } public componentDidMount() { // React throttles updates, so it's impossible to capture the intermediate navigation without // listening to the history directly. - this.unobserveHistory = (this.props.history as History).listen( - (location, action, affectedEntries) => { - this.setState({ - previousLocation: affectedEntries[0], - currentLocation: location, - action, - }); - }, - ); + this.unobserveHistory = this.props.history.listen((location, action, transition) => { + this.setState({ + currentLocation: location, + transition, + action, + }); + }); } public componentWillUnmount() { @@ -67,17 +65,12 @@ class AppRoutes extends React.Component<RouteComponentProps, IAppRoutesState> { public render() { const location = this.state.currentLocation; - const transitionProps = getTransitionProps( - this.state.previousLocation ? this.state.previousLocation.pathname : null, - location.pathname, - this.state.action, - ); return ( <PlatformWindowContainer> <KeyboardNavigation> <Focus ref={this.focusRef}> - <TransitionContainer onTransitionEnd={this.onNavigation} {...transitionProps}> + <TransitionContainer onTransitionEnd={this.onNavigation} {...this.state.transition}> <TransitionView viewId={location.key || ''}> <Switch key={location.key} location={location}> <Route exact={true} path="/" component={Launch} /> @@ -122,6 +115,6 @@ class AppRoutes extends React.Component<RouteComponentProps, IAppRoutesState> { }; } -const AppRoutesWithRouter = withRouter(AppRoutes); +const AppRoutesWithRouter = withHistory(AppRoutes); export default AppRoutesWithRouter; diff --git a/gui/src/renderer/transitions.ts b/gui/src/renderer/transitions.ts deleted file mode 100644 index cddf8c57a2..0000000000 --- a/gui/src/renderer/transitions.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { Action } from 'history'; -import TransitionRule, { ITransitionDescriptor, ITransitionFork } from './lib/transition-rule'; - -export interface ITransitionGroupProps { - name: string; - duration: number; -} - -interface ITransitionMap { - [name: string]: ITransitionFork; -} - -/** - * Transition descriptors - */ -const transitions: ITransitionMap = { - slide: { - forward: { - name: 'slide-up', - duration: 450, - }, - backward: { - name: 'slide-down', - duration: 450, - }, - }, - push: { - forward: { - name: 'push', - duration: 450, - }, - backward: { - name: 'pop', - duration: 450, - }, - }, -}; - -/** - * Transition rules - * (null) is used to indicate any route. - */ -const transitionRules = [ - r('/settings', '/settings/language', transitions.push), - r('/settings', '/settings/account', transitions.push), - r('/settings', '/settings/preferences', transitions.push), - r('/settings', '/settings/advanced', transitions.push), - r('/settings/advanced', '/settings/advanced/wireguard-keys', transitions.push), - r('/settings/advanced', '/settings/advanced/linux-split-tunneling', transitions.push), - r('/settings', '/settings/support', transitions.push), - r('/main', '/main/voucher/redeem', transitions.push), - r('/main/voucher/redeem', '/main/voucher/success', transitions.push), - r('/main/voucher/success', '/main/setup-finished', transitions.push), - r('/main/voucher/success', '/main', transitions.push), - r('/main/time-added', '/main/setup-finished', transitions.push), - r('/main/time-added', '/main', transitions.push), - r('/main', '/main/time-added', transitions.push), - r('/main/setup-finished', '/main', transitions.push), - r(null, '/settings', transitions.slide), - r(null, '/select-location', transitions.slide), -]; - -/** - * Calculate TransitionGroup props. - * - * @param {string} [fromRoute] - source route - * @param {string} toRoute - target route - */ -export function getTransitionProps( - fromRoute: string | null, - toRoute: string, - action?: Action, -): ITransitionGroupProps { - // ignore initial transition and transition between the same routes - if (!fromRoute || fromRoute === toRoute) { - return noTransitionProps(); - } - - for (const rule of transitionRules) { - const match = rule.match(fromRoute, toRoute, action); - if (match) { - return toTransitionGroupProps(match.descriptor); - } - } - - return noTransitionProps(); -} - -/** - * Integrate ITransitionDescriptor into ITransitionGroupProps - * @param {ITransitionDescriptor} descriptor - */ -function toTransitionGroupProps(descriptor: ITransitionDescriptor): ITransitionGroupProps { - const { name, duration } = descriptor; - return { - name, - duration, - }; -} - -/** - * Returns default props with no animation - */ -function noTransitionProps(): ITransitionGroupProps { - return { - name: '', - duration: 0, - }; -} - -/** - * Shortcut to create TransitionRule - */ -function r(from: string | null, to: string, fork: ITransitionFork): TransitionRule { - return new TransitionRule(from, to, fork); -} |
