diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2020-11-02 09:34:37 +0100 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2020-11-02 09:34:37 +0100 |
| commit | 45d8a1e36b968775d8f0966f07ee9e40e1dbb66d (patch) | |
| tree | b24281d35226027c799aeb666f89a5352e771c88 /gui/src | |
| parent | 01f0dc92f693976bc02b6d50c322b8cb3de1e514 (diff) | |
| parent | c89e9abf27059c9b979c239126c61cbc226c35b3 (diff) | |
| download | mullvadvpn-45d8a1e36b968775d8f0966f07ee9e40e1dbb66d.tar.xz mullvadvpn-45d8a1e36b968775d8f0966f07ee9e40e1dbb66d.zip | |
Merge branch 'implement-custom-history'
Diffstat (limited to 'gui/src')
| -rw-r--r-- | gui/src/renderer/app.tsx | 20 | ||||
| -rw-r--r-- | gui/src/renderer/lib/history.ts | 112 | ||||
| -rw-r--r-- | gui/src/renderer/redux/store.ts | 2 |
3 files changed, 123 insertions, 11 deletions
diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx index b7c9b1a187..4d7b9e9931 100644 --- a/gui/src/renderer/app.tsx +++ b/gui/src/renderer/app.tsx @@ -5,7 +5,6 @@ import { } from 'connected-react-router'; import { ipcRenderer, shell, webFrame } from 'electron'; import log from 'electron-log'; -import { createMemoryHistory } from 'history'; import * as React from 'react'; import { Provider } from 'react-redux'; import { bindActionCreators } from 'redux'; @@ -29,6 +28,7 @@ import { IpcRendererEventChannel, IRelayListPair } from '../shared/ipc-event-cha import ISplitTunnelingApplication from '../shared/linux-split-tunneling-application'; import { getRendererLogFile, setupLogging } from '../shared/logging'; import consumePromise from '../shared/promise'; +import History from './lib/history'; import { AccountToken, @@ -76,8 +76,8 @@ const SUPPORTED_LOCALE_LIST = [ ]; export default class AppRenderer { - private memoryHistory = createMemoryHistory(); - private reduxStore = configureStore(this.memoryHistory); + private history = new History('/'); + private reduxStore = configureStore(this.history); private reduxActions = { account: bindActionCreators(accountActions, this.reduxStore.dispatch), connection: bindActionCreators(connectionActions, this.reduxStore.dispatch), @@ -229,7 +229,7 @@ export default class AppRenderer { return ( <AppContext.Provider value={{ app: this }}> <Provider store={this.reduxStore}> - <ConnectedRouter history={this.memoryHistory}> + <ConnectedRouter history={this.history}> <ErrorBoundary> <AppRoutes /> </ErrorBoundary> @@ -450,7 +450,7 @@ export default class AppRenderer { private redirectToConnect() { // Redirect the user after some time to allow for the 'Logged in' screen to be visible - this.loginTimer = global.setTimeout(() => this.memoryHistory.replace('/connect'), 1000); + this.loginTimer = global.setTimeout(() => this.history.resetWith('/connect'), 1000); } private loadTranslations(locale: string) { @@ -530,12 +530,12 @@ export default class AppRenderer { this.connectedToDaemon = true; if (this.settings.accountToken) { - this.memoryHistory.replace('/connect'); + this.history.resetWith('/connect'); // try to autoconnect the tunnel await this.autoConnect(); } else { - this.memoryHistory.replace('/login'); + this.history.resetWith('/login'); // show window when account is not set ipcRenderer.send('show-window'); @@ -548,7 +548,7 @@ export default class AppRenderer { this.connectedToDaemon = false; if (error && wasConnected) { - this.memoryHistory.replace('/'); + this.history.resetWith('/'); } } @@ -660,12 +660,12 @@ export default class AppRenderer { clearTimeout(this.loginTimer); } reduxAccount.loggedOut(); - this.memoryHistory.replace('/login'); + this.history.resetWith('/login'); } else if (newAccount && oldAccount !== newAccount && !this.doingLogin) { reduxAccount.updateAccountToken(newAccount); reduxAccount.loggedIn(); if (!oldAccount) { - this.memoryHistory.replace('/connect'); + this.history.resetWith('/connect'); } } diff --git a/gui/src/renderer/lib/history.ts b/gui/src/renderer/lib/history.ts new file mode 100644 index 0000000000..b726e72bbb --- /dev/null +++ b/gui/src/renderer/lib/history.ts @@ -0,0 +1,112 @@ +import { Location, Action, LocationListener, LocationDescriptor } from 'history'; + +// It currently isn't possible to implement this correctly with support for a generic state. State +// can be added as a generic type (<S = unknown>) after this issue has been resolved: +// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/49060 +type S = unknown; +export default class History { + private listeners: LocationListener<S>[] = []; + private entries: Location<S>[]; + private index = 0; + private lastAction: Action = 'POP'; + + public constructor(location: string | Location<S>, state?: S) { + this.entries = [this.createLocation(location, state)]; + } + + public get location(): Location<S> { + return this.entries[this.index]; + } + + public get length(): number { + return this.entries.length; + } + + public get action(): Action { + return this.lastAction; + } + + public push = (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); + this.notify(); + }; + + public replace = (nextLocation: LocationDescriptor<S>, nextState?: S) => { + this.entries[this.index] = this.createLocation(nextLocation, nextState); + this.lastAction = 'REPLACE'; + this.notify(); + }; + + public go = (n: number) => { + if (this.canGo(n)) { + this.index += n; + this.lastAction = 'POP'; + this.notify(); + } + }; + + public goBack = () => this.go(-1); + public goForward = () => this.go(1); + + public reset = () => { + this.lastAction = 'POP'; + this.index = 0; + this.notify(); + }; + + public resetWith = (nextLocation: LocationDescriptor<S>, nextState?: S) => { + this.entries = [this.createLocation(nextLocation, nextState)]; + this.lastAction = 'REPLACE'; + this.index = 0; + this.notify(); + }; + + 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(): () => void { + throw Error('Not implemented'); + } + + public createHref(): string { + throw Error('Not implemented'); + } + + private notify() { + this.listeners.forEach((listener) => listener(this.location, this.action)); + } + + private createLocation(location: LocationDescriptor<S>, state?: S): Location<S> { + if (typeof location === 'object') { + return { + pathname: location.pathname ?? this.location.pathname, + search: location.search ?? '', + hash: location.hash ?? '', + state: location.state, + key: location.key ?? this.getRandomKey(), + }; + } else { + return { + pathname: location, + search: '', + hash: '', + state, + key: this.getRandomKey(), + }; + } + } + + private getRandomKey() { + return Math.random().toString(36).substr(8); + } +} diff --git a/gui/src/renderer/redux/store.ts b/gui/src/renderer/redux/store.ts index bedaddf1bd..43ebc312ef 100644 --- a/gui/src/renderer/redux/store.ts +++ b/gui/src/renderer/redux/store.ts @@ -14,7 +14,7 @@ import userInterfaceReducer, { IUserInterfaceReduxState } from './userinterface/ import versionActions, { VersionAction } from './version/actions'; import versionReducer, { IVersionReduxState } from './version/reducers'; -import { History } from 'history'; +import History from '../lib/history'; export interface IReduxState { account: IAccountReduxState; |
