summaryrefslogtreecommitdiffhomepage
path: root/gui/src
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2021-07-01 12:57:12 +0200
committerOskar Nyberg <oskar@mullvad.net>2021-07-01 12:57:12 +0200
commitbc19f9d15e30ee3b6cea233c60995b9a70368af0 (patch)
treea05e106ba78eb1c9a9f856af9b6498acd4db35dd /gui/src
parentd962e59af7ebe3980686324d430ff1854a832445 (diff)
parentd8ef9a4b86d402d3d4191c0275f6f1007f3eeb1b (diff)
downloadmullvadvpn-bc19f9d15e30ee3b6cea233c60995b9a70368af0.tar.xz
mullvadvpn-bc19f9d15e30ee3b6cea233c60995b9a70368af0.zip
Merge branch 'rework-history-and-transitions'
Diffstat (limited to 'gui/src')
-rw-r--r--gui/src/renderer/app.tsx21
-rw-r--r--gui/src/renderer/components/ExpiredAccountAddTime.tsx9
-rw-r--r--gui/src/renderer/components/HeaderBar.tsx4
-rw-r--r--gui/src/renderer/components/KeyboardNavigation.tsx7
-rw-r--r--gui/src/renderer/components/LinuxSplitTunnelingSettings.tsx4
-rw-r--r--gui/src/renderer/components/MainView.tsx2
-rw-r--r--gui/src/renderer/components/NavigationBar.tsx2
-rw-r--r--gui/src/renderer/components/TransitionContainer.tsx6
-rw-r--r--gui/src/renderer/containers/AccountPage.tsx8
-rw-r--r--gui/src/renderer/containers/AdvancedSettingsPage.tsx8
-rw-r--r--gui/src/renderer/containers/ConnectPage.tsx8
-rw-r--r--gui/src/renderer/containers/ExpiredAccountErrorViewContainer.tsx6
-rw-r--r--gui/src/renderer/containers/PreferencesPage.tsx8
-rw-r--r--gui/src/renderer/containers/SelectLanguagePage.tsx10
-rw-r--r--gui/src/renderer/containers/SelectLocationPage.tsx14
-rw-r--r--gui/src/renderer/containers/SettingsPage.tsx8
-rw-r--r--gui/src/renderer/containers/SupportPage.tsx8
-rw-r--r--gui/src/renderer/containers/WireguardKeysPage.tsx8
-rw-r--r--gui/src/renderer/lib/history.tsx (renamed from gui/src/renderer/lib/history.ts)165
-rw-r--r--gui/src/renderer/lib/transition-rule.ts42
-rw-r--r--gui/src/renderer/routes.tsx41
-rw-r--r--gui/src/renderer/transitions.ts116
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);
-}