diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2019-02-04 14:43:45 +0100 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2019-02-08 12:12:10 +0100 |
| commit | 3e99a6c9cdb95056bbd4c61e82fb66d40649cd6b (patch) | |
| tree | 4aece2a2c47a2579a835a5d3e08d59e63e1c91e7 /gui/packages | |
| parent | 552f06b3793ace66d8581d9b5249ef165bd89971 (diff) | |
| download | mullvadvpn-3e99a6c9cdb95056bbd4c61e82fb66d40649cd6b.tar.xz mullvadvpn-3e99a6c9cdb95056bbd4c61e82fb66d40649cd6b.zip | |
Fix linter issues
Diffstat (limited to 'gui/packages')
76 files changed, 2803 insertions, 2782 deletions
diff --git a/gui/packages/components/package.json b/gui/packages/components/package.json index a085f7fc38..fb96900c35 100644 --- a/gui/packages/components/package.json +++ b/gui/packages/components/package.json @@ -36,7 +36,6 @@ "rimraf": "^2.6.2", "ts-node": "^7.0.1", "tslint": "^5.12.1", - "tslint-config-prettier": "^1.17.0", "typescript": "^3.2.4" }, "dependencies": {}, diff --git a/gui/packages/config/package.json b/gui/packages/config/package.json index b876bce389..fac7c77025 100644 --- a/gui/packages/config/package.json +++ b/gui/packages/config/package.json @@ -1,5 +1,9 @@ { "private": true, "name": "@mullvad/config", - "version": "0.1.0" + "version": "0.1.0", + "devDependencies": { + "tslint-config-prettier": "^1.17.0", + "tslint-react": "^3.6.0" + } } diff --git a/gui/packages/config/tslint.json b/gui/packages/config/tslint.json index b5f8043a6b..1022d301af 100644 --- a/gui/packages/config/tslint.json +++ b/gui/packages/config/tslint.json @@ -2,12 +2,18 @@ "defaultSeverity": "error", "extends": [ "tslint:latest", + "tslint-react", "tslint-config-prettier" ], - "jsRules": {}, "rules": { "max-classes-per-file": false, - "object-literal-sort-keys": false - }, - "rulesDirectory": [] + "object-literal-sort-keys": false, + "variable-name": [ + true, + "ban-keywords", + "check-format", + "allow-pascal-case", + "allow-leading-underscore" + ] + } } diff --git a/gui/packages/desktop/package.json b/gui/packages/desktop/package.json index f25c1f548b..68fe6a59fd 100644 --- a/gui/packages/desktop/package.json +++ b/gui/packages/desktop/package.json @@ -38,19 +38,19 @@ }, "devDependencies": { "@types/chai": "^4.1.7", - "@types/chai-spies": "^1.0.0", "@types/chai-as-promised": "^7.1.0", + "@types/chai-spies": "^1.0.0", "@types/d3-geo": "^1.11.0", "@types/enzyme": "^3.1.15", "@types/enzyme-adapter-react-16": "^1.0.3", + "@types/mkdirp": "^0.5.2", "@types/node": "^10.12.3", "@types/rbush": "^2.0.2", "@types/react": "16.3.18", "@types/react-dom": "16.0.7", - "@types/react-router": "^4.4.3", "@types/react-redux": "^7.0.0", + "@types/react-router": "^4.4.3", "@types/sinon": "^7.0.5", - "@types/mkdirp": "^0.5.2", "@types/uuid": "^3.4.4", "browser-sync": "^2.26.3", "chai": "^4.2.0", @@ -69,13 +69,12 @@ "sinon": "^7.1.1", "ts-node": "^7.0.1", "tslint": "^5.12.1", - "tslint-config-prettier": "^1.17.0", "typescript": "^3.2.4" }, "scripts": { "postinstall": "electron-builder install-app-deps", "build": "run-s private:clean private:copy-assets private:compile", - "lint": "tslint -t stylish -p . || true", + "lint": "tslint -t stylish -p .", "develop": "cross-env run-s private:copy-assets private:compile:dev && run-p -r private:watch private:serve", "test": "electron-mocha --renderer -R spec --require ts-node/register --require-main ts-node/register --require-main \"test/setup/main.ts\" --preload \"test/setup/renderer.ts\" \"test/*.spec.ts\" \"test/**/*.spec.ts\" \"test/**/*.spec.tsx\" || true", "pack:mac": "run-s build private:build:mac private:postbuild:mac", diff --git a/gui/packages/desktop/src/main/autostart.ts b/gui/packages/desktop/src/main/autostart.ts index 512a53d4ce..a42691550c 100644 --- a/gui/packages/desktop/src/main/autostart.ts +++ b/gui/packages/desktop/src/main/autostart.ts @@ -1,8 +1,8 @@ +import { app } from 'electron'; +import log from 'electron-log'; import * as fs from 'fs'; import * as path from 'path'; import { promisify } from 'util'; -import { app } from 'electron'; -import log from 'electron-log'; const DESKTOP_FILE_NAME = 'mullvad-vpn.desktop'; diff --git a/gui/packages/desktop/src/main/daemon-rpc.ts b/gui/packages/desktop/src/main/daemon-rpc.ts index ff319f90a2..eac4918ed2 100644 --- a/gui/packages/desktop/src/main/daemon-rpc.ts +++ b/gui/packages/desktop/src/main/daemon-rpc.ts @@ -1,36 +1,35 @@ -import JsonRpcClient, { - RemoteError as JsonRpcRemoteError, - TimeOutError as JsonRpcTimeOutError, - SocketTransport, -} from './jsonrpc-client'; -import { CommunicationError, InvalidAccountError, NoDaemonError } from './errors'; import { - AccountData, AccountToken, - AppVersionInfo, - Location, - RelayList, + IAccountData, + IAppVersionInfo, + ILocation, + IRelayList, + ISettings, RelaySettingsUpdate, - Settings, TunnelStateTransition, } from '../shared/daemon-rpc-types'; +import { CommunicationError, InvalidAccountError, NoDaemonError } from './errors'; +import JsonRpcClient, { + RemoteError as JsonRpcRemoteError, + SocketTransport, + TimeOutError as JsonRpcTimeOutError, +} from './jsonrpc-client'; +import { validate } from 'validated/object'; import { - object, - partialObject, - maybe, - string, - number, + arrayOf, boolean, enumeration, - arrayOf, + maybe, + Node as SchemaNode, + number, + object, oneOf, + partialObject, + string, } from 'validated/schema'; -import { validate } from 'validated/object'; -import { Node as SchemaNode } from 'validated/schema'; - -const LocationSchema = maybe( +const locationSchema = maybe( partialObject({ ip: maybe(string), country: string, @@ -51,7 +50,7 @@ const constraint = <T>(constraintValue: SchemaNode<T>) => { ); }; -const CustomTunnelEndpoint = oneOf( +const customTunnelEndpointSchema = oneOf( object({ openvpn: object({ endpoint: object({ @@ -78,7 +77,7 @@ const CustomTunnelEndpoint = oneOf( }), ); -const RelaySettingsSchema = oneOf( +const relaySettingsSchema = oneOf( object({ normal: partialObject({ location: constraint( @@ -107,12 +106,12 @@ const RelaySettingsSchema = oneOf( object({ custom_tunnel_endpoint: partialObject({ host: string, - config: CustomTunnelEndpoint, + config: customTunnelEndpointSchema, }), }), ); -const RelayListSchema = partialObject({ +const relayListSchema = partialObject({ countries: arrayOf( partialObject({ name: string, @@ -137,7 +136,7 @@ const RelayListSchema = partialObject({ ), }); -const OpenVpnProxySchema = maybe( +const openVpnProxySchema = maybe( oneOf( object({ local: partialObject({ @@ -159,10 +158,10 @@ const OpenVpnProxySchema = maybe( ), ); -const TunnelOptionsSchema = partialObject({ +const tunnelOptionsSchema = partialObject({ openvpn: partialObject({ mssfix: maybe(number), - proxy: OpenVpnProxySchema, + proxy: openVpnProxySchema, }), wireguard: partialObject({ mtu: maybe(number), @@ -174,11 +173,11 @@ const TunnelOptionsSchema = partialObject({ }), }); -const AccountDataSchema = partialObject({ +const accountDataSchema = partialObject({ expiry: string, }); -const TunnelStateTransitionSchema = oneOf( +const tunnelStateTransitionSchema = oneOf( object({ state: enumeration('disconnecting'), details: enumeration('nothing', 'block', 'reconnect'), @@ -216,7 +215,7 @@ const TunnelStateTransitionSchema = oneOf( }), ); -const AppVersionInfoSchema = partialObject({ +const appVersionInfoSchema = partialObject({ current_is_supported: boolean, latest: partialObject({ latest_stable: string, @@ -225,60 +224,56 @@ const AppVersionInfoSchema = partialObject({ }); export class ConnectionObserver { - _openHandler: () => void; - _closeHandler: (error?: Error) => void; - - constructor(openHandler: () => void, closeHandler: (error?: Error) => void) { - this._openHandler = openHandler; - this._closeHandler = closeHandler; - } + constructor(private openHandler: () => void, private closeHandler: (error?: Error) => void) {} - _onOpen = () => { - this._openHandler(); + // Only meant to be called by DaemonRpc + // @internal + public onOpen = () => { + this.openHandler(); }; - _onClose = (error?: Error) => { - this._closeHandler(error); + // Only meant to be called by DaemonRpc + // @internal + public onClose = (error?: Error) => { + this.closeHandler(error); }; } export class SubscriptionListener<T> { - _eventHandler: (payload: T) => void; - _errorHandler: (error: Error) => void; - - constructor(eventHandler: (payload: T) => void, errorHandler: (error: Error) => void) { - this._eventHandler = eventHandler; - this._errorHandler = errorHandler; - } + constructor( + private eventHandler: (payload: T) => void, + private errorHandler: (error: Error) => void, + ) {} - _onEvent(payload: T) { - this._eventHandler(payload); + // Only meant to be called by DaemonRpc + // @internal + public onEvent(payload: T) { + this.eventHandler(payload); } - _onError(error: Error) { - this._errorHandler(error); + // Only meant to be called by DaemonRpc + // @internal + public onError(error: Error) { + this.errorHandler(error); } } -const SettingsSchema = partialObject({ +const settingsSchema = partialObject({ account_token: maybe(string), allow_lan: boolean, auto_connect: boolean, block_when_disconnected: boolean, - relay_settings: RelaySettingsSchema, - tunnel_options: TunnelOptionsSchema, + relay_settings: relaySettingsSchema, + tunnel_options: tunnelOptionsSchema, }); export class ResponseParseError extends Error { - _validationError?: Error; - - constructor(message: string, validationError?: Error) { + constructor(message: string, private validationErrorValue?: Error) { super(message); - this._validationError = validationError; } get validationError(): Error | undefined { - return this._validationError; + return this.validationErrorValue; } } @@ -286,28 +281,28 @@ export class ResponseParseError extends Error { const NETWORK_CALL_TIMEOUT = 10000; export class DaemonRpc { - _transport = new JsonRpcClient(new SocketTransport()); + private transport = new JsonRpcClient(new SocketTransport()); - connect(connectionParams: { path: string }) { - this._transport.connect(connectionParams); + public connect(connectionParams: { path: string }) { + this.transport.connect(connectionParams); } - disconnect() { - this._transport.disconnect(); + public disconnect() { + this.transport.disconnect(); } - addConnectionObserver(observer: ConnectionObserver) { - this._transport.on('open', observer._onOpen).on('close', observer._onClose); + public addConnectionObserver(observer: ConnectionObserver) { + this.transport.on('open', observer.onOpen).on('close', observer.onClose); } - removeConnectionObserver(observer: ConnectionObserver) { - this._transport.off('open', observer._onOpen).off('close', observer._onClose); + public removeConnectionObserver(observer: ConnectionObserver) { + this.transport.off('open', observer.onOpen).off('close', observer.onClose); } - async getAccountData(accountToken: AccountToken): Promise<AccountData> { + public async getAccountData(accountToken: AccountToken): Promise<IAccountData> { let response; try { - response = await this._transport.send('get_account_data', accountToken, NETWORK_CALL_TIMEOUT); + response = await this.transport.send('get_account_data', accountToken, NETWORK_CALL_TIMEOUT); } catch (error) { if (error instanceof JsonRpcRemoteError) { switch (error.code) { @@ -324,112 +319,114 @@ export class DaemonRpc { } try { - return validate(AccountDataSchema, response); + return validate(accountDataSchema, response); } catch (error) { throw new ResponseParseError('Invalid response from get_account_data', error); } } - async getRelayLocations(): Promise<RelayList> { - const response = await this._transport.send('get_relay_locations'); + public async getRelayLocations(): Promise<IRelayList> { + const response = await this.transport.send('get_relay_locations'); try { - return camelCaseObjectKeys(validate(RelayListSchema, response)) as RelayList; + return camelCaseObjectKeys(validate(relayListSchema, response)) as IRelayList; } catch (error) { throw new ResponseParseError('Invalid response from get_relay_locations', error); } } - async setAccount(accountToken?: AccountToken): Promise<void> { - await this._transport.send('set_account', [accountToken]); + public async setAccount(accountToken?: AccountToken): Promise<void> { + await this.transport.send('set_account', [accountToken]); } - async updateRelaySettings(relaySettings: RelaySettingsUpdate): Promise<void> { - await this._transport.send('update_relay_settings', [underscoreObjectKeys(relaySettings)]); + public async updateRelaySettings(relaySettings: RelaySettingsUpdate): Promise<void> { + await this.transport.send('update_relay_settings', [underscoreObjectKeys(relaySettings)]); } - async setAllowLan(allowLan: boolean): Promise<void> { - await this._transport.send('set_allow_lan', [allowLan]); + public async setAllowLan(allowLan: boolean): Promise<void> { + await this.transport.send('set_allow_lan', [allowLan]); } - async setEnableIpv6(enableIpv6: boolean): Promise<void> { - await this._transport.send('set_enable_ipv6', [enableIpv6]); + public async setEnableIpv6(enableIpv6: boolean): Promise<void> { + await this.transport.send('set_enable_ipv6', [enableIpv6]); } - async setBlockWhenDisconnected(blockWhenDisconnected: boolean): Promise<void> { - await this._transport.send('set_block_when_disconnected', [blockWhenDisconnected]); + public async setBlockWhenDisconnected(blockWhenDisconnected: boolean): Promise<void> { + await this.transport.send('set_block_when_disconnected', [blockWhenDisconnected]); } - async setOpenVpnMssfix(mssfix?: number): Promise<void> { - await this._transport.send('set_openvpn_mssfix', [mssfix]); + public async setOpenVpnMssfix(mssfix?: number): Promise<void> { + await this.transport.send('set_openvpn_mssfix', [mssfix]); } - async setAutoConnect(autoConnect: boolean): Promise<void> { - await this._transport.send('set_auto_connect', [autoConnect]); + public async setAutoConnect(autoConnect: boolean): Promise<void> { + await this.transport.send('set_auto_connect', [autoConnect]); } - async connectTunnel(): Promise<void> { - await this._transport.send('connect'); + public async connectTunnel(): Promise<void> { + await this.transport.send('connect'); } - async disconnectTunnel(): Promise<void> { - await this._transport.send('disconnect'); + public async disconnectTunnel(): Promise<void> { + await this.transport.send('disconnect'); } - async getLocation(): Promise<Location | undefined> { - const response = await this._transport.send('get_current_location', [], NETWORK_CALL_TIMEOUT); + public async getLocation(): Promise<ILocation | undefined> { + const response = await this.transport.send('get_current_location', [], NETWORK_CALL_TIMEOUT); try { - return camelCaseObjectKeys(validate(LocationSchema, response)) as Location; + return camelCaseObjectKeys(validate(locationSchema, response)) as ILocation; } catch (error) { throw new ResponseParseError('Invalid response from get_current_location', error); } } - async getState(): Promise<TunnelStateTransition> { - const response = await this._transport.send('get_state'); + public async getState(): Promise<TunnelStateTransition> { + const response = await this.transport.send('get_state'); try { return camelCaseObjectKeys( - validate(TunnelStateTransitionSchema, response), + validate(tunnelStateTransitionSchema, response), ) as TunnelStateTransition; } catch (error) { throw new ResponseParseError('Invalid response from get_state', error); } } - async getSettings(): Promise<Settings> { - const response = await this._transport.send('get_settings'); + public async getSettings(): Promise<ISettings> { + const response = await this.transport.send('get_settings'); try { - return camelCaseObjectKeys(validate(SettingsSchema, response)) as Settings; + return camelCaseObjectKeys(validate(settingsSchema, response)) as ISettings; } catch (error) { throw new ResponseParseError('Invalid response from get_settings', error); } } - subscribeStateListener(listener: SubscriptionListener<TunnelStateTransition>): Promise<void> { - return this._transport.subscribe('new_state', (payload) => { + public subscribeStateListener( + listener: SubscriptionListener<TunnelStateTransition>, + ): Promise<void> { + return this.transport.subscribe('new_state', (payload) => { try { const newState = camelCaseObjectKeys( - validate(TunnelStateTransitionSchema, payload), + validate(tunnelStateTransitionSchema, payload), ) as TunnelStateTransition; - listener._onEvent(newState); + listener.onEvent(newState); } catch (error) { - listener._onError(new ResponseParseError('Invalid payload from new_state', error)); + listener.onError(new ResponseParseError('Invalid payload from new_state', error)); } }); } - subscribeSettingsListener(listener: SubscriptionListener<Settings>): Promise<void> { - return this._transport.subscribe('settings', (payload) => { + public subscribeSettingsListener(listener: SubscriptionListener<ISettings>): Promise<void> { + return this.transport.subscribe('settings', (payload) => { try { - const newSettings = camelCaseObjectKeys(validate(SettingsSchema, payload)) as Settings; - listener._onEvent(newSettings); + const newSettings = camelCaseObjectKeys(validate(settingsSchema, payload)) as ISettings; + listener.onEvent(newSettings); } catch (error) { - listener._onError(new ResponseParseError('Invalid payload from settings', error)); + listener.onError(new ResponseParseError('Invalid payload from settings', error)); } }); } - async getAccountHistory(): Promise<Array<AccountToken>> { - const response = await this._transport.send('get_account_history'); + public async getAccountHistory(): Promise<AccountToken[]> { + const response = await this.transport.send('get_account_history'); try { return validate(arrayOf(string), response); } catch (error) { @@ -437,12 +434,12 @@ export class DaemonRpc { } } - async removeAccountFromHistory(accountToken: AccountToken): Promise<void> { - await this._transport.send('remove_account_from_history', accountToken); + public async removeAccountFromHistory(accountToken: AccountToken): Promise<void> { + await this.transport.send('remove_account_from_history', accountToken); } - async getCurrentVersion(): Promise<string> { - const response = await this._transport.send('get_current_version'); + public async getCurrentVersion(): Promise<string> { + const response = await this.transport.send('get_current_version'); try { return validate(string, response); } catch (error) { @@ -450,10 +447,10 @@ export class DaemonRpc { } } - async getVersionInfo(): Promise<AppVersionInfo> { - const response = await this._transport.send('get_version_info', [], NETWORK_CALL_TIMEOUT); + public async getVersionInfo(): Promise<IAppVersionInfo> { + const response = await this.transport.send('get_version_info', [], NETWORK_CALL_TIMEOUT); try { - return camelCaseObjectKeys(validate(AppVersionInfoSchema, response)) as AppVersionInfo; + return camelCaseObjectKeys(validate(appVersionInfoSchema, response)) as IAppVersionInfo; } catch (error) { throw new ResponseParseError('Invalid response from get_version_info'); } @@ -470,30 +467,30 @@ function camelCaseToUnderscore(str: string): string { .toLowerCase(); } -function camelCaseObjectKeys(object: { [key: string]: any }) { - return transformObjectKeys(object, underscoreToCamelCase); +function camelCaseObjectKeys(anObject: { [key: string]: any }) { + return transformObjectKeys(anObject, underscoreToCamelCase); } -function underscoreObjectKeys(object: { [key: string]: any }) { - return transformObjectKeys(object, camelCaseToUnderscore); +function underscoreObjectKeys(anObject: { [key: string]: any }) { + return transformObjectKeys(anObject, camelCaseToUnderscore); } function transformObjectKeys( - object: { [key: string]: any }, + anObject: { [key: string]: any }, keyTransformer: (key: string) => string, ) { - for (const sourceKey of Object.keys(object)) { + for (const sourceKey of Object.keys(anObject)) { const targetKey = keyTransformer(sourceKey); - const sourceValue = object[sourceKey]; + const sourceValue = anObject[sourceKey]; - object[targetKey] = + anObject[targetKey] = sourceValue !== null && typeof sourceValue === 'object' ? transformObjectKeys(sourceValue, keyTransformer) : sourceValue; if (sourceKey !== targetKey) { - delete object[sourceKey]; + delete anObject[sourceKey]; } } - return object; + return anObject; } diff --git a/gui/packages/desktop/src/main/gui-settings.ts b/gui/packages/desktop/src/main/gui-settings.ts index 1b68b5e13c..57c034b162 100644 --- a/gui/packages/desktop/src/main/gui-settings.ts +++ b/gui/packages/desktop/src/main/gui-settings.ts @@ -1,79 +1,79 @@ -import * as fs from 'fs'; -import * as path from 'path'; import { app } from 'electron'; import log from 'electron-log'; +import * as fs from 'fs'; +import * as path from 'path'; -import { GuiSettingsState } from '../shared/gui-settings-state'; +import { IGuiSettingsState } from '../shared/gui-settings-state'; export default class GuiSettings { - _state: GuiSettingsState = { + get state(): IGuiSettingsState { + return this.stateValue; + } + + set autoConnect(newValue: boolean) { + this.changeStateAndNotify({ ...this.stateValue, autoConnect: newValue }); + } + + get autoConnect(): boolean { + return this.stateValue.autoConnect; + } + + set monochromaticIcon(newValue: boolean) { + this.changeStateAndNotify({ ...this.stateValue, monochromaticIcon: newValue }); + } + + get monochromaticIcon(): boolean { + return this.stateValue.monochromaticIcon; + } + + set startMinimized(newValue: boolean) { + this.changeStateAndNotify({ ...this.stateValue, startMinimized: newValue }); + } + + get startMinimized(): boolean { + return this.stateValue.startMinimized; + } + + public onChange?: (newState: IGuiSettingsState, oldState: IGuiSettingsState) => void; + + private stateValue: IGuiSettingsState = { autoConnect: true, monochromaticIcon: false, startMinimized: false, }; - onChange?: (newState: GuiSettingsState, oldState: GuiSettingsState) => void; - - load() { + public load() { try { - const settingsFile = this._filePath(); + const settingsFile = this.filePath(); const contents = fs.readFileSync(settingsFile, 'utf8'); const settings = JSON.parse(contents); - this._state.autoConnect = + this.stateValue.autoConnect = typeof settings.autoConnect === 'boolean' ? settings.autoConnect : true; - this._state.monochromaticIcon = settings.monochromaticIcon || false; - this._state.startMinimized = settings.startMinimized || false; + this.stateValue.monochromaticIcon = settings.monochromaticIcon || false; + this.stateValue.startMinimized = settings.startMinimized || false; } catch (error) { log.error(`Failed to read GUI settings file: ${error}`); } } - store() { + public store() { try { - const settingsFile = this._filePath(); + const settingsFile = this.filePath(); - fs.writeFileSync(settingsFile, JSON.stringify(this._state)); + fs.writeFileSync(settingsFile, JSON.stringify(this.stateValue)); } catch (error) { log.error(`Failed to write GUI settings file: ${error}`); } } - get state(): GuiSettingsState { - return this._state; - } - - set autoConnect(newValue: boolean) { - this._changeStateAndNotify({ ...this._state, autoConnect: newValue }); - } - - get autoConnect(): boolean { - return this._state.autoConnect; - } - - set monochromaticIcon(newValue: boolean) { - this._changeStateAndNotify({ ...this._state, monochromaticIcon: newValue }); - } - - get monochromaticIcon(): boolean { - return this._state.monochromaticIcon; - } - - set startMinimized(newValue: boolean) { - this._changeStateAndNotify({ ...this._state, startMinimized: newValue }); - } - - get startMinimized(): boolean { - return this._state.startMinimized; - } - - _filePath() { + private filePath() { return path.join(app.getPath('userData'), 'gui_settings.json'); } - _changeStateAndNotify(newState: GuiSettingsState) { - const oldState = this._state; - this._state = newState; + private changeStateAndNotify(newState: IGuiSettingsState) { + const oldState = this.stateValue; + this.stateValue = newState; this.store(); diff --git a/gui/packages/desktop/src/main/index.ts b/gui/packages/desktop/src/main/index.ts index 70890f0a2a..173be30819 100644 --- a/gui/packages/desktop/src/main/index.ts +++ b/gui/packages/desktop/src/main/index.ts @@ -1,34 +1,28 @@ -import * as fs from 'fs'; -import log from 'electron-log'; -import * as path from 'path'; import { execFile } from 'child_process'; +import { app, BrowserWindow, ipcMain, Menu, nativeImage, screen, Tray } from 'electron'; +import log from 'electron-log'; +import * as fs from 'fs'; import mkdirp from 'mkdirp'; +import * as path from 'path'; import * as uuid from 'uuid'; -import { app, screen, BrowserWindow, ipcMain, Tray, Menu, nativeImage } from 'electron'; - -import { getOpenAtLogin, setOpenAtLogin } from './autostart'; -import NotificationController from './notification-controller'; -import WindowController from './window-controller'; - -import TrayIconController from './tray-icon-controller'; -import { TrayIconType } from './tray-icon-controller'; - -import { IpcMainEventChannel } from '../shared/ipc-event-channel'; - -import { DaemonRpc, ConnectionObserver, SubscriptionListener } from './daemon-rpc'; import { AccountToken, - AppVersionInfo, - Location, - RelayList, + IAppVersionInfo, + ILocation, + IRelayList, + ISettings, RelaySettingsUpdate, - Settings, TunnelStateTransition, } from '../shared/daemon-rpc-types'; - +import { IpcMainEventChannel } from '../shared/ipc-event-channel'; +import { getOpenAtLogin, setOpenAtLogin } from './autostart'; +import { ConnectionObserver, DaemonRpc, SubscriptionListener } from './daemon-rpc'; import GuiSettings from './gui-settings'; -import ReconnectionBackoff from './reconnection-backoff'; +import NotificationController from './notification-controller'; import { resolveBin } from './proc'; +import ReconnectionBackoff from './reconnection-backoff'; +import TrayIconController, { TrayIconType } from './tray-icon-controller'; +import WindowController from './window-controller'; const RELAY_LIST_UPDATE_INTERVAL = 60 * 60 * 1000; const VERSION_UPDATE_INTERVAL = 24 * 60 * 60 * 1000; @@ -36,34 +30,38 @@ const VERSION_UPDATE_INTERVAL = 24 * 60 * 60 * 1000; const DAEMON_RPC_PATH = process.platform === 'win32' ? '//./pipe/Mullvad VPN' : '/var/run/mullvad-vpn'; -type AppQuitStage = 'unready' | 'initiated' | 'ready'; +enum AppQuitStage { + unready, + initiated, + ready, +} -export type CurrentAppVersionInfo = { +export interface ICurrentAppVersionInfo { gui: string; daemon: string; isConsistent: boolean; -}; +} -export type AppUpgradeInfo = { +export interface IAppUpgradeInfo extends IAppVersionInfo { nextUpgrade?: string; upToDate: boolean; -} & AppVersionInfo; +} -const ApplicationMain = { - _notificationController: new NotificationController(), - _windowController: undefined as WindowController | undefined, - _trayIconController: undefined as TrayIconController | undefined, +class ApplicationMain { + private notificationController = new NotificationController(); + private windowController?: WindowController; + private trayIconController?: TrayIconController; - _daemonRpc: new DaemonRpc(), - _reconnectBackoff: new ReconnectionBackoff(), - _connectedToDaemon: false, + private daemonRpc = new DaemonRpc(); + private reconnectBackoff = new ReconnectionBackoff(); + private connectedToDaemon = false; - _logFilePath: '', - _oldLogFilePath: undefined as undefined | string, - _quitStage: 'unready' as AppQuitStage, + private logFilePath = ''; + private oldLogFilePath?: string; + private quitStage = AppQuitStage.unready; - _tunnelState: { state: 'disconnected' } as TunnelStateTransition, - _settings: { + private tunnelState: TunnelStateTransition = { state: 'disconnected' }; + private settings: ISettings = { accountToken: undefined, allowLan: false, autoConnect: false, @@ -87,21 +85,21 @@ const ApplicationMain = { fwmark: undefined, }, }, - } as Settings, - _guiSettings: new GuiSettings(), - _location: undefined as Location | undefined, - _lastDisconnectedLocation: undefined as Location | undefined, + }; + private guiSettings = new GuiSettings(); + private location?: ILocation; + private lastDisconnectedLocation?: ILocation; - _relays: { countries: [] } as RelayList, - _relaysInterval: undefined as NodeJS.Timeout | undefined, + private relays: IRelayList = { countries: [] }; + private relaysInterval?: NodeJS.Timeout; - _currentVersion: { + private currentVersion: ICurrentAppVersionInfo = { daemon: '', gui: '', isConsistent: true, - } as CurrentAppVersionInfo, + }; - _upgradeVersion: { + private upgradeVersion: IAppUpgradeInfo = { currentIsSupported: true, latest: { latestStable: '', @@ -109,22 +107,22 @@ const ApplicationMain = { }, nextUpgrade: undefined, upToDate: true, - } as AppUpgradeInfo, - _latestVersionInterval: undefined as NodeJS.Timeout | undefined, + }; + private latestVersionInterval?: NodeJS.Timeout; - run() { + public run() { // Since electron's GPU blacklists are broken, GPU acceleration won't work on older distros if (process.platform === 'linux') { app.commandLine.appendSwitch('--disable-gpu'); } - this._overrideAppPaths(); + this.overrideAppPaths(); - if (this._ensureSingleInstance()) { + if (this.ensureSingleInstance()) { return; } - this._initLogging(); + this.initLogging(); log.info(`Running version ${app.getVersion()}`); @@ -132,31 +130,31 @@ const ApplicationMain = { app.setAppUserModelId('net.mullvad.vpn'); } - this._guiSettings.load(); + this.guiSettings.load(); - app.on('activate', () => this._onActivate()); - app.on('ready', () => this._onReady()); + app.on('activate', () => this.onActivate()); + app.on('ready', () => this.onReady()); app.on('window-all-closed', () => app.quit()); - app.on('before-quit', (event: Event) => this._onBeforeQuit(event)); + app.on('before-quit', (event: Event) => this.onBeforeQuit(event)); const connectionObserver = new ConnectionObserver( () => { - this._onDaemonConnected(); + this.onDaemonConnected(); }, (error) => { - this._onDaemonDisconnected(error); + this.onDaemonDisconnected(error); }, ); - this._daemonRpc.addConnectionObserver(connectionObserver); - this._connectToDaemon(); - }, + this.daemonRpc.addConnectionObserver(connectionObserver); + this.connectToDaemon(); + } - _ensureSingleInstance() { + private ensureSingleInstance() { if (app.requestSingleInstanceLock()) { app.on('second-instance', (_event, _commandLine, _workingDirectory) => { - if (this._windowController) { - this._windowController.show(); + if (this.windowController) { + this.windowController.show(); } }); return false; @@ -164,9 +162,9 @@ const ApplicationMain = { app.quit(); return true; } - }, + } - _overrideAppPaths() { + private overrideAppPaths() { // This ensures that on Windows the %LOCALAPPDATA% directory is used instead of the %ADDDATA% // directory that has roaming contents if (process.platform === 'win32') { @@ -178,13 +176,13 @@ const ApplicationMain = { throw new Error('Missing %LOCALAPPDATA% environment variable'); } } - }, + } - _initLogging() { - const logDirectory = this._getLogsDirectory(); + private initLogging() { + const logDirectory = this.getLogsDirectory(); const format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}][{level}] {text}'; - this._logFilePath = path.join(logDirectory, 'frontend.log'); + this.logFilePath = path.join(logDirectory, 'frontend.log'); log.transports.console.format = format; log.transports.file.format = format; @@ -199,9 +197,9 @@ const ApplicationMain = { // Backup previous log file if it exists try { - fs.accessSync(this._logFilePath); - this._oldLogFilePath = path.join(logDirectory, 'frontend.old.log'); - fs.renameSync(this._logFilePath, this._oldLogFilePath); + fs.accessSync(this.logFilePath); + this.oldLogFilePath = path.join(logDirectory, 'frontend.old.log'); + fs.renameSync(this.logFilePath, this.oldLogFilePath); } catch (error) { // No previous log file exists } @@ -209,17 +207,17 @@ const ApplicationMain = { // Configure logging to file log.transports.console.level = 'debug'; log.transports.file.level = 'debug'; - log.transports.file.file = this._logFilePath; + log.transports.file.file = this.logFilePath; - log.debug(`Logging to ${this._logFilePath}`); + log.debug(`Logging to ${this.logFilePath}`); } - }, + } // Returns platform specific logs folder for application // See open issue and PR on Github: // 1. https://github.com/electron/electron/issues/10118 // 2. https://github.com/electron/electron/pull/10191 - _getLogsDirectory() { + private getLogsDirectory() { switch (process.platform) { case 'darwin': // macOS: ~/Library/Logs/{appname} @@ -229,43 +227,43 @@ const ApplicationMain = { // Linux: ~/.config/{appname}/logs return path.join(app.getPath('userData'), 'logs'); } - }, + } - _onActivate() { - if (this._windowController) { - this._windowController.show(); + private onActivate() { + if (this.windowController) { + this.windowController.show(); } - }, + } - async _onBeforeQuit(event: Event) { - switch (this._quitStage) { - case 'unready': + private async onBeforeQuit(event: Event) { + switch (this.quitStage) { + case AppQuitStage.unready: // postpone the app shutdown event.preventDefault(); - this._quitStage = 'initiated'; - await this._prepareToQuit(); + this.quitStage = AppQuitStage.initiated; + await this.prepareToQuit(); // terminate the app - this._quitStage = 'ready'; + this.quitStage = AppQuitStage.ready; app.quit(); break; - case 'initiated': + case AppQuitStage.initiated: // prevent immediate exit, the app will quit after running the shutdown routine event.preventDefault(); return; - case 'ready': + case AppQuitStage.ready: // let the app quit freely at this point break; } - }, + } - async _prepareToQuit() { - if (this._connectedToDaemon) { + private async prepareToQuit() { + if (this.connectedToDaemon) { try { - await this._daemonRpc.disconnectTunnel(); + await this.daemonRpc.disconnectTunnel(); log.info('Disconnected the tunnel'); } catch (e) { log.error(`Failed to disconnect the tunnel: ${e.message}`); @@ -273,163 +271,163 @@ const ApplicationMain = { } else { log.info('Cannot close the tunnel because there is no active connection to daemon.'); } - }, + } - async _onReady() { - const window = this._createWindow(); - const tray = this._createTray(); + private async onReady() { + const window = this.createWindow(); + const tray = this.createTray(); const windowController = new WindowController(window, tray); const trayIconController = new TrayIconController( tray, 'unsecured', - process.platform === 'darwin' && this._guiSettings.monochromaticIcon, + process.platform === 'darwin' && this.guiSettings.monochromaticIcon, ); - this._registerWindowListener(windowController); - this._registerIpcListeners(); - this._setAppMenu(); - this._addContextMenu(window); + this.registerWindowListener(windowController); + this.registerIpcListeners(); + this.setAppMenu(); + this.addContextMenu(window); - this._windowController = windowController; - this._trayIconController = trayIconController; + this.windowController = windowController; + this.trayIconController = trayIconController; - this._guiSettings.onChange = (newState, oldState) => { + this.guiSettings.onChange = (newState, oldState) => { if ( process.platform === 'darwin' && oldState.monochromaticIcon !== newState.monochromaticIcon ) { - if (this._trayIconController) { - this._trayIconController.useMonochromaticIcon = newState.monochromaticIcon; + if (this.trayIconController) { + this.trayIconController.useMonochromaticIcon = newState.monochromaticIcon; } } if (newState.autoConnect !== oldState.autoConnect) { - this._updateDaemonsAutoConnect(); + this.updateDaemonsAutoConnect(); } - if (this._windowController) { - IpcMainEventChannel.guiSettings.notify(this._windowController.webContents, newState); + if (this.windowController) { + IpcMainEventChannel.guiSettings.notify(this.windowController.webContents, newState); } }; if (process.env.NODE_ENV === 'development') { - await this._installDevTools(); + await this.installDevTools(); window.webContents.openDevTools({ mode: 'detach' }); } switch (process.platform) { case 'win32': - this._installWindowsMenubarAppWindowHandlers(tray, windowController); + this.installWindowsMenubarAppWindowHandlers(tray, windowController); break; case 'darwin': - this._installMacOsMenubarAppWindowHandlers(tray, windowController); + this.installMacOsMenubarAppWindowHandlers(tray, windowController); break; case 'linux': - this._installGenericMenubarAppWindowHandlers(tray, windowController); - this._installLinuxWindowCloseHandler(windowController); + this.installGenericMenubarAppWindowHandlers(tray, windowController); + this.installLinuxWindowCloseHandler(windowController); break; default: - this._installGenericMenubarAppWindowHandlers(tray, windowController); + this.installGenericMenubarAppWindowHandlers(tray, windowController); break; } - if (this._shouldShowWindowOnStart() || process.env.NODE_ENV === 'development') { + if (this.shouldShowWindowOnStart() || process.env.NODE_ENV === 'development') { windowController.show(); } window.loadFile(path.resolve(path.join(__dirname, '../renderer/index.html'))); - }, + } - async _onDaemonConnected() { - this._connectedToDaemon = true; + private async onDaemonConnected() { + this.connectedToDaemon = true; // subscribe to events try { - await this._subscribeEvents(); + await this.subscribeEvents(); } catch (error) { log.error(`Failed to subscribe: ${error.message}`); - return this._recoverFromBootstrapError(error); + return this.recoverFromBootstrapError(error); } // fetch the tunnel state try { - this._setTunnelState(await this._daemonRpc.getState()); + this.setTunnelState(await this.daemonRpc.getState()); } catch (error) { log.error(`Failed to fetch the tunnel state: ${error.message}`); - return this._recoverFromBootstrapError(error); + return this.recoverFromBootstrapError(error); } // fetch settings try { - this._setSettings(await this._daemonRpc.getSettings()); + this.setSettings(await this.daemonRpc.getSettings()); } catch (error) { log.error(`Failed to fetch settings: ${error.message}`); - return this._recoverFromBootstrapError(error); + return this.recoverFromBootstrapError(error); } // fetch relays try { - this._setRelays(await this._daemonRpc.getRelayLocations()); + this.setRelays(await this.daemonRpc.getRelayLocations()); } catch (error) { log.error(`Failed to fetch relay locations: ${error.message}`); - return this._recoverFromBootstrapError(error); + return this.recoverFromBootstrapError(error); } // fetch the daemon's version try { - this._setDaemonVersion(await this._daemonRpc.getCurrentVersion()); + this.setDaemonVersion(await this.daemonRpc.getCurrentVersion()); } catch (error) { log.error(`Failed to fetch the daemon's version: ${error.message}`); - return this._recoverFromBootstrapError(error); + return this.recoverFromBootstrapError(error); } // fetch the latest version info in background - this._fetchLatestVersion(); + this.fetchLatestVersion(); // start periodic updates - this._startRelaysPeriodicUpdates(); - this._startLatestVersionPeriodicUpdates(); + this.startRelaysPeriodicUpdates(); + this.startLatestVersionPeriodicUpdates(); // notify user about inconsistent version if ( process.env.NODE_ENV !== 'development' && - !this._shouldSuppressNotifications() && - !this._currentVersion.isConsistent + !this.shouldSuppressNotifications() && + !this.currentVersion.isConsistent ) { - this._notificationController.notifyInconsistentVersion(); + this.notificationController.notifyInconsistentVersion(); } // reset the reconnect backoff when connection established. - this._reconnectBackoff.reset(); + this.reconnectBackoff.reset(); // notify renderer - if (this._windowController) { - IpcMainEventChannel.daemonConnected.notify(this._windowController.webContents); + if (this.windowController) { + IpcMainEventChannel.daemonConnected.notify(this.windowController.webContents); } - }, + } - _onDaemonDisconnected(error?: Error) { + private onDaemonDisconnected(error?: Error) { // make sure we were connected before to distinguish between a failed attempt to reconnect and // connection loss. - const wasConnected = this._connectedToDaemon; + const wasConnected = this.connectedToDaemon; if (wasConnected) { - this._connectedToDaemon = false; + this.connectedToDaemon = false; // stop periodic updates - this._stopRelaysPeriodicUpdates(); - this._stopLatestVersionPeriodicUpdates(); + this.stopRelaysPeriodicUpdates(); + this.stopLatestVersionPeriodicUpdates(); // notify renderer process - if (this._windowController) { + if (this.windowController) { IpcMainEventChannel.daemonDisconnected.notify( - this._windowController.webContents, + this.windowController.webContents, error ? error.message : undefined, ); } @@ -443,34 +441,34 @@ const ApplicationMain = { log.error(`Failed to connect to daemon: ${error.message}`); } - this._reconnectToDaemon(); + this.reconnectToDaemon(); } else { log.info('Disconnected from the daemon'); } - }, + } - _connectToDaemon() { - this._daemonRpc.connect({ path: DAEMON_RPC_PATH }); - }, + private connectToDaemon() { + this.daemonRpc.connect({ path: DAEMON_RPC_PATH }); + } - _reconnectToDaemon() { - this._reconnectBackoff.attempt(() => { - this._connectToDaemon(); + private reconnectToDaemon() { + this.reconnectBackoff.attempt(() => { + this.connectToDaemon(); }); - }, + } - _recoverFromBootstrapError(_error?: Error) { + private recoverFromBootstrapError(_error?: Error) { // Attempt to reconnect to daemon if the program fails to fetch settings, tunnel state or // subscribe for RPC events. - this._daemonRpc.disconnect(); + this.daemonRpc.disconnect(); - this._reconnectToDaemon(); - }, + this.reconnectToDaemon(); + } - async _subscribeEvents(): Promise<void> { + private async subscribeEvents(): Promise<void> { const stateListener = new SubscriptionListener( (newState: TunnelStateTransition) => { - this._setTunnelState(newState); + this.setTunnelState(newState); }, (error: Error) => { log.error(`Cannot deserialize the new state: ${error.message}`); @@ -478,8 +476,8 @@ const ApplicationMain = { ); const settingsListener = new SubscriptionListener( - (newSettings: Settings) => { - this._setSettings(newSettings); + (newSettings: ISettings) => { + this.setSettings(newSettings); }, (error: Error) => { log.error(`Cannot deserialize the new settings: ${error.message}`); @@ -487,74 +485,74 @@ const ApplicationMain = { ); await Promise.all([ - this._daemonRpc.subscribeStateListener(stateListener), - this._daemonRpc.subscribeSettingsListener(settingsListener), + this.daemonRpc.subscribeStateListener(stateListener), + this.daemonRpc.subscribeSettingsListener(settingsListener), ]); - }, + } - _setTunnelState(newState: TunnelStateTransition) { - this._tunnelState = newState; - this._updateTrayIcon(newState, this._settings.blockWhenDisconnected); - this._updateLocation(); + private setTunnelState(newState: TunnelStateTransition) { + this.tunnelState = newState; + this.updateTrayIcon(newState, this.settings.blockWhenDisconnected); + this.updateLocation(); - if (!this._shouldSuppressNotifications()) { - this._notificationController.notifyTunnelState(newState); + if (!this.shouldSuppressNotifications()) { + this.notificationController.notifyTunnelState(newState); } - if (this._windowController) { - IpcMainEventChannel.tunnel.notify(this._windowController.webContents, newState); + if (this.windowController) { + IpcMainEventChannel.tunnel.notify(this.windowController.webContents, newState); } - }, + } - _setSettings(newSettings: Settings) { - this._settings = newSettings; - this._updateTrayIcon(this._tunnelState, newSettings.blockWhenDisconnected); + private setSettings(newSettings: ISettings) { + this.settings = newSettings; + this.updateTrayIcon(this.tunnelState, newSettings.blockWhenDisconnected); - if (this._windowController) { - IpcMainEventChannel.settings.notify(this._windowController.webContents, newSettings); + if (this.windowController) { + IpcMainEventChannel.settings.notify(this.windowController.webContents, newSettings); } - }, + } - _setLocation(newLocation: Location) { - this._location = newLocation; + private setLocation(newLocation: ILocation) { + this.location = newLocation; - if (this._windowController) { - IpcMainEventChannel.location.notify(this._windowController.webContents, newLocation); + if (this.windowController) { + IpcMainEventChannel.location.notify(this.windowController.webContents, newLocation); } - }, + } - _setRelays(newRelayList: RelayList) { - this._relays = newRelayList; + private setRelays(newRelayList: IRelayList) { + this.relays = newRelayList; - if (this._windowController) { - IpcMainEventChannel.relays.notify(this._windowController.webContents, newRelayList); + if (this.windowController) { + IpcMainEventChannel.relays.notify(this.windowController.webContents, newRelayList); } - }, + } - _startRelaysPeriodicUpdates() { + private startRelaysPeriodicUpdates() { log.debug('Start relays periodic updates'); const handler = async () => { try { - this._setRelays(await this._daemonRpc.getRelayLocations()); + this.setRelays(await this.daemonRpc.getRelayLocations()); } catch (error) { log.error(`Failed to fetch relay locations: ${error.message}`); } }; - this._relaysInterval = setInterval(handler, RELAY_LIST_UPDATE_INTERVAL); - }, + this.relaysInterval = setInterval(handler, RELAY_LIST_UPDATE_INTERVAL); + } - _stopRelaysPeriodicUpdates() { - if (this._relaysInterval) { - clearInterval(this._relaysInterval); - this._relaysInterval = undefined; + private stopRelaysPeriodicUpdates() { + if (this.relaysInterval) { + clearInterval(this.relaysInterval); + this.relaysInterval = undefined; log.debug('Stop relays periodic updates'); } - }, + } - _setDaemonVersion(daemonVersion: string) { + private setDaemonVersion(daemonVersion: string) { const guiVersion = app.getVersion().replace('.0', ''); const versionInfo = { daemon: daemonVersion, @@ -562,15 +560,15 @@ const ApplicationMain = { isConsistent: daemonVersion === guiVersion, }; - this._currentVersion = versionInfo; + this.currentVersion = versionInfo; // notify renderer - if (this._windowController) { - IpcMainEventChannel.currentVersion.notify(this._windowController.webContents, versionInfo); + if (this.windowController) { + IpcMainEventChannel.currentVersion.notify(this.windowController.webContents, versionInfo); } - }, + } - _setLatestVersion(latestVersionInfo: AppVersionInfo) { + private setLatestVersion(latestVersionInfo: IAppVersionInfo) { function isBeta(version: string) { return version.includes('-'); } @@ -597,7 +595,7 @@ const ApplicationMain = { } } - const currentVersionInfo = this._currentVersion; + const currentVersionInfo = this.currentVersion; const latestVersion = latestVersionInfo.latest.latest; const latestStableVersion = latestVersionInfo.latest.latestStable; @@ -616,64 +614,64 @@ const ApplicationMain = { upToDate: isUpToDate, }; - this._upgradeVersion = upgradeInfo; + this.upgradeVersion = upgradeInfo; // notify user to update the app if it became unsupported if ( process.env.NODE_ENV !== 'development' && - !this._shouldSuppressNotifications() && + !this.shouldSuppressNotifications() && currentVersionInfo.isConsistent && !latestVersionInfo.currentIsSupported && upgradeVersion ) { - this._notificationController.notifyUnsupportedVersion(upgradeVersion); + this.notificationController.notifyUnsupportedVersion(upgradeVersion); } - if (this._windowController) { - IpcMainEventChannel.upgradeVersion.notify(this._windowController.webContents, upgradeInfo); + if (this.windowController) { + IpcMainEventChannel.upgradeVersion.notify(this.windowController.webContents, upgradeInfo); } - }, + } - async _fetchLatestVersion() { + private async fetchLatestVersion() { try { - this._setLatestVersion(await this._daemonRpc.getVersionInfo()); + this.setLatestVersion(await this.daemonRpc.getVersionInfo()); } catch (error) { - console.error(`Failed to request the version info: ${error.message}`); + log.error(`Failed to request the version info: ${error.message}`); } - }, + } - _startLatestVersionPeriodicUpdates() { + private startLatestVersionPeriodicUpdates() { const handler = () => { - this._fetchLatestVersion(); + this.fetchLatestVersion(); }; - this._latestVersionInterval = setInterval(handler, VERSION_UPDATE_INTERVAL); - }, + this.latestVersionInterval = setInterval(handler, VERSION_UPDATE_INTERVAL); + } - _stopLatestVersionPeriodicUpdates() { - if (this._latestVersionInterval) { - clearInterval(this._latestVersionInterval); + private stopLatestVersionPeriodicUpdates() { + if (this.latestVersionInterval) { + clearInterval(this.latestVersionInterval); - this._latestVersionInterval = undefined; + this.latestVersionInterval = undefined; } - }, + } - _shouldSuppressNotifications(): boolean { - return this._windowController ? this._windowController.isVisible() : false; - }, + private shouldSuppressNotifications(): boolean { + return this.windowController ? this.windowController.isVisible() : false; + } - async _updateLocation() { - const state = this._tunnelState.state; + private async updateLocation() { + const state = this.tunnelState.state; if (state === 'connected' || state === 'disconnected' || state === 'connecting') { try { // It may take some time to fetch the new user location. // So take the user to the last known location when disconnected. - if (state === 'disconnected' && this._lastDisconnectedLocation) { - this._setLocation(this._lastDisconnectedLocation); + if (state === 'disconnected' && this.lastDisconnectedLocation) { + this.setLocation(this.lastDisconnectedLocation); } // Fetch the new user location - const location = await this._daemonRpc.getLocation(); + const location = await this.daemonRpc.getLocation(); // If the location is currently unavailable, do nothing! This only ever // happens when a custom relay is set or we are in a blocked state. if (!location) { @@ -683,22 +681,25 @@ const ApplicationMain = { // Cache the user location // Note: hostname is only set for relay servers. if (location.hostname === null) { - this._lastDisconnectedLocation = location; + this.lastDisconnectedLocation = location; } // Broadcast the new location. // There is a chance that the location is not stale if the tunnel state before the location // request is the same as after receiving the response. - if (this._tunnelState.state === state) { - this._setLocation(location); + if (this.tunnelState.state === state) { + this.setLocation(location); } } catch (error) { log.error(`Failed to update the location: ${error.message}`); } } - }, + } - _trayIconType(tunnelState: TunnelStateTransition, blockWhenDisconnected: boolean): TrayIconType { + private trayIconType( + tunnelState: TunnelStateTransition, + blockWhenDisconnected: boolean, + ): TrayIconType { switch (tunnelState.state) { case 'connected': return 'secured'; @@ -724,130 +725,127 @@ const ApplicationMain = { return 'unsecured'; } } - }, + } - _updateTrayIcon(tunnelState: TunnelStateTransition, blockWhenDisconnected: boolean) { - const type = this._trayIconType(tunnelState, blockWhenDisconnected); + private updateTrayIcon(tunnelState: TunnelStateTransition, blockWhenDisconnected: boolean) { + const type = this.trayIconType(tunnelState, blockWhenDisconnected); - if (this._trayIconController) { - this._trayIconController.animateToIcon(type); + if (this.trayIconController) { + this.trayIconController.animateToIcon(type); } - }, + } - _registerWindowListener(windowController: WindowController) { + private registerWindowListener(windowController: WindowController) { windowController.window.on('show', () => { // cancel notifications when window appears - this._notificationController.cancelPendingNotifications(); + this.notificationController.cancelPendingNotifications(); windowController.send('window-shown'); }); - }, + } - _registerIpcListeners() { + private registerIpcListeners() { IpcMainEventChannel.state.handleGet(() => ({ - isConnected: this._connectedToDaemon, + isConnected: this.connectedToDaemon, autoStart: getOpenAtLogin(), - tunnelState: this._tunnelState, - settings: this._settings, - location: this._location, - relays: this._relays, - currentVersion: this._currentVersion, - upgradeVersion: this._upgradeVersion, - guiSettings: this._guiSettings.state, + tunnelState: this.tunnelState, + settings: this.settings, + location: this.location, + relays: this.relays, + currentVersion: this.currentVersion, + upgradeVersion: this.upgradeVersion, + guiSettings: this.guiSettings.state, })); IpcMainEventChannel.settings.handleAllowLan((allowLan: boolean) => - this._daemonRpc.setAllowLan(allowLan), + this.daemonRpc.setAllowLan(allowLan), ); IpcMainEventChannel.settings.handleEnableIpv6((enableIpv6: boolean) => - this._daemonRpc.setEnableIpv6(enableIpv6), + this.daemonRpc.setEnableIpv6(enableIpv6), ); IpcMainEventChannel.settings.handleBlockWhenDisconnected((blockWhenDisconnected: boolean) => - this._daemonRpc.setBlockWhenDisconnected(blockWhenDisconnected), + this.daemonRpc.setBlockWhenDisconnected(blockWhenDisconnected), ); IpcMainEventChannel.settings.handleOpenVpnMssfix((mssfix?: number) => - this._daemonRpc.setOpenVpnMssfix(mssfix), + this.daemonRpc.setOpenVpnMssfix(mssfix), ); IpcMainEventChannel.settings.handleUpdateRelaySettings((update: RelaySettingsUpdate) => - this._daemonRpc.updateRelaySettings(update), + this.daemonRpc.updateRelaySettings(update), ); IpcMainEventChannel.autoStart.handleSet((autoStart: boolean) => { - return this._setAutoStart(autoStart); + return this.setAutoStart(autoStart); }); - IpcMainEventChannel.tunnel.handleConnect(() => this._daemonRpc.connectTunnel()); - IpcMainEventChannel.tunnel.handleDisconnect(() => this._daemonRpc.disconnectTunnel()); + IpcMainEventChannel.tunnel.handleConnect(() => this.daemonRpc.connectTunnel()); + IpcMainEventChannel.tunnel.handleDisconnect(() => this.daemonRpc.disconnectTunnel()); IpcMainEventChannel.guiSettings.handleAutoConnect((autoConnect: boolean) => { - this._guiSettings.autoConnect = autoConnect; + this.guiSettings.autoConnect = autoConnect; }); IpcMainEventChannel.guiSettings.handleStartMinimized((startMinimized: boolean) => { - this._guiSettings.startMinimized = startMinimized; + this.guiSettings.startMinimized = startMinimized; }); IpcMainEventChannel.guiSettings.handleMonochromaticIcon((monochromaticIcon: boolean) => { - this._guiSettings.monochromaticIcon = monochromaticIcon; + this.guiSettings.monochromaticIcon = monochromaticIcon; }); IpcMainEventChannel.account.handleSet((token: AccountToken) => - this._daemonRpc.setAccount(token), + this.daemonRpc.setAccount(token), ); - IpcMainEventChannel.account.handleUnset(() => this._daemonRpc.setAccount()); + IpcMainEventChannel.account.handleUnset(() => this.daemonRpc.setAccount()); IpcMainEventChannel.account.handleGetData((token: AccountToken) => - this._daemonRpc.getAccountData(token), + this.daemonRpc.getAccountData(token), ); - IpcMainEventChannel.accountHistory.handleGet(() => this._daemonRpc.getAccountHistory()); + IpcMainEventChannel.accountHistory.handleGet(() => this.daemonRpc.getAccountHistory()); IpcMainEventChannel.accountHistory.handleRemoveItem((token: AccountToken) => - this._daemonRpc.removeAccountFromHistory(token), + this.daemonRpc.removeAccountFromHistory(token), ); ipcMain.on('show-window', () => { - const windowController = this._windowController; + const windowController = this.windowController; if (windowController) { windowController.show(); } }); - ipcMain.on( - 'collect-logs', - (event: Electron.Event, requestId: string, toRedact: Array<string>) => { - const reportPath = path.join(app.getPath('temp'), uuid.v4() + '.log'); - const executable = resolveBin('problem-report'); - const args = ['collect', '--output', reportPath]; - if (toRedact.length > 0) { - args.push('--redact', ...toRedact, '--'); - } - args.push(this._logFilePath); - if (this._oldLogFilePath) { - args.push(this._oldLogFilePath); - } + ipcMain.on('collect-logs', (event: Electron.Event, requestId: string, toRedact: string[]) => { + const reportPath = path.join(app.getPath('temp'), uuid.v4() + '.log'); + const executable = resolveBin('problem-report'); + const args = ['collect', '--output', reportPath]; + if (toRedact.length > 0) { + args.push('--redact', ...toRedact, '--'); + } + args.push(this.logFilePath); + if (this.oldLogFilePath) { + args.push(this.oldLogFilePath); + } - execFile(executable, args, { windowsHide: true }, (error, stdout, stderr) => { - if (error) { - log.error( - `Failed to collect a problem report: ${error.message} + execFile(executable, args, { windowsHide: true }, (error, stdout, stderr) => { + if (error) { + log.error( + `Failed to collect a problem report: ${error.message} Stdout: ${stdout.toString()} Stderr: ${stderr.toString()}`, - ); + ); - event.sender.send('collect-logs-reply', requestId, { - success: false, - error: error.message, - }); - } else { - log.debug(`Problem report was written to ${reportPath}`); + event.sender.send('collect-logs-reply', requestId, { + success: false, + error: error.message, + }); + } else { + log.debug(`Problem report was written to ${reportPath}`); - event.sender.send('collect-logs-reply', requestId, { - success: true, - reportPath, - }); - } - }); - }, - ); + event.sender.send('collect-logs-reply', requestId, { + success: true, + reportPath, + }); + } + }); + }); ipcMain.on( 'send-problem-report', @@ -883,33 +881,33 @@ const ApplicationMain = { }); }, ); - }, + } - _updateDaemonsAutoConnect() { - const daemonAutoConnect = this._guiSettings.autoConnect && getOpenAtLogin(); - if (daemonAutoConnect !== this._settings.autoConnect) { - this._daemonRpc.setAutoConnect(daemonAutoConnect); + private updateDaemonsAutoConnect() { + const daemonAutoConnect = this.guiSettings.autoConnect && getOpenAtLogin(); + if (daemonAutoConnect !== this.settings.autoConnect) { + this.daemonRpc.setAutoConnect(daemonAutoConnect); } - }, + } - async _setAutoStart(autoStart: boolean): Promise<void> { + private async setAutoStart(autoStart: boolean): Promise<void> { try { await setOpenAtLogin(autoStart); - if (this._windowController) { - IpcMainEventChannel.autoStart.notify(this._windowController.webContents, autoStart); + if (this.windowController) { + IpcMainEventChannel.autoStart.notify(this.windowController.webContents, autoStart); } - this._updateDaemonsAutoConnect(); + this.updateDaemonsAutoConnect(); } catch (error) { log.error( `Failed to update the autostart to ${autoStart.toString()}. ${error.message.toString()}`, ); } return Promise.resolve(); - }, + } - async _installDevTools() { + private async installDevTools() { const installer = require('electron-devtools-installer'); const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS']; const forceDownload = !!process.env.UPGRADE_EXTENSIONS; @@ -920,9 +918,9 @@ const ApplicationMain = { log.info(`Error installing ${name} extension: ${e.message}`); } } - }, + } - _createWindow(): BrowserWindow { + private createWindow(): BrowserWindow { const contentHeight = 568; // the size of transparent area around arrow on macOS @@ -967,9 +965,9 @@ const ApplicationMain = { default: return new BrowserWindow(options); } - }, + } - _setAppMenu() { + private setAppMenu() { const template: Electron.MenuItemConstructorOptions[] = [ { label: 'Mullvad', @@ -987,9 +985,9 @@ const ApplicationMain = { }, ]; Menu.setApplicationMenu(Menu.buildFromTemplate(template)); - }, + } - _addContextMenu(window: BrowserWindow) { + private addContextMenu(window: BrowserWindow) { const menuTemplate: Electron.MenuItemConstructorOptions[] = [ { role: 'cut' }, { role: 'copy' }, @@ -1031,9 +1029,9 @@ const ApplicationMain = { } }, ); - }, + } - _createTray(): Tray { + private createTray(): Tray { const tray = new Tray(nativeImage.createEmpty()); tray.setToolTip('Mullvad VPN'); @@ -1046,9 +1044,9 @@ const ApplicationMain = { } return tray; - }, + } - _installWindowsMenubarAppWindowHandlers(tray: Tray, windowController: WindowController) { + private installWindowsMenubarAppWindowHandlers(tray: Tray, windowController: WindowController) { tray.on('click', () => windowController.toggle()); tray.on('right-click', () => windowController.hide()); @@ -1065,49 +1063,51 @@ const ApplicationMain = { windowController.hide(); } }); - }, + } // setup NSEvent monitor to fix inconsistent window.blur on macOS // see https://github.com/electron/electron/issues/8689 - _installMacOsMenubarAppWindowHandlers(tray: Tray, windowController: WindowController) { + private installMacOsMenubarAppWindowHandlers(tray: Tray, windowController: WindowController) { // $FlowFixMe: this module is only available on macOS const { NSEventMonitor, NSEventMask } = require('nseventmonitor'); const macEventMonitor = new NSEventMonitor(); + // tslint:disable-next-line const eventMask = NSEventMask.leftMouseDown | NSEventMask.rightMouseDown; const window = windowController.window; window.on('show', () => macEventMonitor.start(eventMask, () => windowController.hide())); window.on('hide', () => macEventMonitor.stop()); tray.on('click', () => windowController.toggle()); - }, + } - _installGenericMenubarAppWindowHandlers(tray: Tray, windowController: WindowController) { + private installGenericMenubarAppWindowHandlers(tray: Tray, windowController: WindowController) { tray.on('click', () => { windowController.toggle(); }); - }, + } - _installLinuxWindowCloseHandler(windowController: WindowController) { + private installLinuxWindowCloseHandler(windowController: WindowController) { windowController.window.on('close', (closeEvent: Event) => { - if (process.platform === 'linux' && this._quitStage !== 'ready') { + if (process.platform === 'linux' && this.quitStage !== AppQuitStage.ready) { closeEvent.preventDefault(); windowController.hide(); } }); - }, + } - _shouldShowWindowOnStart(): boolean { + private shouldShowWindowOnStart(): boolean { switch (process.platform) { case 'win32': return false; case 'darwin': return false; case 'linux': - return !this._guiSettings.startMinimized; + return !this.guiSettings.startMinimized; default: return true; } - }, -}; + } +} -ApplicationMain.run(); +const applicationMain = new ApplicationMain(); +applicationMain.run(); diff --git a/gui/packages/desktop/src/main/jsonrpc-client.ts b/gui/packages/desktop/src/main/jsonrpc-client.ts index 811c151fb5..2faf473c3e 100644 --- a/gui/packages/desktop/src/main/jsonrpc-client.ts +++ b/gui/packages/desktop/src/main/jsonrpc-client.ts @@ -1,19 +1,19 @@ import assert from 'assert'; -import { EventEmitter } from 'events'; import log from 'electron-log'; +import { EventEmitter } from 'events'; import jsonrpc from 'jsonrpc-lite'; -import * as uuid from 'uuid'; -import * as net from 'net'; import JSONStream from 'JSONStream'; +import * as net from 'net'; +import * as uuid from 'uuid'; -export type UnansweredRequest = { +export interface IUnansweredRequest { resolve: (value: any) => void; reject: (value: any) => void; timerId: NodeJS.Timeout; - message: Object; -}; + message: object; +} -export type JsonRpcErrorResponse = { +export interface IJsonRpcErrorResponse { type: 'error'; payload: { id: string; @@ -22,8 +22,8 @@ export type JsonRpcErrorResponse = { message: string; }; }; -}; -export type JsonRpcNotification = { +} +export interface IJsonRpcNotification { type: 'notification'; payload: { method: string; @@ -32,78 +32,56 @@ export type JsonRpcNotification = { result: any; }; }; -}; -export type JsonRpcSuccess = { +} +export interface IJsonRpcSuccess { type: 'success'; payload: { id: string; result: any; }; -}; -export type JsonRpcMessage = JsonRpcErrorResponse | JsonRpcNotification | JsonRpcSuccess; +} +export type JsonRpcMessage = IJsonRpcErrorResponse | IJsonRpcNotification | IJsonRpcSuccess; export class RemoteError extends Error { - _code: number; - _details: string; - - constructor(code: number, details: string) { - super(`Remote JSON-RPC error ${code}: ${details}`); - this._code = code; - this._details = details; + constructor(private codeValue: number, private detailsValue: string) { + super(`Remote JSON-RPC error ${codeValue}: ${detailsValue}`); } get code(): number { - return this._code; + return this.codeValue; } get details(): string { - return this._details; + return this.detailsValue; } } export class TimeOutError extends Error { - _jsonRpcMessage: Object; - - constructor(jsonRpcMessage: Object) { + constructor(private jsonRpcMessageValue: object) { super('Request timed out'); - - this._jsonRpcMessage = jsonRpcMessage; } - get jsonRpcMessage(): Object { - return this._jsonRpcMessage; + get jsonRpcMessage(): object { + return this.jsonRpcMessageValue; } } export class SubscriptionError extends Error { - _reply: any; - - constructor(message: string, reply: any) { - const replyString = JSON.stringify(reply); - - super(`${message}: ${replyString}`); - - this._reply = reply; + constructor(message: string, private replyValue: any) { + super(`${message}: ${JSON.stringify(replyValue)}`); } get reply(): any { - return this._reply; + return this.replyValue; } } export class WebSocketError extends Error { - _code: number; - - constructor(code: number) { - super(WebSocketError.reason(code)); - this._code = code; - } - get code(): number { - return this._code; + return this.codeValue; } - static reason(code: number): string { + private static reason(code: number): string { switch (code) { case 1006: return 'Abnormal closure'; @@ -117,6 +95,9 @@ export class WebSocketError extends Error { return `Unknown (${code})`; } } + constructor(private codeValue: number) { + super(WebSocketError.reason(codeValue)); + } } export class TransportError extends Error {} @@ -124,18 +105,18 @@ export class TransportError extends Error {} const DEFAULT_TIMEOUT_MILLIS = 5000; export default class JsonRpcClient<T> extends EventEmitter { - _unansweredRequests: Map<string, UnansweredRequest> = new Map(); - _subscriptions: Map<string | number, (value: any) => void> = new Map(); - _transport: Transport<T>; + private unansweredRequests: Map<string, IUnansweredRequest> = new Map(); + private subscriptions: Map<string | number, (value: any) => void> = new Map(); + private transport: ITransport<T>; - constructor(transport: Transport<T>) { + constructor(transport: ITransport<T>) { super(); - this._transport = transport; + this.transport = transport; } /// Connect websocket - connect(connectionParams: T): Promise<void> { + public connect(connectionParams: T): Promise<void> { return new Promise((resolve, reject) => { this.disconnect(); @@ -144,10 +125,10 @@ export default class JsonRpcClient<T> extends EventEmitter { // A flag used to determine if Promise was resolved. let isPromiseResolved = false; - const transport = this._transport; + const transport = this.transport; transport.onOpen = () => { - log.info('Transport is connected'); + log.info('ITransport is connected'); this.emit('open'); // Resolve the Promise @@ -156,12 +137,12 @@ export default class JsonRpcClient<T> extends EventEmitter { }; transport.onMessage = (obj) => { - this._onMessage(obj); + this.onMessage(obj); }; transport.onClose = (error?: Error) => { // Remove all subscriptions since they are connection based - this._subscriptions.clear(); + this.subscriptions.clear(); this.emit('close', error); @@ -172,23 +153,23 @@ export default class JsonRpcClient<T> extends EventEmitter { }; transport.connect(connectionParams); - this._transport = transport; + this.transport = transport; }); } - disconnect() { - if (this._transport) { - this._transport.close(); + public disconnect() { + if (this.transport) { + this.transport.close(); } } - async subscribe(event: string, listener: (value: any) => void): Promise<void> { + public async subscribe(event: string, listener: (value: any) => void): Promise<void> { log.silly(`Adding a listener for ${event}`); try { const subscriptionId = await this.send(`${event}_subscribe`); if (typeof subscriptionId === 'string' || typeof subscriptionId === 'number') { - this._subscriptions.set(subscriptionId, listener); + this.subscriptions.set(subscriptionId, listener); } else { throw new SubscriptionError( 'The subscription id was not a string or a number', @@ -201,19 +182,19 @@ export default class JsonRpcClient<T> extends EventEmitter { } } - send(action: string, data?: any, timeout: number = DEFAULT_TIMEOUT_MILLIS): Promise<any> { + public send(action: string, data?: any, timeout: number = DEFAULT_TIMEOUT_MILLIS): Promise<any> { return new Promise((resolve, reject) => { - const transport = this._transport; + const transport = this.transport; if (!transport) { reject(new Error('RPC client transport is not connected.')); return; } const id = uuid.v4(); - const payload = this._prepareParams(data); - const timerId = setTimeout(() => this._onTimeout(id), timeout); + const payload = this.prepareParams(data); + const timerId = setTimeout(() => this.onTimeout(id), timeout); const message = jsonrpc.request(id, action, payload); - this._unansweredRequests.set(id, { + this.unansweredRequests.set(id, { resolve, reject, timerId, @@ -227,7 +208,7 @@ export default class JsonRpcClient<T> extends EventEmitter { log.error(`Failed sending RPC message "${action}": ${error.message}`); // clean up on error - this._unansweredRequests.delete(id); + this.unansweredRequests.delete(id); clearTimeout(timerId); throw error; @@ -235,7 +216,7 @@ export default class JsonRpcClient<T> extends EventEmitter { }); } - _prepareParams(data?: any): Array<any> | Object { + private prepareParams(data?: any): any[] | object { // JSONRPC only accepts arrays and objects as params, but // this isn't very nice to use, so this method wraps other // types in an array. The choice of array is based on try-and-error @@ -251,10 +232,10 @@ export default class JsonRpcClient<T> extends EventEmitter { } } - _onTimeout(requestId: string) { - const request = this._unansweredRequests.get(requestId); + private onTimeout(requestId: string) { + const request = this.unansweredRequests.get(requestId); - this._unansweredRequests.delete(requestId); + this.unansweredRequests.delete(requestId); if (request) { log.warn(`Request ${requestId} timed out: `, request.message); @@ -264,29 +245,25 @@ export default class JsonRpcClient<T> extends EventEmitter { } } - _onMessage(obj: Object) { - let messages: Array<any> = []; + private onMessage(obj: object) { + let message: any; try { - // TODO: Fix the type weirdness. // @ts-ignore - const message = jsonrpc.parseObject(obj); - messages = Array.isArray(message) ? message : [message]; + message = jsonrpc.parseObject(obj); } catch (error) { log.error(`Failed to parse JSON-RPC message: ${error} for object`); } - for (const message of messages) { - if (message.type === 'notification') { - this._onNotification(message); - } else { - this._onReply(message); - } + if (message.type === 'notification') { + this.onNotification(message); + } else { + this.onReply(message); } } - _onNotification(message: JsonRpcNotification) { + private onNotification(message: IJsonRpcNotification) { const subscriptionId = message.payload.params.subscription; - const listener = this._subscriptions.get(subscriptionId); + const listener = this.subscriptions.get(subscriptionId); if (listener) { log.silly(`Got notification for ${message.payload.method}`); @@ -296,10 +273,10 @@ export default class JsonRpcClient<T> extends EventEmitter { } } - _onReply(message: JsonRpcErrorResponse | JsonRpcSuccess) { + private onReply(message: IJsonRpcErrorResponse | IJsonRpcSuccess) { const id = message.payload.id; - const request = this._unansweredRequests.get(id); - this._unansweredRequests.delete(id); + const request = this.unansweredRequests.get(id); + this.unansweredRequests.delete(id); if (request) { log.silly('Got answer to', id, message.type); @@ -319,36 +296,44 @@ export default class JsonRpcClient<T> extends EventEmitter { } } -interface Transport<T> { - close(): void; +interface ITransport<T> { onOpen: () => void; - onMessage: (data: Object) => void; + onMessage: (data: object) => void; onClose: (error?: Error) => void; + close(): void; send(message: string): void; connect(params: T): void; } -export class WebsocketTransport implements Transport<string> { - ws?: WebSocket; - onOpen = () => {}; - onMessage = (_message: Object) => {}; - onClose = (_error?: Error) => {}; +export class WebsocketTransport implements ITransport<string> { + public ws?: WebSocket; constructor(ws?: WebSocket) { this.ws = ws; } + public onOpen = () => { + // no-op + }; + public onMessage = (_message: object) => { + // no-op + }; + public onClose = (_error?: Error) => { + // no-op + }; - close() { - if (this.ws) this.ws.close(); + public close() { + if (this.ws) { + this.ws.close(); + } } - send(msg: string) { + public send(msg: string) { if (this.ws) { this.ws.send(msg); } } - connect(params: string): void { + public connect(params: string): void { if (this.ws) { this.ws.close(); } @@ -383,32 +368,37 @@ export class WebsocketTransport implements Transport<string> { // Given the correct parameters, this transport supports named pipes/unix // domain sockets, and also TCP/UDP sockets -export class SocketTransport implements Transport<{ path: string }> { - onMessage = (_message: Object) => {}; - onClose = (_error?: Error) => {}; - onOpen = () => {}; - - _connection?: net.Socket; - _socketReady = false; - _shouldClose = false; - _lastError?: Error; +export class SocketTransport implements ITransport<{ path: string }> { + private connection?: net.Socket; + private socketReady = false; + private shouldClose = false; + private lastError?: Error; + public onMessage = (_message: object) => { + // no-op + }; + public onClose = (_error?: Error) => { + // no-op + }; + public onOpen = () => { + // no-op + }; - connect(options: { path: string }) { - assert(!this._connection, 'Make sure to close the existing socket'); + public connect(options: { path: string }) { + assert(!this.connection, 'Make sure to close the existing socket'); const jsonStream = JSONStream.parse(null) - .on('data', this._onJsonStreamData) - .on('error', this._onJsonStreamError); + .on('data', this.onJsonStreamData) + .on('error', this.onJsonStreamError); const connection = new net.Socket() - .on('ready', this._onSocketReady) - .on('error', this._onSocketError) - .on('close', this._onSocketClose); + .on('ready', this.onSocketReady) + .on('error', this.onSocketError) + .on('close', this.onSocketClose); - this._connection = connection; - this._socketReady = false; - this._shouldClose = false; - this._lastError = undefined; + this.connection = connection; + this.socketReady = false; + this.shouldClose = false; + this.lastError = undefined; log.debug('Connect socket'); @@ -416,51 +406,51 @@ export class SocketTransport implements Transport<{ path: string }> { connection.connect(options); } - close() { - this._shouldClose = true; + public close() { + this.shouldClose = true; try { - if (this._connection) { - this._connection.end(); + if (this.connection) { + this.connection.end(); } } catch (error) { log.error('Failed to close the socket: ', error); } - this._connection = undefined; + this.connection = undefined; } - send(msg: string) { - if (this._socketReady && this._connection) { - this._connection.write(msg); + public send(msg: string) { + if (this.socketReady && this.connection) { + this.connection.write(msg); } else { throw new TransportError('Socket not connected'); } } - _onSocketReady = () => { - this._socketReady = true; + private onSocketReady = () => { + this.socketReady = true; log.debug('Socket is ready'); this.onOpen(); }; - _onSocketError = (error: Error) => { - this._lastError = error; + private onSocketError = (error: Error) => { + this.lastError = error; log.error('Socket error: ', error); }; - _onSocketClose = (hadError: boolean) => { - if (this._shouldClose) { + private onSocketClose = (hadError: boolean) => { + if (this.shouldClose) { log.debug(`Socket was closed deliberately`); this.onClose(); } else if (hadError) { log.debug(`Socket was closed due to an error`); - this.onClose(this._lastError); + this.onClose(this.lastError); } else { log.debug(`Socket was closed by peer`); @@ -468,16 +458,16 @@ export class SocketTransport implements Transport<{ path: string }> { } }; - _onJsonStreamData = (data: Object) => { + private onJsonStreamData = (data: object) => { this.onMessage(data); }; - _onJsonStreamError = (error: Error) => { + private onJsonStreamError = (error: Error) => { log.error('Socket JSON stream error: ', error); - if (this._connection) { + if (this.connection) { // This will destroy the socket and emit "error" and "close" events - this._connection.destroy(error); + this.connection.destroy(error); } }; } diff --git a/gui/packages/desktop/src/main/keyframe-animation.ts b/gui/packages/desktop/src/main/keyframe-animation.ts index aad9a3d24f..6e405c6ef7 100644 --- a/gui/packages/desktop/src/main/keyframe-animation.ts +++ b/gui/packages/desktop/src/main/keyframe-animation.ts @@ -1,129 +1,130 @@ export type OnFrameFn = (frame: number) => void; export type OnFinishFn = () => void; -export type KeyframeAnimationOptions = { + +export interface IKeyframeAnimationOptions { start?: number; end: number; -}; +} export type KeyframeAnimationRange = [number, number]; export default class KeyframeAnimation { - _speed: number = 200; // ms + private speedValue: number = 200; // ms - _onFrame?: OnFrameFn; - _onFinish?: OnFinishFn; + private onFrameValue?: OnFrameFn; + private onFinishValue?: OnFinishFn; - _currentFrame: number = 0; - _targetFrame: number = 0; + private currentFrame: number = 0; + private targetFrame: number = 0; - _isRunning: boolean = false; - _isFinished: boolean = false; + private isRunningValue: boolean = false; + private isFinishedValue: boolean = false; - _timeout?: NodeJS.Timeout; + private timeout?: NodeJS.Timeout; set onFrame(newValue: OnFrameFn | undefined) { - this._onFrame = newValue; + this.onFrameValue = newValue; } get onFrame(): OnFrameFn | undefined { - return this._onFrame; + return this.onFrameValue; } // called when animation finished set onFinish(newValue: OnFinishFn | undefined) { - this._onFinish = newValue; + this.onFinishValue = newValue; } get onFinish(): OnFinishFn | undefined { - return this._onFinish; + return this.onFinishValue; } // pace per frame in ms set speed(newValue: number) { - this._speed = newValue; + this.speedValue = newValue; } get speed(): number { - return this._speed; + return this.speedValue; } get isRunning(): boolean { - return this._isRunning; + return this.isRunningValue; } get isFinished(): boolean { - return this._isFinished; + return this.isFinishedValue; } - play(options: KeyframeAnimationOptions) { + public play(options: IKeyframeAnimationOptions) { const { start, end } = options; if (start !== undefined) { - this._currentFrame = start; + this.currentFrame = start; } - this._targetFrame = end; + this.targetFrame = end; - this._isRunning = true; - this._isFinished = false; + this.isRunningValue = true; + this.isFinishedValue = false; - this._unscheduleUpdate(); + this.unscheduleUpdate(); - this._render(); - this._scheduleUpdate(); + this.render(); + this.scheduleUpdate(); } - stop() { - this._isRunning = false; - this._unscheduleUpdate(); + public stop() { + this.isRunningValue = false; + this.unscheduleUpdate(); } - _unscheduleUpdate() { - if (this._timeout) { - clearTimeout(this._timeout); - this._timeout = undefined; + private unscheduleUpdate() { + if (this.timeout) { + clearTimeout(this.timeout); + this.timeout = undefined; } } - _scheduleUpdate() { - this._timeout = setTimeout(() => this._onUpdateFrame(), this._speed); + private scheduleUpdate() { + this.timeout = setTimeout(() => this.onUpdateFrame(), this.speedValue); } - _render() { - if (this._onFrame) { - this._onFrame(this._currentFrame); + private render() { + if (this.onFrameValue) { + this.onFrameValue(this.currentFrame); } } - _didFinish() { - this._isFinished = true; - this._isRunning = false; + private didFinish() { + this.isFinishedValue = true; + this.isRunningValue = false; - if (this._onFinish) { - this._onFinish(); + if (this.onFinishValue) { + this.onFinishValue(); } } - _onUpdateFrame() { - this._advanceFrame(); + private onUpdateFrame() { + this.advanceFrame(); - if (!this._isFinished) { - this._render(); + if (!this.isFinishedValue) { + this.render(); // check once again since onFrame() may stop animation - if (this._isRunning) { - this._scheduleUpdate(); + if (this.isRunningValue) { + this.scheduleUpdate(); } } } - _advanceFrame() { - if (this._isFinished) { + private advanceFrame() { + if (this.isFinishedValue) { return; } - if (this._currentFrame === this._targetFrame) { - this._didFinish(); - } else if (this._currentFrame < this._targetFrame) { - this._currentFrame += 1; + if (this.currentFrame === this.targetFrame) { + this.didFinish(); + } else if (this.currentFrame < this.targetFrame) { + this.currentFrame += 1; } else { - this._currentFrame -= 1; + this.currentFrame -= 1; } } } diff --git a/gui/packages/desktop/src/main/notification-controller.ts b/gui/packages/desktop/src/main/notification-controller.ts index 2825775e97..86f23a8b4f 100644 --- a/gui/packages/desktop/src/main/notification-controller.ts +++ b/gui/packages/desktop/src/main/notification-controller.ts @@ -1,34 +1,34 @@ -import { shell, Notification } from 'electron'; +import { Notification, shell } from 'electron'; import config from '../config.json'; import { TunnelStateTransition } from '../shared/daemon-rpc-types'; export default class NotificationController { - _lastTunnelStateAnnouncement?: { body: string; notification: Notification }; - _reconnecting = false; - _presentedNotifications: { [key: string]: boolean } = {}; - _pendingNotifications: Array<Notification> = []; + private lastTunnelStateAnnouncement?: { body: string; notification: Notification }; + private reconnecting = false; + private presentedNotifications: { [key: string]: boolean } = {}; + private pendingNotifications: Notification[] = []; - notifyTunnelState(tunnelState: TunnelStateTransition) { + public notifyTunnelState(tunnelState: TunnelStateTransition) { switch (tunnelState.state) { case 'connecting': - if (!this._reconnecting) { - this._showTunnelStateNotification('Connecting'); + if (!this.reconnecting) { + this.showTunnelStateNotification('Connecting'); } break; case 'connected': - this._showTunnelStateNotification('Secured'); + this.showTunnelStateNotification('Secured'); break; case 'disconnected': - this._showTunnelStateNotification('Unsecured'); + this.showTunnelStateNotification('Unsecured'); break; case 'blocked': switch (tunnelState.details.reason) { case 'set_firewall_policy_error': - this._showTunnelStateNotification('Critical failure - Unsecured'); + this.showTunnelStateNotification('Critical failure - Unsecured'); break; default: - this._showTunnelStateNotification('Blocked all connections'); + this.showTunnelStateNotification('Blocked all connections'); break; } break; @@ -39,29 +39,29 @@ export default class NotificationController { // no-op break; case 'reconnect': - this._showTunnelStateNotification('Reconnecting'); - this._reconnecting = true; + this.showTunnelStateNotification('Reconnecting'); + this.reconnecting = true; return; } break; } - this._reconnecting = false; + this.reconnecting = false; } - notifyInconsistentVersion() { - this._presentNotificationOnce('inconsistent-version', () => { + public notifyInconsistentVersion() { + this.presentNotificationOnce('inconsistent-version', () => { const notification = new Notification({ title: '', body: 'Inconsistent internal version information, please restart the app', silent: true, }); - this._scheduleNotification(notification); + this.scheduleNotification(notification); }); } - notifyUnsupportedVersion(upgradeVersion: string) { - this._presentNotificationOnce('unsupported-version', () => { + public notifyUnsupportedVersion(upgradeVersion: string) { + this.presentNotificationOnce('unsupported-version', () => { const notification = new Notification({ title: '', body: `You are running an unsupported app version. Please upgrade to ${upgradeVersion} now to ensure your security`, @@ -72,18 +72,18 @@ export default class NotificationController { shell.openExternal(config.links.download); }); - this._scheduleNotification(notification); + this.scheduleNotification(notification); }); } - cancelPendingNotifications() { - for (const notification of this._pendingNotifications) { + public cancelPendingNotifications() { + for (const notification of this.pendingNotifications) { notification.close(); } } - _showTunnelStateNotification(message: string) { - const lastAnnouncement = this._lastTunnelStateAnnouncement; + private showTunnelStateNotification(message: string) { + const lastAnnouncement = this.lastTunnelStateAnnouncement; const sameAsLastNotification = lastAnnouncement && lastAnnouncement.body === message; if (sameAsLastNotification) { @@ -100,42 +100,42 @@ export default class NotificationController { lastAnnouncement.notification.close(); } - this._lastTunnelStateAnnouncement = { + this.lastTunnelStateAnnouncement = { body: message, notification: newNotification, }; - this._scheduleNotification(newNotification); + this.scheduleNotification(newNotification); } - _presentNotificationOnce(notificationName: string, presentNotification: () => void) { - const presented = this._presentedNotifications; + private presentNotificationOnce(notificationName: string, presentNotification: () => void) { + const presented = this.presentedNotifications; if (!presented[notificationName]) { presented[notificationName] = true; presentNotification(); } } - _scheduleNotification(notification: Notification) { - this._addPendingNotification(notification); + private scheduleNotification(notification: Notification) { + this.addPendingNotification(notification); notification.show(); setTimeout(() => notification.close(), 4000); } - _addPendingNotification(notification: Notification) { + private addPendingNotification(notification: Notification) { notification.on('close', () => { - this._removePendingNotification(notification); + this.removePendingNotification(notification); }); - this._pendingNotifications.push(notification); + this.pendingNotifications.push(notification); } - _removePendingNotification(notification: Notification) { - const index = this._pendingNotifications.indexOf(notification); + private removePendingNotification(notification: Notification) { + const index = this.pendingNotifications.indexOf(notification); if (index !== -1) { - this._pendingNotifications.splice(index, 1); + this.pendingNotifications.splice(index, 1); } } } diff --git a/gui/packages/desktop/src/main/reconnection-backoff.ts b/gui/packages/desktop/src/main/reconnection-backoff.ts index a9a2cd9c75..5709f053d7 100644 --- a/gui/packages/desktop/src/main/reconnection-backoff.ts +++ b/gui/packages/desktop/src/main/reconnection-backoff.ts @@ -3,20 +3,20 @@ * It uses a linear backoff function that goes from 500ms to 3000ms. */ export default class ReconnectionBackoff { - _attempt = 0; + private attemptValue = 0; - attempt(handler: () => void) { - setTimeout(handler, this._getIncreasedBackoff()); + public attempt(handler: () => void) { + setTimeout(handler, this.getIncreasedBackoff()); } - reset() { - this._attempt = 0; + public reset() { + this.attemptValue = 0; } - _getIncreasedBackoff() { - if (this._attempt < 6) { - this._attempt++; + private getIncreasedBackoff() { + if (this.attemptValue < 6) { + this.attemptValue++; } - return this._attempt * 500; + return this.attemptValue * 500; } } diff --git a/gui/packages/desktop/src/main/tray-icon-controller.ts b/gui/packages/desktop/src/main/tray-icon-controller.ts index 1a8c5b28c8..f3333a2636 100644 --- a/gui/packages/desktop/src/main/tray-icon-controller.ts +++ b/gui/packages/desktop/src/main/tray-icon-controller.ts @@ -1,78 +1,77 @@ +import { nativeImage, NativeImage, Tray } from 'electron'; import path from 'path'; import KeyframeAnimation from './keyframe-animation'; -import { nativeImage } from 'electron'; -import { NativeImage, Tray } from 'electron'; export type TrayIconType = 'unsecured' | 'securing' | 'secured'; export default class TrayIconController { - _animation?: KeyframeAnimation; - _iconType: TrayIconType; - _iconImages: Array<NativeImage> = []; - _monochromaticIconImages: Array<NativeImage> = []; - _useMonochromaticIcon: boolean; + private animation?: KeyframeAnimation; + private iconImages: NativeImage[] = []; + private monochromaticIconImages: NativeImage[] = []; - constructor(tray: Tray, initialType: TrayIconType, useMonochromaticIcon: boolean) { - this._loadImages(); - this._iconType = initialType; - this._useMonochromaticIcon = useMonochromaticIcon; + constructor( + tray: Tray, + private iconTypeValue: TrayIconType, + private useMonochromaticIconValue: boolean, + ) { + this.loadImages(); - const initialFrame = this._targetFrame(); + const initialFrame = this.targetFrame(); const animation = new KeyframeAnimation(); animation.speed = 100; - animation.onFrame = (frameNumber) => tray.setImage(this._imageForFrame(frameNumber)); + animation.onFrame = (frameNumber) => tray.setImage(this.imageForFrame(frameNumber)); animation.play({ start: initialFrame, end: initialFrame }); - this._animation = animation; + this.animation = animation; } - dispose() { - if (this._animation) { - this._animation.stop(); - this._animation = undefined; + public dispose() { + if (this.animation) { + this.animation.stop(); + this.animation = undefined; } } get iconType(): TrayIconType { - return this._iconType; + return this.iconTypeValue; } set useMonochromaticIcon(useMonochromaticIcon: boolean) { - this._useMonochromaticIcon = useMonochromaticIcon; + this.useMonochromaticIconValue = useMonochromaticIcon; - if (this._animation && !this._animation.isRunning) { - this._animation.play({ end: this._targetFrame() }); + if (this.animation && !this.animation.isRunning) { + this.animation.play({ end: this.targetFrame() }); } } - animateToIcon(type: TrayIconType) { - if (this._iconType === type || !this._animation) { + public animateToIcon(type: TrayIconType) { + if (this.iconTypeValue === type || !this.animation) { return; } - this._iconType = type; + this.iconTypeValue = type; - const animation = this._animation; - const frame = this._targetFrame(); + const animation = this.animation; + const frame = this.targetFrame(); animation.play({ end: frame }); } - _loadImages() { + private loadImages() { const basePath = path.resolve(path.join(__dirname, '../../assets/images/menubar icons')); const frames = Array.from({ length: 10 }, (_, i) => i + 1); - this._iconImages = frames.map((frame) => + this.iconImages = frames.map((frame) => nativeImage.createFromPath(path.join(basePath, `lock-${frame}.png`)), ); - this._monochromaticIconImages = frames.map((frame) => + this.monochromaticIconImages = frames.map((frame) => nativeImage.createFromPath(path.join(basePath, `lock-${frame}Template.png`)), ); } - _targetFrame(): number { - switch (this._iconType) { + private targetFrame(): number { + switch (this.iconTypeValue) { case 'unsecured': return 0; case 'securing': @@ -82,9 +81,9 @@ export default class TrayIconController { } } - _imageForFrame(frame: number): NativeImage { - return this._useMonochromaticIcon - ? this._monochromaticIconImages[frame] - : this._iconImages[frame]; + private imageForFrame(frame: number): NativeImage { + return this.useMonochromaticIconValue + ? this.monochromaticIconImages[frame] + : this.iconImages[frame]; } } diff --git a/gui/packages/desktop/src/main/window-controller.ts b/gui/packages/desktop/src/main/window-controller.ts index e740f216ab..a0cbd77399 100644 --- a/gui/packages/desktop/src/main/window-controller.ts +++ b/gui/packages/desktop/src/main/window-controller.ts @@ -1,19 +1,21 @@ -import { screen } from 'electron'; -import { BrowserWindow, Tray, Display, WebContents } from 'electron'; +import { BrowserWindow, Display, screen, Tray, WebContents } from 'electron'; -type Position = { x: number; y: number }; +interface IPosition { + x: number; + y: number; +} -export type WindowShapeParameters = { +export interface IWindowShapeParameters { arrowPosition?: number; -}; +} -interface WindowPositioning { - getPosition(window: BrowserWindow): Position; - getWindowShapeParameters(window: BrowserWindow): WindowShapeParameters; +interface IWindowPositioning { + getPosition(window: BrowserWindow): IPosition; + getWindowShapeParameters(window: BrowserWindow): IWindowShapeParameters; } -class StandaloneWindowPositioning implements WindowPositioning { - getPosition(window: BrowserWindow): Position { +class StandaloneWindowPositioning implements IWindowPositioning { + public getPosition(window: BrowserWindow): IPosition { const windowBounds = window.getBounds(); const primaryDisplay = screen.getPrimaryDisplay(); @@ -27,33 +29,34 @@ class StandaloneWindowPositioning implements WindowPositioning { return { x, y }; } - getWindowShapeParameters(_window: BrowserWindow): WindowShapeParameters { + public getWindowShapeParameters(_window: BrowserWindow): IWindowShapeParameters { return {}; } } -class AttachedToTrayWindowPositioning implements WindowPositioning { - _tray: Tray; +class AttachedToTrayWindowPositioning implements IWindowPositioning { + private tray: Tray; constructor(tray: Tray) { - this._tray = tray; + this.tray = tray; } - getPosition(window: BrowserWindow): Position { + public getPosition(window: BrowserWindow): IPosition { const windowBounds = window.getBounds(); - const trayBounds = this._tray.getBounds(); + const trayBounds = this.tray.getBounds(); const activeDisplay = screen.getDisplayNearestPoint({ x: trayBounds.x, y: trayBounds.y, }); const workArea = activeDisplay.workArea; - const placement = this._getTrayPlacement(); + const placement = this.getTrayPlacement(); const maxX = workArea.x + workArea.width - windowBounds.width; const maxY = workArea.y + workArea.height - windowBounds.height; - let x = 0, - y = 0; + let x = 0; + let y = 0; + switch (placement) { case 'top': x = trayBounds.x + (trayBounds.width - windowBounds.width) * 0.5; @@ -90,8 +93,8 @@ class AttachedToTrayWindowPositioning implements WindowPositioning { }; } - getWindowShapeParameters(window: BrowserWindow): WindowShapeParameters { - const trayBounds = this._tray.getBounds(); + public getWindowShapeParameters(window: BrowserWindow): IWindowShapeParameters { + const trayBounds = this.tray.getBounds(); const windowBounds = window.getBounds(); const arrowPosition = trayBounds.x - windowBounds.x + trayBounds.width * 0.5; return { @@ -99,7 +102,7 @@ class AttachedToTrayWindowPositioning implements WindowPositioning { }; } - _getTrayPlacement() { + private getTrayPlacement() { switch (process.platform) { case 'darwin': // macOS has menubar always placed at the top @@ -127,122 +130,118 @@ class AttachedToTrayWindowPositioning implements WindowPositioning { } export default class WindowController { - _window: BrowserWindow; - _width: number; - _height: number; - _windowPositioning: WindowPositioning; - _isWindowReady = false; + private width: number; + private height: number; + private windowPositioning: IWindowPositioning; + private isWindowReady = false; get window(): BrowserWindow { - return this._window; + return this.windowValue; } get webContents(): WebContents { - return this._window.webContents; + return this.windowValue.webContents; } - constructor(window: BrowserWindow, tray: Tray) { - this._window = window; - const [width, height] = window.getSize(); - this._width = width; - this._height = height; - - if (process.platform === 'linux') { - this._windowPositioning = new StandaloneWindowPositioning(); - } else { - this._windowPositioning = new AttachedToTrayWindowPositioning(tray); - } + constructor(private windowValue: BrowserWindow, tray: Tray) { + const [width, height] = windowValue.getSize(); + this.width = width; + this.height = height; + this.windowPositioning = + process.platform === 'linux' + ? new StandaloneWindowPositioning() + : new AttachedToTrayWindowPositioning(tray); - this._installDisplayMetricsHandler(); - this._installWindowReadyHandlers(); + this.installDisplayMetricsHandler(); + this.installWindowReadyHandlers(); } - show(whenReady: boolean = true) { + public show(whenReady: boolean = true) { if (whenReady) { - this._executeWhenWindowIsReady(() => this._showImmediately()); + this.executeWhenWindowIsReady(() => this.showImmediately()); } else { - this._showImmediately(); + this.showImmediately(); } } - hide() { - this._window.hide(); + public hide() { + this.windowValue.hide(); } - toggle() { - if (this._window.isVisible()) { + public toggle() { + if (this.windowValue.isVisible()) { this.hide(); } else { this.show(); } } - isVisible(): boolean { - return this._window.isVisible(); + public isVisible(): boolean { + return this.windowValue.isVisible(); } - send(event: string, ...data: any[]): void { - this._window.webContents.send(event, ...data); + public send(event: string, ...data: any[]): void { + this.windowValue.webContents.send(event, ...data); } - _showImmediately() { - const window = this._window; + private showImmediately() { + const window = this.windowValue; - this._updatePosition(); - this._notifyUpdateWindowShape(); + this.updatePosition(); + this.notifyUpdateWindowShape(); window.show(); window.focus(); } - _updatePosition() { - const { x, y } = this._windowPositioning.getPosition(this._window); - this._window.setPosition(x, y, false); + private updatePosition() { + const { x, y } = this.windowPositioning.getPosition(this.windowValue); + this.windowValue.setPosition(x, y, false); } - _notifyUpdateWindowShape() { - const shapeParameters = this._windowPositioning.getWindowShapeParameters(this._window); - this._window.webContents.send('update-window-shape', shapeParameters); + private notifyUpdateWindowShape() { + const shapeParameters = this.windowPositioning.getWindowShapeParameters(this.windowValue); + this.windowValue.webContents.send('update-window-shape', shapeParameters); } // Installs display event handlers to update the window position on any changes in the display or // workarea dimensions. - _installDisplayMetricsHandler() { - screen.addListener('display-metrics-changed', this._onDisplayMetricsChanged); - this._window.once('closed', () => { - screen.removeListener('display-metrics-changed', this._onDisplayMetricsChanged); + private installDisplayMetricsHandler() { + screen.addListener('display-metrics-changed', this.onDisplayMetricsChanged); + this.windowValue.once('closed', () => { + screen.removeListener('display-metrics-changed', this.onDisplayMetricsChanged); }); } - _onDisplayMetricsChanged = (_event: any, _display: Display, changedMetrics: Array<string>) => { - if (changedMetrics.includes('workArea') && this._window.isVisible()) { - this._updatePosition(); - this._notifyUpdateWindowShape(); + private onDisplayMetricsChanged = (_event: any, _display: Display, changedMetrics: string[]) => { + if (changedMetrics.includes('workArea') && this.windowValue.isVisible()) { + this.updatePosition(); + this.notifyUpdateWindowShape(); } // On linux, the window won't be properly rescaled back to it's original // size if the DPI scaling factor is changed. // https://github.com/electron/electron/issues/11050 if (process.platform === 'linux' && changedMetrics.includes('scaleFactor')) { - this._forceResizeWindow(); + this.forceResizeWindow(); } }; - _forceResizeWindow() { - this._window.setSize(this._width, this._height); + private forceResizeWindow() { + this.windowValue.setSize(this.width, this.height); } - _installWindowReadyHandlers() { - this._window.once('ready-to-show', () => { - this._isWindowReady = true; + private installWindowReadyHandlers() { + this.windowValue.once('ready-to-show', () => { + this.isWindowReady = true; }); } - _executeWhenWindowIsReady(closure: () => any) { - if (this._isWindowReady) { + private executeWhenWindowIsReady(closure: () => any) { + if (this.isWindowReady) { closure(); } else { - this._window.once('ready-to-show', () => { + this.windowValue.once('ready-to-show', () => { closure(); }); } diff --git a/gui/packages/desktop/src/renderer/app.tsx b/gui/packages/desktop/src/renderer/app.tsx index 50c4773ca0..9ba305c7d0 100644 --- a/gui/packages/desktop/src/renderer/app.tsx +++ b/gui/packages/desktop/src/renderer/app.tsx @@ -1,67 +1,67 @@ -import log from 'electron-log'; -import { webFrame, ipcRenderer } from 'electron'; -import * as React from 'react'; -import { bindActionCreators } from 'redux'; -import { Provider } from 'react-redux'; import { ConnectedRouter, push as pushHistory, replace as replaceHistory, } from 'connected-react-router'; +import { ipcRenderer, 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'; import { InvalidAccountError } from '../main/errors'; import makeRoutes from './routes'; -import configureStore from './redux/store'; import accountActions from './redux/account/actions'; import connectionActions from './redux/connection/actions'; import settingsActions from './redux/settings/actions'; -import versionActions from './redux/version/actions'; +import configureStore from './redux/store'; import userInterfaceActions from './redux/userinterface/actions'; +import versionActions from './redux/version/actions'; -import { WindowShapeParameters } from '../main/window-controller'; -import { CurrentAppVersionInfo, AppUpgradeInfo } from '../main'; -import { GuiSettingsState } from '../shared/gui-settings-state'; +import { IAppUpgradeInfo, ICurrentAppVersionInfo } from '../main'; +import { IWindowShapeParameters } from '../main/window-controller'; +import { IGuiSettingsState } from '../shared/gui-settings-state'; import { IpcRendererEventChannel } from '../shared/ipc-event-channel'; import { AccountToken, - AccountData, ConnectionConfig, - Location, - RelayList, - RelaySettingsUpdate, + IAccountData, + ILocation, + IRelayList, + ISettings, RelaySettings, - Settings, + RelaySettingsUpdate, TunnelStateTransition, } from '../shared/daemon-rpc-types'; export default class AppRenderer { - _memoryHistory = createMemoryHistory(); - _reduxStore = configureStore(null, this._memoryHistory); - _reduxActions: { [key: string]: any }; - _accountDataCache = new AccountDataCache( + private memoryHistory = createMemoryHistory(); + private reduxStore = configureStore(this.memoryHistory); + private reduxActions: { [key: string]: any }; + private accountDataCache = new AccountDataCache( (accountToken) => { return IpcRendererEventChannel.account.getData(accountToken); }, (accountData) => { const expiry = accountData ? accountData.expiry : null; - this._reduxActions.account.updateAccountExpiry(expiry); + this.reduxActions.account.updateAccountExpiry(expiry); }, ); - _tunnelState: TunnelStateTransition; - _settings: Settings; - _guiSettings: GuiSettingsState; - _connectedToDaemon = false; - _autoConnected = false; - _doingLogin = false; - _loginTimer?: NodeJS.Timeout; + private tunnelState: TunnelStateTransition; + private settings: ISettings; + private guiSettings: IGuiSettingsState; + private connectedToDaemon = false; + private autoConnected = false; + private doingLogin = false; + private loginTimer?: NodeJS.Timeout; constructor() { - const dispatch = this._reduxStore.dispatch; - this._reduxActions = { + const dispatch = this.reduxStore.dispatch; + this.reduxActions = { account: bindActionCreators(accountActions, dispatch), connection: bindActionCreators(connectionActions, dispatch), settings: bindActionCreators(settingsActions, dispatch), @@ -78,105 +78,105 @@ export default class AppRenderer { ipcRenderer.on( 'update-window-shape', - (_event: Electron.Event, shapeParams: WindowShapeParameters) => { + (_event: Electron.Event, shapeParams: IWindowShapeParameters) => { if (typeof shapeParams.arrowPosition === 'number') { - this._reduxActions.userInterface.updateWindowArrowPosition(shapeParams.arrowPosition); + this.reduxActions.userInterface.updateWindowArrowPosition(shapeParams.arrowPosition); } }, ); ipcRenderer.on('window-shown', () => { - if (this._connectedToDaemon) { + if (this.connectedToDaemon) { this.updateAccountExpiry(); } }); IpcRendererEventChannel.daemonConnected.listen(() => { - this._onDaemonConnected(); + this.onDaemonConnected(); }); IpcRendererEventChannel.daemonDisconnected.listen((errorMessage?: string) => { - this._onDaemonDisconnected(errorMessage ? new Error(errorMessage) : undefined); + this.onDaemonDisconnected(errorMessage ? new Error(errorMessage) : undefined); }); IpcRendererEventChannel.tunnel.listen((newState: TunnelStateTransition) => { - this._setTunnelState(newState); + this.setTunnelState(newState); }); - IpcRendererEventChannel.settings.listen((newSettings: Settings) => { - const oldSettings = this._settings; + IpcRendererEventChannel.settings.listen((newSettings: ISettings) => { + const oldSettings = this.settings; - this._setSettings(newSettings); - this._handleAccountChange(oldSettings.accountToken, newSettings.accountToken); + this.setSettings(newSettings); + this.handleAccountChange(oldSettings.accountToken, newSettings.accountToken); }); - IpcRendererEventChannel.location.listen((newLocation: Location) => { - this._setLocation(newLocation); + IpcRendererEventChannel.location.listen((newLocation: ILocation) => { + this.setLocation(newLocation); }); - IpcRendererEventChannel.relays.listen((newRelays: RelayList) => { - this._setRelays(newRelays); + IpcRendererEventChannel.relays.listen((newRelays: IRelayList) => { + this.setRelays(newRelays); }); - IpcRendererEventChannel.currentVersion.listen((currentVersion: CurrentAppVersionInfo) => { - this._setCurrentVersion(currentVersion); + IpcRendererEventChannel.currentVersion.listen((currentVersion: ICurrentAppVersionInfo) => { + this.setCurrentVersion(currentVersion); }); - IpcRendererEventChannel.upgradeVersion.listen((upgradeVersion: AppUpgradeInfo) => { - this._setUpgradeVersion(upgradeVersion); + IpcRendererEventChannel.upgradeVersion.listen((upgradeVersion: IAppUpgradeInfo) => { + this.setUpgradeVersion(upgradeVersion); }); - IpcRendererEventChannel.guiSettings.listen((guiSettings: GuiSettingsState) => { - this._setGuiSettings(guiSettings); + IpcRendererEventChannel.guiSettings.listen((guiSettings: IGuiSettingsState) => { + this.setGuiSettings(guiSettings); }); IpcRendererEventChannel.autoStart.listen((autoStart: boolean) => { - this._setAutoStart(autoStart); + this.storeAutoStart(autoStart); }); // Request the initial state from the main process const initialState = IpcRendererEventChannel.state.get(); - this._tunnelState = initialState.tunnelState; - this._settings = initialState.settings; - this._guiSettings = initialState.guiSettings; + this.tunnelState = initialState.tunnelState; + this.settings = initialState.settings; + this.guiSettings = initialState.guiSettings; - this._setTunnelState(initialState.tunnelState); - this._setSettings(initialState.settings); + this.setTunnelState(initialState.tunnelState); + this.setSettings(initialState.settings); if (initialState.location) { - this._setLocation(initialState.location); + this.setLocation(initialState.location); } - this._setRelays(initialState.relays); - this._setCurrentVersion(initialState.currentVersion); - this._setUpgradeVersion(initialState.upgradeVersion); - this._setGuiSettings(initialState.guiSettings); - this._setAutoStart(initialState.autoStart); + this.setRelays(initialState.relays); + this.setCurrentVersion(initialState.currentVersion); + this.setUpgradeVersion(initialState.upgradeVersion); + this.setGuiSettings(initialState.guiSettings); + this.storeAutoStart(initialState.autoStart); if (initialState.isConnected) { - this._onDaemonConnected(); + this.onDaemonConnected(); } // disable pinch to zoom webFrame.setVisualZoomLevelLimits(1, 1); } - renderView() { + public renderView() { return ( - <Provider store={this._reduxStore}> - <ConnectedRouter history={this._memoryHistory}>{makeRoutes({ app: this })}</ConnectedRouter> + <Provider store={this.reduxStore}> + <ConnectedRouter history={this.memoryHistory}>{makeRoutes({ app: this })}</ConnectedRouter> </Provider> ); } - async login(accountToken: AccountToken) { - const actions = this._reduxActions; + public async login(accountToken: AccountToken) { + const actions = this.reduxActions; actions.account.startLogin(accountToken); log.info('Logging in'); - this._doingLogin = true; + this.doingLogin = true; try { const verification = await this.verifyAccount(accountToken); @@ -188,8 +188,8 @@ export default class AppRenderer { await IpcRendererEventChannel.account.set(accountToken); // Redirect the user after some time to allow for the 'Logged in' screen to be visible - this._loginTimer = setTimeout(async () => { - this._memoryHistory.replace('/connect'); + this.loginTimer = setTimeout(async () => { + this.memoryHistory.replace('/connect'); try { log.info('Auto-connecting the tunnel'); @@ -205,10 +205,10 @@ export default class AppRenderer { } } - verifyAccount(accountToken: AccountToken): Promise<AccountVerification> { + public verifyAccount(accountToken: AccountToken): Promise<AccountVerification> { return new Promise((resolve, reject) => { - this._accountDataCache.invalidate(); - this._accountDataCache.fetch(accountToken, { + this.accountDataCache.invalidate(); + this.accountDataCache.fetch(accountToken, { onFinish: () => resolve({ status: 'verified' }), onError: (error): AccountFetchRetryAction => { if (error instanceof InvalidAccountError) { @@ -223,7 +223,7 @@ export default class AppRenderer { }); } - async logout() { + public async logout() { try { await IpcRendererEventChannel.account.unset(); } catch (e) { @@ -231,28 +231,81 @@ export default class AppRenderer { } } - async connectTunnel(): Promise<void> { - const state = this._tunnelState.state; + public async connectTunnel(): Promise<void> { + const state = this.tunnelState.state; // connect only if tunnel is disconnected or blocked. if (state === 'disconnecting' || state === 'disconnected' || state === 'blocked') { // switch to the connecting state ahead of time to make the app look more responsive - this._reduxActions.connection.connecting(null); + this.reduxActions.connection.connecting(null); return IpcRendererEventChannel.tunnel.connect(); } } - disconnectTunnel(): Promise<void> { + public disconnectTunnel(): Promise<void> { return IpcRendererEventChannel.tunnel.disconnect(); } - updateRelaySettings(relaySettings: RelaySettingsUpdate) { + public updateRelaySettings(relaySettings: RelaySettingsUpdate) { return IpcRendererEventChannel.settings.updateRelaySettings(relaySettings); } - _setRelaySettings(relaySettings: RelaySettings) { - const actions = this._reduxActions; + public updateAccountExpiry() { + if (this.settings.accountToken) { + this.accountDataCache.fetch(this.settings.accountToken); + } + } + + public async removeAccountFromHistory(accountToken: AccountToken): Promise<void> { + await IpcRendererEventChannel.accountHistory.removeItem(accountToken); + await this.fetchAccountHistory(); + } + + public async setAllowLan(allowLan: boolean) { + const actions = this.reduxActions; + await IpcRendererEventChannel.settings.setAllowLan(allowLan); + actions.settings.updateAllowLan(allowLan); + } + + public async setEnableIpv6(enableIpv6: boolean) { + const actions = this.reduxActions; + await IpcRendererEventChannel.settings.setEnableIpv6(enableIpv6); + actions.settings.updateEnableIpv6(enableIpv6); + } + + public async setBlockWhenDisconnected(blockWhenDisconnected: boolean) { + const actions = this.reduxActions; + await IpcRendererEventChannel.settings.setBlockWhenDisconnected(blockWhenDisconnected); + actions.settings.updateBlockWhenDisconnected(blockWhenDisconnected); + } + + public async setOpenVpnMssfix(mssfix?: number) { + const actions = this.reduxActions; + actions.settings.updateOpenVpnMssfix(mssfix); + await IpcRendererEventChannel.settings.setOpenVpnMssfix(mssfix); + } + + public async setAutoConnect(autoConnect: boolean) { + return IpcRendererEventChannel.guiSettings.setAutoConnect(autoConnect); + } + + public async setAutoStart(autoStart: boolean): Promise<void> { + this.storeAutoStart(autoStart); + + return IpcRendererEventChannel.autoStart.set(autoStart); + } + + public setStartMinimized(startMinimized: boolean) { + IpcRendererEventChannel.guiSettings.setStartMinimized(startMinimized); + } + + public setMonochromaticIcon(monochromaticIcon: boolean) { + IpcRendererEventChannel.guiSettings.setMonochromaticIcon(monochromaticIcon); + } + + private setRelaySettings(relaySettings: RelaySettings) { + const actions = this.reduxActions; if ('normal' in relaySettings) { const payload: { [key: string]: any } = {}; @@ -260,11 +313,7 @@ export default class AppRenderer { const tunnel = normal.tunnel; const location = normal.location; - if (location === 'any') { - payload.location = 'any'; - } else { - payload.location = location.only; - } + payload.location = location === 'any' ? 'any' : location.only; if (tunnel === 'any') { payload.port = 'any'; @@ -304,110 +353,57 @@ export default class AppRenderer { } } - updateAccountExpiry() { - if (this._settings.accountToken) { - this._accountDataCache.fetch(this._settings.accountToken); - } - } - - async removeAccountFromHistory(accountToken: AccountToken): Promise<void> { - await IpcRendererEventChannel.accountHistory.removeItem(accountToken); - await this._fetchAccountHistory(); - } - - async _fetchAccountHistory(): Promise<void> { + private async fetchAccountHistory(): Promise<void> { const accountHistory = await IpcRendererEventChannel.accountHistory.get(); - this._reduxActions.account.updateAccountHistory(accountHistory); - } - - async setAllowLan(allowLan: boolean) { - const actions = this._reduxActions; - await IpcRendererEventChannel.settings.setAllowLan(allowLan); - actions.settings.updateAllowLan(allowLan); - } - - async setEnableIpv6(enableIpv6: boolean) { - const actions = this._reduxActions; - await IpcRendererEventChannel.settings.setEnableIpv6(enableIpv6); - actions.settings.updateEnableIpv6(enableIpv6); - } - - async setBlockWhenDisconnected(blockWhenDisconnected: boolean) { - const actions = this._reduxActions; - await IpcRendererEventChannel.settings.setBlockWhenDisconnected(blockWhenDisconnected); - actions.settings.updateBlockWhenDisconnected(blockWhenDisconnected); - } - - async setOpenVpnMssfix(mssfix?: number) { - const actions = this._reduxActions; - actions.settings.updateOpenVpnMssfix(mssfix); - await IpcRendererEventChannel.settings.setOpenVpnMssfix(mssfix); - } - - async setAutoConnect(autoConnect: boolean) { - return IpcRendererEventChannel.guiSettings.setAutoConnect(autoConnect); - } - - async setAutoStart(autoStart: boolean): Promise<void> { - this._setAutoStart(autoStart); - - return IpcRendererEventChannel.autoStart.set(autoStart); + this.reduxActions.account.updateAccountHistory(accountHistory); } - setStartMinimized(startMinimized: boolean) { - IpcRendererEventChannel.guiSettings.setStartMinimized(startMinimized); - } - - setMonochromaticIcon(monochromaticIcon: boolean) { - IpcRendererEventChannel.guiSettings.setMonochromaticIcon(monochromaticIcon); - } - - async _onDaemonConnected() { - this._connectedToDaemon = true; + private async onDaemonConnected() { + this.connectedToDaemon = true; try { - await this._fetchAccountHistory(); + await this.fetchAccountHistory(); } catch (error) { log.error(`Cannot fetch the account history: ${error.message}`); } - if (this._settings.accountToken) { - this._memoryHistory.replace('/connect'); + if (this.settings.accountToken) { + this.memoryHistory.replace('/connect'); // try to autoconnect the tunnel - await this._autoConnect(); + await this.autoConnect(); } else { - this._memoryHistory.replace('/login'); + this.memoryHistory.replace('/login'); // show window when account is not set ipcRenderer.send('show-window'); } } - _onDaemonDisconnected(error?: Error) { - const wasConnected = this._connectedToDaemon; + private onDaemonDisconnected(error?: Error) { + const wasConnected = this.connectedToDaemon; - this._connectedToDaemon = false; + this.connectedToDaemon = false; if (error && wasConnected) { - this._memoryHistory.replace('/'); + this.memoryHistory.replace('/'); } } - async _autoConnect() { + private async autoConnect() { if (process.env.NODE_ENV === 'development') { log.info('Skip autoconnect in development'); - } else if (this._autoConnected) { + } else if (this.autoConnected) { log.info('Skip autoconnect because it was done before'); - } else if (this._settings.accountToken) { - if (this._guiSettings.autoConnect) { + } else if (this.settings.accountToken) { + if (this.guiSettings.autoConnect) { try { log.info('Autoconnect the tunnel'); await this.connectTunnel(); - this._autoConnected = true; + this.autoConnected = true; } catch (error) { log.error(`Failed to autoconnect the tunnel: ${error.message}`); } @@ -419,12 +415,12 @@ export default class AppRenderer { } } - _setTunnelState(tunnelState: TunnelStateTransition) { - const actions = this._reduxActions; + private setTunnelState(tunnelState: TunnelStateTransition) { + const actions = this.reduxActions; log.debug(`Tunnel state: ${tunnelState.state}`); - this._tunnelState = tunnelState; + this.tunnelState = tunnelState; switch (tunnelState.state) { case 'connecting': @@ -449,18 +445,18 @@ export default class AppRenderer { } } - _setSettings(newSettings: Settings) { - this._settings = newSettings; + private setSettings(newSettings: ISettings) { + this.settings = newSettings; - const reduxSettings = this._reduxActions.settings; - const reduxAccount = this._reduxActions.account; + const reduxSettings = this.reduxActions.settings; + const reduxAccount = this.reduxActions.account; reduxSettings.updateAllowLan(newSettings.allowLan); reduxSettings.updateEnableIpv6(newSettings.tunnelOptions.generic.enableIpv6); reduxSettings.updateBlockWhenDisconnected(newSettings.blockWhenDisconnected); reduxSettings.updateOpenVpnMssfix(newSettings.tunnelOptions.openvpn.mssfix); - this._setRelaySettings(newSettings.relaySettings); + this.setRelaySettings(newSettings.relaySettings); if (newSettings.accountToken) { reduxAccount.updateAccountToken(newSettings.accountToken); @@ -470,33 +466,33 @@ export default class AppRenderer { } } - _handleAccountChange(oldAccount?: string, newAccount?: string) { + private handleAccountChange(oldAccount?: string, newAccount?: string) { if (oldAccount && !newAccount) { - this._accountDataCache.invalidate(); + this.accountDataCache.invalidate(); - if (this._loginTimer) { - clearTimeout(this._loginTimer); + if (this.loginTimer) { + clearTimeout(this.loginTimer); } - this._memoryHistory.replace('/login'); + this.memoryHistory.replace('/login'); } else if (!oldAccount && newAccount) { - this._accountDataCache.fetch(newAccount); + this.accountDataCache.fetch(newAccount); - if (!this._doingLogin) { - this._memoryHistory.replace('/connect'); + if (!this.doingLogin) { + this.memoryHistory.replace('/connect'); } } else if (oldAccount && newAccount && oldAccount !== newAccount) { - this._accountDataCache.fetch(newAccount); + this.accountDataCache.fetch(newAccount); } - this._doingLogin = false; + this.doingLogin = false; } - _setLocation(location: Location) { - this._reduxActions.connection.newLocation(location); + private setLocation(location: ILocation) { + this.reduxActions.connection.newLocation(location); } - _setRelays(relayList: RelayList) { + private setRelays(relayList: IRelayList) { const locations = relayList.countries.map((country) => ({ name: country.name, code: country.code, @@ -511,143 +507,137 @@ export default class AppRenderer { })), })); - this._reduxActions.settings.updateRelayLocations(locations); + this.reduxActions.settings.updateRelayLocations(locations); } - _setCurrentVersion(versionInfo: CurrentAppVersionInfo) { - this._reduxActions.version.updateVersion(versionInfo.gui, versionInfo.isConsistent); + private setCurrentVersion(versionInfo: ICurrentAppVersionInfo) { + this.reduxActions.version.updateVersion(versionInfo.gui, versionInfo.isConsistent); } - _setUpgradeVersion(upgradeVersion: AppUpgradeInfo) { - this._reduxActions.version.updateLatest(upgradeVersion); + private setUpgradeVersion(upgradeVersion: IAppUpgradeInfo) { + this.reduxActions.version.updateLatest(upgradeVersion); } - _setGuiSettings(guiSettings: GuiSettingsState) { - this._guiSettings = guiSettings; - this._reduxActions.settings.updateGuiSettings(guiSettings); + private setGuiSettings(guiSettings: IGuiSettingsState) { + this.guiSettings = guiSettings; + this.reduxActions.settings.updateGuiSettings(guiSettings); } - _setAutoStart(autoStart: boolean) { - this._reduxActions.settings.updateAutoStart(autoStart); + private storeAutoStart(autoStart: boolean) { + this.reduxActions.settings.updateAutoStart(autoStart); } } type AccountVerification = { status: 'verified' } | { status: 'deferred'; error: Error }; type AccountFetchRetryAction = 'stop' | 'retry'; -type AccountFetchWatcher = { +interface IAccountFetchWatcher { onFinish: () => void; onError: (error: Error) => AccountFetchRetryAction; -}; +} // An account data cache that helps to throttle RPC requests to get_account_data and retain the // cached value for 1 minute. export class AccountDataCache { - _currentAccount?: AccountToken; - _expiresAt?: Date; - _fetchAttempt: number; - _fetchRetryTimeout?: NodeJS.Timeout; - _fetch: (token: AccountToken) => Promise<AccountData>; - _update: (data?: AccountData) => void; - _watchers: Array<AccountFetchWatcher>; + private currentAccount?: AccountToken; + private expiresAt?: Date; + private fetchAttempt = 0; + private fetchRetryTimeout?: NodeJS.Timeout; + private watchers: IAccountFetchWatcher[] = []; constructor( - fetch: (token: AccountToken) => Promise<AccountData>, - update: (data?: AccountData) => void, - ) { - this._fetch = fetch; - this._update = update; - this._watchers = []; - this._fetchAttempt = 0; - } + private fetchHandler: (token: AccountToken) => Promise<IAccountData>, + private updateHandler: (data?: IAccountData) => void, + ) {} - fetch(accountToken: AccountToken, watcher?: AccountFetchWatcher) { + public fetch(accountToken: AccountToken, watcher?: IAccountFetchWatcher) { // invalidate cache if account token has changed - if (accountToken !== this._currentAccount) { + if (accountToken !== this.currentAccount) { this.invalidate(); - this._currentAccount = accountToken; + this.currentAccount = accountToken; } // Only fetch is value has expired - if (this._isExpired()) { + if (this.isExpired()) { if (watcher) { - this._watchers.push(watcher); + this.watchers.push(watcher); } - this._performFetch(accountToken); + this.performFetch(accountToken); } else if (watcher) { watcher.onFinish(); } } - invalidate() { - if (this._fetchRetryTimeout) { - clearTimeout(this._fetchRetryTimeout); - this._fetchRetryTimeout = undefined; - this._fetchAttempt = 0; + public invalidate() { + if (this.fetchRetryTimeout) { + clearTimeout(this.fetchRetryTimeout); + this.fetchRetryTimeout = undefined; + this.fetchAttempt = 0; } - this._expiresAt = undefined; - this._update(); - this._notifyWatchers((watcher) => { + this.expiresAt = undefined; + this.updateHandler(); + this.notifyWatchers((watcher) => { watcher.onError(new Error('Cancelled')); }); } - _setValue(value: AccountData) { - this._expiresAt = new Date(Date.now() + 60 * 1000); // 60s expiration - this._update(value); - this._notifyWatchers((watcher) => watcher.onFinish()); + private setValue(value: IAccountData) { + this.expiresAt = new Date(Date.now() + 60 * 1000); // 60s expiration + this.updateHandler(value); + this.notifyWatchers((watcher) => watcher.onFinish()); } - _isExpired() { - return !this._expiresAt || this._expiresAt < new Date(); + private isExpired() { + return !this.expiresAt || this.expiresAt < new Date(); } - async _performFetch(accountToken: AccountToken) { + private async performFetch(accountToken: AccountToken) { try { // it's possible for invalidate() to be called or for a fetch for a different account token // to start before this fetch completes, so checking if the current account token is the one // used is necessary below. - const accountData = await this._fetch(accountToken); + const accountData = await this.fetchHandler(accountToken); - if (this._currentAccount === accountToken) { - this._setValue(accountData); + if (this.currentAccount === accountToken) { + this.setValue(accountData); } } catch (error) { - if (this._currentAccount === accountToken) { - this._handleFetchError(accountToken, error); + if (this.currentAccount === accountToken) { + this.handleFetchError(accountToken, error); } } } - _handleFetchError(accountToken: AccountToken, error: any) { + private handleFetchError(accountToken: AccountToken, error: any) { let shouldRetry = true; - this._notifyWatchers((watcher) => { + this.notifyWatchers((watcher) => { if (watcher.onError(error) === 'stop') { shouldRetry = false; } }); if (shouldRetry) { - this._scheduleRetry(accountToken); + this.scheduleRetry(accountToken); } } - _scheduleRetry(accountToken: AccountToken) { - this._fetchAttempt += 1; + private scheduleRetry(accountToken: AccountToken) { + this.fetchAttempt += 1; - const delay = Math.min(2048, 1 << (this._fetchAttempt + 2)) * 1000; + // tslint:disable-next-line + const delay = Math.min(2048, 1 << (this.fetchAttempt + 2)) * 1000; log.warn(`Failed to fetch account data. Retrying in ${delay} ms`); - this._fetchRetryTimeout = setTimeout(() => { - this._fetchRetryTimeout = undefined; - this._performFetch(accountToken); + this.fetchRetryTimeout = setTimeout(() => { + this.fetchRetryTimeout = undefined; + this.performFetch(accountToken); }, delay); } - _notifyWatchers(notify: (watcher: AccountFetchWatcher) => void) { - this._watchers.splice(0).forEach(notify); + private notifyWatchers(notify: (watcher: IAccountFetchWatcher) => void) { + this.watchers.splice(0).forEach(notify); } } diff --git a/gui/packages/desktop/src/renderer/components/Account.tsx b/gui/packages/desktop/src/renderer/components/Account.tsx index 8ff36aafbe..e49bfc0a61 100644 --- a/gui/packages/desktop/src/renderer/components/Account.tsx +++ b/gui/packages/desktop/src/renderer/components/Account.tsx @@ -1,15 +1,15 @@ +import { ClipboardLabel, HeaderTitle, SettingsHeader } from '@mullvad/components'; import moment from 'moment'; import * as React from 'react'; import { Component, Text, View } from 'reactxp'; -import { ClipboardLabel, SettingsHeader, HeaderTitle } from '@mullvad/components'; -import * as AppButton from './AppButton'; -import { Layout, Container } from './Layout'; -import { NavigationBar, BackBarItem } from './NavigationBar'; import styles from './AccountStyles'; +import * as AppButton from './AppButton'; +import { Container, Layout } from './Layout'; +import { BackBarItem, NavigationBar } from './NavigationBar'; import { AccountToken } from '../../shared/daemon-rpc-types'; -type Props = { +interface IProps { accountToken?: AccountToken; accountExpiry?: string; expiryLocale: string; @@ -17,10 +17,10 @@ type Props = { onLogout: () => void; onClose: () => void; onBuyMore: () => void; -}; +} -export default class Account extends Component<Props> { - render() { +export default class Account extends Component<IProps> { + public render() { return ( <Layout> <Container> @@ -75,7 +75,7 @@ export default class Account extends Component<Props> { } } -const FormattedAccountExpiry = (props: { expiry?: string; locale: string }) => { +function FormattedAccountExpiry(props: { expiry?: string; locale: string }) { if (!props.expiry) { return <Text style={styles.account__row_value}>{'Currently unavailable'}</Text>; } @@ -99,4 +99,4 @@ const FormattedAccountExpiry = (props: { expiry?: string; locale: string }) => { {expiry.toDate().toLocaleString(props.locale, formatOptions)} </Text> ); -}; +} diff --git a/gui/packages/desktop/src/renderer/components/AdvancedSettings.tsx b/gui/packages/desktop/src/renderer/components/AdvancedSettings.tsx index 99632a35f5..c8a2b67c93 100644 --- a/gui/packages/desktop/src/renderer/components/AdvancedSettings.tsx +++ b/gui/packages/desktop/src/renderer/components/AdvancedSettings.tsx @@ -1,23 +1,26 @@ +/* tslint:disable:jsx-no-lambda */ +// TODO: Refactor this file to fix the jsx-no-lambda warnings + +import { HeaderTitle, ImageView, SettingsHeader } from '@mullvad/components'; import * as React from 'react'; import { Button, Component, Text, View } from 'reactxp'; -import { ImageView, SettingsHeader, HeaderTitle } from '@mullvad/components'; +import { colors } from '../../config.json'; +import styles from './AdvancedSettingsStyles'; import * as Cell from './Cell'; -import { Layout, Container } from './Layout'; +import { Container, Layout } from './Layout'; import { + BackBarItem, NavigationBar, NavigationContainer, NavigationScrollbars, - BackBarItem, TitleBarItem, } from './NavigationBar'; import Switch from './Switch'; -import styles from './AdvancedSettingsStyles'; -import { colors } from '../../config.json'; const MIN_MSSFIX_VALUE = 1000; const MAX_MSSFIX_VALUE = 1450; -type Props = { +interface IProps { enableIpv6: boolean; blockWhenDisconnected: boolean; protocol: string; @@ -28,16 +31,16 @@ type Props = { setOpenVpnMssfix: (value: number | undefined) => void; onUpdate: (protocol: string, port: string | number) => void; onClose: () => void; -}; +} -type State = { +interface IState { persistedMssfix?: number; editedMssfix?: number; focusOnMssfix: boolean; -}; +} -export class AdvancedSettings extends Component<Props, State> { - constructor(props: Props) { +export class AdvancedSettings extends Component<IProps, IState> { + constructor(props: IProps) { super(props); this.state = { @@ -47,7 +50,7 @@ export class AdvancedSettings extends Component<Props, State> { }; } - componentDidUpdate(_oldProps: Props, _oldState: State) { + public componentDidUpdate(_oldProps: IProps, _oldState: IState) { if (this.props.mssfix !== this.state.persistedMssfix) { this.setState((state, props) => ({ ...state, @@ -57,22 +60,19 @@ export class AdvancedSettings extends Component<Props, State> { } } - render() { + public render() { let portSelector = null; let protocol = this.props.protocol.toUpperCase(); if (protocol === 'AUTOMATIC') { protocol = 'Automatic'; } else { - portSelector = this._createPortSelector(); + portSelector = this.createPortSelector(); } - let mssfixStyle; - if (this._mssfixIsValid()) { - mssfixStyle = styles.advanced_settings__mssfix_valid_value; - } else { - mssfixStyle = styles.advanced_settings__mssfix_invalid_value; - } + const mssfixStyle = this.mssfixIsValid() + ? styles.advanced_settings__mssfix_valid_value + : styles.advanced_settings__mssfix_invalid_value; const mssfixValue = this.state.editedMssfix; @@ -118,8 +118,8 @@ export class AdvancedSettings extends Component<Props, State> { title={'Network protocols'} values={['Automatic', 'UDP', 'TCP']} value={protocol} - onSelect={(protocol) => { - this.props.onUpdate(protocol, 'Automatic'); + onSelect={(selectedProtocol) => { + this.props.onUpdate(selectedProtocol, 'Automatic'); }} /> @@ -137,9 +137,9 @@ export class AdvancedSettings extends Component<Props, State> { placeholder={'Default'} value={mssfixValue ? mssfixValue.toString() : ''} style={mssfixStyle} - onChangeText={this._onMssfixChange} - onFocus={this._onMssfixFocus} - onBlur={this._onMssfixBlur} + onChangeText={this.onMssfixChange} + onFocus={this.onMssfixFocus} + onBlur={this.onMssfixBlur} /> </Cell.InputFrame> </Cell.Container> @@ -155,7 +155,7 @@ export class AdvancedSettings extends Component<Props, State> { ); } - _createPortSelector() { + private createPortSelector() { const protocol = this.props.protocol.toUpperCase(); const ports = protocol === 'TCP' @@ -174,7 +174,7 @@ export class AdvancedSettings extends Component<Props, State> { ); } - _onMssfixChange = (mssfixString: string) => { + private onMssfixChange = (mssfixString: string) => { const mssfix = mssfixString.replace(/[^0-9]/g, ''); if (mssfix === '') { @@ -184,64 +184,64 @@ export class AdvancedSettings extends Component<Props, State> { } }; - _onMssfixFocus = () => { + private onMssfixFocus = () => { this.setState({ focusOnMssfix: true }); }; - _onMssfixBlur = () => { + private onMssfixBlur = () => { this.setState({ focusOnMssfix: false }); - if (this._mssfixIsValid()) { + if (this.mssfixIsValid()) { this.props.setOpenVpnMssfix(this.state.editedMssfix); this.setState((state, _props) => ({ persistedMssfix: state.editedMssfix })); } }; - _mssfixIsValid(): boolean { + private mssfixIsValid(): boolean { const mssfix = this.state.editedMssfix; return mssfix === undefined || (mssfix >= MIN_MSSFIX_VALUE && mssfix <= MAX_MSSFIX_VALUE); } } -type SelectorProps<T> = { +interface ISelectorProps<T> { title: string; - values: Array<T>; + values: T[]; value: T; onSelect: (value: T) => void; -}; +} -type SelectorState<T> = { +interface ISelectorState<T> { hoveredButtonValue?: T; -}; +} -class Selector<T> extends Component<SelectorProps<T>, SelectorState<T>> { - state = { hoveredButtonValue: undefined }; +class Selector<T> extends Component<ISelectorProps<T>, ISelectorState<T>> { + public state: ISelectorState<T> = {}; - handleButtonHover = (value?: T) => { - this.setState({ hoveredButtonValue: value }); - }; - - render() { + public render() { return ( <View> <View style={styles.advanced_settings__section_title}>{this.props.title}</View> - {this.props.values.map((value) => this._renderCell(value))} + {this.props.values.map((value) => this.renderCell(value))} </View> ); } - _renderCell(value: T) { + private handleButtonHover = (value?: T) => { + this.setState({ hoveredButtonValue: value }); + }; + + private renderCell(value: T) { const selected = value === this.props.value; if (selected) { - return this._renderSelectedCell(value); + return this.renderSelectedCell(value); } else { - return this._renderUnselectedCell(value); + return this.renderUnselectedCell(value); } } - _renderSelectedCell(value: T) { + private renderSelectedCell(value: T) { return ( <Button style={[ @@ -264,7 +264,7 @@ class Selector<T> extends Component<SelectorProps<T>, SelectorState<T>> { ); } - _renderUnselectedCell(value: T) { + private renderUnselectedCell(value: T) { return ( <Button style={[ diff --git a/gui/packages/desktop/src/renderer/components/AppButton.tsx b/gui/packages/desktop/src/renderer/components/AppButton.tsx index 31fc9bd679..42e2205b77 100644 --- a/gui/packages/desktop/src/renderer/components/AppButton.tsx +++ b/gui/packages/desktop/src/renderer/components/AppButton.tsx @@ -1,27 +1,27 @@ +import { ImageView } from '@mullvad/components'; import * as React from 'react'; import { Button, Component, Text, Types } from 'reactxp'; -import { ImageView } from '@mullvad/components'; -import styles from './AppButtonStyles'; import { colors } from '../../config.json'; +import styles from './AppButtonStyles'; -type LabelProps = { +interface ILabelProps { children?: React.ReactText; -}; +} -export class Label extends Component<LabelProps> { - render() { +export class Label extends Component<ILabelProps> { + public render() { return <Text style={styles.label}>{this.props.children}</Text>; } } -type IconProps = { +interface IIconProps { source: string; width?: number; height?: number; -}; +} -export class Icon extends Component<IconProps> { - render() { +export class Icon extends Component<IIconProps> { + public render() { return ( <ImageView source={this.props.source} @@ -34,27 +34,27 @@ export class Icon extends Component<IconProps> { } } -type Props = { +interface IProps { children?: React.ReactNode; style?: Types.ButtonStyleRuleSet; disabled?: boolean; onPress?: () => void; -}; +} -type State = { +interface IState { hovered: boolean; -}; +} -class BaseButton extends Component<Props, State> { - state: State = { hovered: false }; +class BaseButton extends Component<IProps, IState> { + public state: IState = { hovered: false }; - backgroundStyle = (): Types.ButtonStyleRuleSet => { + public backgroundStyle = (): Types.ButtonStyleRuleSet => { throw new Error('Implement backgroundStyle in subclasses.'); }; - onHoverStart = () => (!this.props.disabled ? this.setState({ hovered: true }) : null); - onHoverEnd = () => (!this.props.disabled ? this.setState({ hovered: false }) : null); + public onHoverStart = () => (!this.props.disabled ? this.setState({ hovered: true }) : null); + public onHoverEnd = () => (!this.props.disabled ? this.setState({ hovered: false }) : null); - render() { + public render() { const { children, style, ...otherProps } = this.props; return ( @@ -72,21 +72,23 @@ class BaseButton extends Component<Props, State> { } export class RedButton extends BaseButton { - backgroundStyle = () => (this.state.hovered ? styles.redHover : styles.red); + public backgroundStyle = () => (this.state.hovered ? styles.redHover : styles.red); } export class GreenButton extends BaseButton { - backgroundStyle = () => (this.state.hovered ? styles.greenHover : styles.green); + public backgroundStyle = () => (this.state.hovered ? styles.greenHover : styles.green); } export class BlueButton extends BaseButton { - backgroundStyle = () => (this.state.hovered ? styles.blueHover : styles.blue); + public backgroundStyle = () => (this.state.hovered ? styles.blueHover : styles.blue); } export class TransparentButton extends BaseButton { - backgroundStyle = () => (this.state.hovered ? styles.transparentHover : styles.transparent); + public backgroundStyle = () => + this.state.hovered ? styles.transparentHover : styles.transparent; } export class RedTransparentButton extends BaseButton { - backgroundStyle = () => (this.state.hovered ? styles.redTransparentHover : styles.redTransparent); + public backgroundStyle = () => + this.state.hovered ? styles.redTransparentHover : styles.redTransparent; } diff --git a/gui/packages/desktop/src/renderer/components/Cell.tsx b/gui/packages/desktop/src/renderer/components/Cell.tsx index 291b891e64..c2dedc9dfc 100644 --- a/gui/packages/desktop/src/renderer/components/Cell.tsx +++ b/gui/packages/desktop/src/renderer/components/Cell.tsx @@ -1,6 +1,6 @@ +import { ImageView } from '@mullvad/components'; import * as React from 'react'; import { Button, Component, Styles, Text, TextInput, Types, View } from 'reactxp'; -import { ImageView } from '@mullvad/components'; import { colors } from '../../config.json'; const styles = { @@ -96,25 +96,27 @@ const styles = { }), }; -type CellButtonProps = { +interface ICellButtonProps { children?: React.ReactNode; disabled?: boolean; cellHoverStyle?: Types.StyleRuleSetRecursive<Types.ButtonStyleRuleSet>; style?: Types.StyleRuleSetRecursive<Types.ButtonStyleRuleSet>; onPress?: () => void; -}; +} -type State = { hovered: boolean }; +interface IState { + hovered: boolean; +} const CellHoverContext = React.createContext<boolean>(false); -export class CellButton extends Component<CellButtonProps, State> { - state = { hovered: false }; +export class CellButton extends Component<ICellButtonProps, IState> { + public state = { hovered: false }; - onHoverStart = () => (!this.props.disabled ? this.setState({ hovered: true }) : null); - onHoverEnd = () => (!this.props.disabled ? this.setState({ hovered: false }) : null); + public onHoverStart = () => (!this.props.disabled ? this.setState({ hovered: true }) : null); + public onHoverEnd = () => (!this.props.disabled ? this.setState({ hovered: false }) : null); - render() { + public render() { const { children, style, cellHoverStyle, ...otherProps } = this.props; const hoverStyle = cellHoverStyle || styles.cellHover; return ( @@ -129,22 +131,24 @@ export class CellButton extends Component<CellButtonProps, State> { } } -type ContainerProps = { children: React.ReactNode }; +interface IContainerProps { + children: React.ReactNode; +} -export function Container({ children }: ContainerProps) { +export function Container({ children }: IContainerProps) { return <View style={styles.cellContainer}>{children}</View>; } -type LabelProps = { +interface ILabelProps { containerStyle?: Types.ViewStyleRuleSet; textStyle?: Types.TextStyleRuleSet; cellHoverContainerStyle?: Types.ViewStyleRuleSet; cellHoverTextStyle?: Types.TextStyleRuleSet; onPress?: (event: Types.SyntheticEvent) => void; children?: React.ReactNode; -}; +} -export function Label(props: LabelProps) { +export function Label(props: ILabelProps) { const { children, containerStyle, @@ -173,10 +177,10 @@ export function Label(props: LabelProps) { ); } -type InputFrameProps = { +interface InputFrameProps { children?: React.ReactNode; style?: Types.StyleRuleSetRecursive<Types.ViewStyleRuleSet>; -}; +} export function InputFrame(props: InputFrameProps) { const { style, children } = props; @@ -237,7 +241,7 @@ export function Icon(props: ImageView['props']) { ); } -export function Footer({ children }: ContainerProps) { +export function Footer({ children }: IContainerProps) { return ( <View style={styles.footer.container}> <Text style={styles.footer.text}>{children}</Text> diff --git a/gui/packages/desktop/src/renderer/components/ChevronButton.tsx b/gui/packages/desktop/src/renderer/components/ChevronButton.tsx index efb7c03fdd..04aabf6a99 100644 --- a/gui/packages/desktop/src/renderer/components/ChevronButton.tsx +++ b/gui/packages/desktop/src/renderer/components/ChevronButton.tsx @@ -1,13 +1,13 @@ import * as React from 'react'; import { Component, Styles, Types } from 'reactxp'; -import * as Cell from './Cell'; import { colors } from '../../config.json'; +import * as Cell from './Cell'; -type Props = { +interface IProps { up: boolean; onPress?: (event: Types.SyntheticEvent) => void; style?: Types.StyleRuleSetRecursive<Types.ViewStyleRuleSet>; -}; +} const style = Styles.createViewStyle({ flex: 0, @@ -17,8 +17,8 @@ const style = Styles.createViewStyle({ paddingLeft: 16, }); -export default class ChevronButton extends Component<Props> { - render() { +export default class ChevronButton extends Component<IProps> { + public render() { return ( <Cell.Icon style={[style, this.props.style]} diff --git a/gui/packages/desktop/src/renderer/components/CityRow.tsx b/gui/packages/desktop/src/renderer/components/CityRow.tsx index 4137762b16..23efa17292 100644 --- a/gui/packages/desktop/src/renderer/components/CityRow.tsx +++ b/gui/packages/desktop/src/renderer/components/CityRow.tsx @@ -1,23 +1,25 @@ +import { Accordion } from '@mullvad/components'; import * as React from 'react'; import { Component, Styles, Types, View } from 'reactxp'; -import { Accordion } from '@mullvad/components'; +import { colors } from '../../config.json'; +import { compareRelayLocation, RelayLocation } from '../../shared/daemon-rpc-types'; import * as Cell from './Cell'; +import ChevronButton from './ChevronButton'; import RelayRow from './RelayRow'; import RelayStatusIndicator from './RelayStatusIndicator'; -import ChevronButton from './ChevronButton'; -import { colors } from '../../config.json'; type RelayRowElement = React.ReactElement<RelayRow['props']>; -type Props = { +interface IProps { name: string; hasActiveRelays: boolean; + location: RelayLocation; selected: boolean; expanded: boolean; - onSelect?: () => void; - onExpand?: (value: boolean) => void; + onSelect?: (location: RelayLocation) => void; + onExpand?: (location: RelayLocation, value: boolean) => void; children?: RelayRowElement | RelayRowElement[]; -}; +} const styles = { base: Styles.createButtonStyle({ @@ -32,12 +34,8 @@ const styles = { }), }; -export default class CityRow extends Component<Props> { - shouldComponentUpdate(nextProps: Props) { - return !CityRow.compareProps(this.props, nextProps); - } - - static compareProps(oldProps: Props, nextProps: Props): boolean { +export default class CityRow extends Component<IProps> { + public static compareProps(oldProps: IProps, nextProps: IProps): boolean { if (React.Children.count(oldProps.children) !== React.Children.count(nextProps.children)) { return false; } @@ -46,7 +44,8 @@ export default class CityRow extends Component<Props> { oldProps.name !== nextProps.name || oldProps.hasActiveRelays !== nextProps.hasActiveRelays || oldProps.selected !== nextProps.selected || - oldProps.expanded !== nextProps.expanded + oldProps.expanded !== nextProps.expanded || + !compareRelayLocation(oldProps.location, nextProps.location) ) { return false; } @@ -66,13 +65,17 @@ export default class CityRow extends Component<Props> { return true; } - render() { + public shouldComponentUpdate(nextProps: IProps) { + return !CityRow.compareProps(this.props, nextProps); + } + + public render() { const hasChildren = React.Children.count(this.props.children) > 1; return ( <View> <Cell.CellButton - onPress={this._handlePress} + onPress={this.handlePress} disabled={!this.props.hasActiveRelays} cellHoverStyle={this.props.selected ? styles.selected : undefined} style={[styles.base, this.props.selected ? styles.selected : undefined]}> @@ -82,7 +85,7 @@ export default class CityRow extends Component<Props> { /> <Cell.Label>{this.props.name}</Cell.Label> - {hasChildren && <ChevronButton onPress={this._toggleCollapse} up={this.props.expanded} />} + {hasChildren && <ChevronButton onPress={this.toggleCollapse} up={this.props.expanded} />} </Cell.CellButton> {hasChildren && <Accordion expanded={this.props.expanded}>{this.props.children}</Accordion>} @@ -90,16 +93,16 @@ export default class CityRow extends Component<Props> { ); } - _toggleCollapse = (event: Types.SyntheticEvent) => { + private toggleCollapse = (event: Types.SyntheticEvent) => { if (this.props.onExpand) { - this.props.onExpand(!this.props.expanded); + this.props.onExpand(this.props.location, !this.props.expanded); } event.stopPropagation(); }; - _handlePress = () => { + private handlePress = () => { if (this.props.onSelect) { - this.props.onSelect(); + this.props.onSelect(this.props.location); } }; } diff --git a/gui/packages/desktop/src/renderer/components/Connect.tsx b/gui/packages/desktop/src/renderer/components/Connect.tsx index 5c4b883e57..6d9552cdc6 100644 --- a/gui/packages/desktop/src/renderer/components/Connect.tsx +++ b/gui/packages/desktop/src/renderer/components/Connect.tsx @@ -1,24 +1,23 @@ +import { Brand, HeaderBarStyle, ImageView, SettingsBarButton } from '@mullvad/components'; import * as React from 'react'; import { Component, View } from 'reactxp'; -import { SettingsBarButton, Brand, HeaderBarStyle, ImageView } from '@mullvad/components'; -import { Layout, Container, Header } from './Layout'; -import NotificationArea from './NotificationArea'; +import { links } from '../../config.json'; +import { NoCreditError, NoInternetError } from '../../main/errors'; +import { ITunnelEndpoint, parseSocketAddress } from '../../shared/daemon-rpc-types'; import * as AppButton from './AppButton'; -import TunnelControl from './TunnelControl'; -import Map, { MarkerStyle, ZoomLevel } from './Map'; import styles from './ConnectStyles'; -import { NoCreditError, NoInternetError } from '../../main/errors'; -import { TunnelEndpoint, parseSocketAddress } from '../../shared/daemon-rpc-types'; -import { links } from '../../config.json'; +import { Container, Header, Layout } from './Layout'; +import Map, { MarkerStyle, ZoomLevel } from './Map'; +import NotificationArea from './NotificationArea'; +import TunnelControl, { IRelayInAddress, IRelayOutAddress } from './TunnelControl'; -import { RelayOutAddress, RelayInAddress } from './TunnelControl'; import AccountExpiry from '../lib/account-expiry'; -import { ConnectionReduxState } from '../redux/connection/reducers'; -import { VersionReduxState } from '../redux/version/reducers'; +import { IConnectionReduxState } from '../redux/connection/reducers'; +import { IVersionReduxState } from '../redux/version/reducers'; -type Props = { - connection: ConnectionReduxState; - version: VersionReduxState; +interface IProps { + connection: IConnectionReduxState; + version: IVersionReduxState; accountExpiry?: AccountExpiry; selectedRelayName: string; connectionInfoOpen: boolean; @@ -29,12 +28,12 @@ type Props = { onDisconnect: () => void; onExternalLink: (url: string) => void; onToggleConnectionInfo: (value: boolean) => void; -}; +} type MarkerOrSpinner = 'marker' | 'spinner'; -export default class Connect extends Component<Props> { - render() { +export default class Connect extends Component<IProps> { + public render() { const error = this.checkForErrors(); const child = error ? this.renderError(error) : this.renderMap(); @@ -49,7 +48,7 @@ export default class Connect extends Component<Props> { ); } - renderError(error: Error) { + public renderError(error: Error) { let title = ''; let message = ''; @@ -75,9 +74,7 @@ export default class Connect extends Component<Props> { <View style={styles.error_message}>{message}</View> {error instanceof NoCreditError ? ( <View> - <AppButton.GreenButton - disabled={isBlocked} - onPress={() => this.props.onExternalLink(links.purchase)}> + <AppButton.GreenButton disabled={isBlocked} onPress={this.handleBuyMorePress}> <AppButton.Label>Buy more time</AppButton.Label> <AppButton.Icon source="icon-extLink" height={16} width={16} /> </AppButton.GreenButton> @@ -88,102 +85,23 @@ export default class Connect extends Component<Props> { ); } - _getMapProps(): Map['props'] { - const { - longitude, - latitude, - status: { state }, - } = this.props.connection; - - // when the user location is known - if (typeof longitude === 'number' && typeof latitude === 'number') { - return { - center: [longitude, latitude], - // do not show the marker when connecting or reconnecting - showMarker: this._showMarkerOrSpinner() === 'marker', - markerStyle: this._getMarkerStyle(), - // zoom in when connected - zoomLevel: state === 'connected' ? ZoomLevel.low : ZoomLevel.medium, - // a magic offset to align marker with spinner - offset: [0, 123], - }; - } else { - return { - center: [0, 0], - showMarker: false, - markerStyle: MarkerStyle.unsecure, - // show the world when user location is not known - zoomLevel: ZoomLevel.high, - // remove the offset since the marker is hidden - offset: [0, 0], - }; - } - } - - _getMarkerStyle(): MarkerStyle { - const { status } = this.props.connection; - - switch (status.state) { - case 'connecting': - case 'connected': - return MarkerStyle.secure; - case 'blocked': - switch (status.details.reason) { - case 'set_firewall_policy_error': - return MarkerStyle.unsecure; - default: - return MarkerStyle.secure; - } - case 'disconnected': - return MarkerStyle.unsecure; - case 'disconnecting': - switch (status.details) { - case 'block': - case 'reconnect': - return MarkerStyle.secure; - case 'nothing': - return MarkerStyle.unsecure; - default: - throw new Error(`Invalid action after disconnection: ${status.details}`); - } - } - } - - _showMarkerOrSpinner(): MarkerOrSpinner { + public renderMap() { const status = this.props.connection.status; - return status.state === 'connecting' || - (status.state === 'disconnecting' && status.details === 'reconnect') - ? 'spinner' - : 'marker'; - } - - _tunnelEndpointToRelayInAddress(tunnelEndpoint: TunnelEndpoint): RelayInAddress { - const socketAddr = parseSocketAddress(tunnelEndpoint.address); - return { - ip: socketAddr.host, - port: socketAddr.port, - protocol: tunnelEndpoint.protocol, - }; - } - - renderMap() { - const status = this.props.connection.status; - - const relayOutAddress: RelayOutAddress = { + const relayOutAddress: IRelayOutAddress = { ipv4: this.props.connection.ip, }; - const relayInAddress: RelayInAddress | undefined = + const relayInAddress: IRelayInAddress | undefined = (status.state === 'connecting' || status.state === 'connected') && status.details - ? this._tunnelEndpointToRelayInAddress(status.details) + ? this.tunnelEndpointToRelayInAddress(status.details) : undefined; return ( <View style={styles.connect}> - <Map style={styles.map} {...this._getMapProps()} /> + <Map style={styles.map} {...this.getMapProps()} /> <View style={styles.container}> {/* show spinner when connecting */} - {this._showMarkerOrSpinner() === 'spinner' ? ( + {this.showMarkerOrSpinner() === 'spinner' ? ( <View style={styles.status_icon}> <ImageView source="icon-spinner" height={60} width={60} /> </View> @@ -217,9 +135,11 @@ export default class Connect extends Component<Props> { ); } - // Private + private handleBuyMorePress = () => { + this.props.onExternalLink(links.purchase); + }; - headerBarStyle(): HeaderBarStyle { + private headerBarStyle(): HeaderBarStyle { const { status } = this.props.connection; switch (status.state) { case 'disconnected': @@ -247,7 +167,7 @@ export default class Connect extends Component<Props> { } } - checkForErrors(): Error | undefined { + private checkForErrors(): Error | undefined { // Offline? if (!this.props.connection.isOnline) { return new NoInternetError(); @@ -260,4 +180,83 @@ export default class Connect extends Component<Props> { return undefined; } + + private getMapProps(): Map['props'] { + const { + longitude, + latitude, + status: { state }, + } = this.props.connection; + + // when the user location is known + if (typeof longitude === 'number' && typeof latitude === 'number') { + return { + center: [longitude, latitude], + // do not show the marker when connecting or reconnecting + showMarker: this.showMarkerOrSpinner() === 'marker', + markerStyle: this.getMarkerStyle(), + // zoom in when connected + zoomLevel: state === 'connected' ? ZoomLevel.low : ZoomLevel.medium, + // a magic offset to align marker with spinner + offset: [0, 123], + }; + } else { + return { + center: [0, 0], + showMarker: false, + markerStyle: MarkerStyle.unsecure, + // show the world when user location is not known + zoomLevel: ZoomLevel.high, + // remove the offset since the marker is hidden + offset: [0, 0], + }; + } + } + + private getMarkerStyle(): MarkerStyle { + const { status } = this.props.connection; + + switch (status.state) { + case 'connecting': + case 'connected': + return MarkerStyle.secure; + case 'blocked': + switch (status.details.reason) { + case 'set_firewall_policy_error': + return MarkerStyle.unsecure; + default: + return MarkerStyle.secure; + } + case 'disconnected': + return MarkerStyle.unsecure; + case 'disconnecting': + switch (status.details) { + case 'block': + case 'reconnect': + return MarkerStyle.secure; + case 'nothing': + return MarkerStyle.unsecure; + default: + throw new Error(`Invalid action after disconnection: ${status.details}`); + } + } + } + + private showMarkerOrSpinner(): MarkerOrSpinner { + const status = this.props.connection.status; + + return status.state === 'connecting' || + (status.state === 'disconnecting' && status.details === 'reconnect') + ? 'spinner' + : 'marker'; + } + + private tunnelEndpointToRelayInAddress(tunnelEndpoint: ITunnelEndpoint): IRelayInAddress { + const socketAddr = parseSocketAddress(tunnelEndpoint.address); + return { + ip: socketAddr.host, + port: socketAddr.port, + protocol: tunnelEndpoint.protocol, + }; + } } diff --git a/gui/packages/desktop/src/renderer/components/CountryRow.tsx b/gui/packages/desktop/src/renderer/components/CountryRow.tsx index eac0c70c15..0a52cd02b4 100644 --- a/gui/packages/desktop/src/renderer/components/CountryRow.tsx +++ b/gui/packages/desktop/src/renderer/components/CountryRow.tsx @@ -1,23 +1,25 @@ +import { Accordion } from '@mullvad/components'; import * as React from 'react'; import { Component, Styles, Types, View } from 'reactxp'; -import { Accordion } from '@mullvad/components'; +import { colors } from '../../config.json'; +import { compareRelayLocation, RelayLocation } from '../../shared/daemon-rpc-types'; import * as Cell from './Cell'; +import ChevronButton from './ChevronButton'; import CityRow from './CityRow'; import RelayStatusIndicator from './RelayStatusIndicator'; -import ChevronButton from './ChevronButton'; -import { colors } from '../../config.json'; type CityRowElement = React.ReactElement<CityRow['props']>; -type Props = { +interface IProps { name: string; hasActiveRelays: boolean; + location: RelayLocation; selected: boolean; expanded: boolean; - onSelect?: () => void; - onExpand?: (value: boolean) => void; + onSelect?: (location: RelayLocation) => void; + onExpand?: (location: RelayLocation, value: boolean) => void; children?: CityRowElement | CityRowElement[]; -}; +} const styles = { container: Styles.createViewStyle({ @@ -35,12 +37,8 @@ const styles = { }), }; -export default class CountryRow extends Component<Props> { - shouldComponentUpdate(nextProps: Props) { - return !CountryRow.compareProps(this.props, nextProps); - } - - static compareProps(oldProps: Props, nextProps: Props) { +export default class CountryRow extends Component<IProps> { + public static compareProps(oldProps: IProps, nextProps: IProps) { if (React.Children.count(oldProps.children) !== React.Children.count(nextProps.children)) { return false; } @@ -49,7 +47,8 @@ export default class CountryRow extends Component<Props> { oldProps.name !== nextProps.name || oldProps.hasActiveRelays !== nextProps.hasActiveRelays || oldProps.selected !== nextProps.selected || - oldProps.expanded !== nextProps.expanded + oldProps.expanded !== nextProps.expanded || + !compareRelayLocation(oldProps.location, nextProps.location) ) { return false; } @@ -69,7 +68,11 @@ export default class CountryRow extends Component<Props> { return true; } - render() { + public shouldComponentUpdate(nextProps: IProps) { + return !CountryRow.compareProps(this.props, nextProps); + } + + public render() { const childrenArray = React.Children.toArray(this.props.children || []) as CityRowElement[]; const numChildren = childrenArray.length; const onlyChild = numChildren === 1 ? childrenArray[0] : undefined; @@ -83,7 +86,7 @@ export default class CountryRow extends Component<Props> { <Cell.CellButton cellHoverStyle={this.props.selected ? styles.selected : undefined} style={[styles.base, this.props.selected ? styles.selected : undefined]} - onPress={this._handlePress} + onPress={this.handlePress} disabled={!this.props.hasActiveRelays}> <RelayStatusIndicator isActive={this.props.hasActiveRelays} @@ -91,7 +94,7 @@ export default class CountryRow extends Component<Props> { /> <Cell.Label>{this.props.name}</Cell.Label> {hasChildren ? ( - <ChevronButton onPress={this._toggleCollapse} up={this.props.expanded} /> + <ChevronButton onPress={this.toggleCollapse} up={this.props.expanded} /> ) : null} </Cell.CellButton> @@ -100,16 +103,16 @@ export default class CountryRow extends Component<Props> { ); } - _toggleCollapse = (event: Types.SyntheticEvent) => { + private toggleCollapse = (event: Types.SyntheticEvent) => { if (this.props.onExpand) { - this.props.onExpand(!this.props.expanded); + this.props.onExpand(this.props.location, !this.props.expanded); } event.stopPropagation(); }; - _handlePress = () => { + private handlePress = () => { if (this.props.onSelect) { - this.props.onSelect(); + this.props.onSelect(this.props.location); } }; } diff --git a/gui/packages/desktop/src/renderer/components/CustomScrollbars.tsx b/gui/packages/desktop/src/renderer/components/CustomScrollbars.tsx index 3e2ccef42f..c97c37341d 100644 --- a/gui/packages/desktop/src/renderer/components/CustomScrollbars.tsx +++ b/gui/packages/desktop/src/renderer/components/CustomScrollbars.tsx @@ -2,15 +2,15 @@ import * as React from 'react'; const AUTOHIDE_TIMEOUT = 1000; -type Props = { +interface IProps { autoHide: boolean; trackPadding: { x: number; y: number }; - onScroll?: (value: ScrollEvent) => void; + onScroll?: (value: IScrollEvent) => void; style?: React.CSSProperties; children?: React.ReactNode; -}; +} -type State = { +interface IState { canScroll: boolean; showScrollIndicators: boolean; showTrack: boolean; @@ -21,24 +21,27 @@ type State = { y: number; }; isWide: boolean; -}; +} -export type ScrollEvent = { scrollLeft: number; scrollTop: number }; +export interface IScrollEvent { + scrollLeft: number; + scrollTop: number; +} export type ScrollPosition = 'top' | 'bottom' | 'middle'; -type ScrollbarUpdateContext = { +interface IScrollbarUpdateContext { size: boolean; position: boolean; -}; +} -export default class CustomScrollbars extends React.Component<Props, State> { - static defaultProps: Props = { +export default class CustomScrollbars extends React.Component<IProps, IState> { + public static defaultProps: IProps = { // auto-hide on macOS by default autoHide: process.platform === 'darwin', trackPadding: { x: 2, y: 2 }, }; - state = { + public state = { canScroll: false, showScrollIndicators: true, showTrack: false, @@ -48,21 +51,21 @@ export default class CustomScrollbars extends React.Component<Props, State> { isWide: false, }; - _scrollableRef = React.createRef<HTMLDivElement>(); - _trackRef = React.createRef<HTMLDivElement>(); - _thumbRef = React.createRef<HTMLDivElement>(); - _autoHideTimer?: NodeJS.Timeout; + private scrollableRef = React.createRef<HTMLDivElement>(); + private trackRef = React.createRef<HTMLDivElement>(); + private thumbRef = React.createRef<HTMLDivElement>(); + private autoHideTimer?: NodeJS.Timeout; - scrollTo(x: number, y: number) { - const scrollable = this._scrollableRef.current; + public scrollTo(x: number, y: number) { + const scrollable = this.scrollableRef.current; if (scrollable) { scrollable.scrollLeft = x; scrollable.scrollTop = y; } } - scrollToElement(child: HTMLElement, scrollPosition: ScrollPosition) { - const scrollable = this._scrollableRef.current; + public scrollToElement(child: HTMLElement, scrollPosition: ScrollPosition) { + const scrollable = this.scrollableRef.current; if (scrollable) { // throw if child is not a descendant of scroll view if (!scrollable.contains(child)) { @@ -71,13 +74,13 @@ export default class CustomScrollbars extends React.Component<Props, State> { ); } - const scrollTop = this._computeScrollTop(scrollable, child, scrollPosition); + const scrollTop = this.computeScrollTop(scrollable, child, scrollPosition); this.scrollTo(0, scrollTop); } } - componentDidMount() { - this._updateScrollbarsHelper({ + public componentDidMount() { + this.updateScrollbarsHelper({ position: true, size: true, }); @@ -88,11 +91,11 @@ export default class CustomScrollbars extends React.Component<Props, State> { // show scroll indicators briefly when mounted if (this.props.autoHide) { - this._startAutoHide(); + this.startAutoHide(); } } - shouldComponentUpdate(nextProps: Props, nextState: State) { + public shouldComponentUpdate(nextProps: IProps, nextState: IState) { const prevProps = this.props; const prevState = this.state; @@ -110,23 +113,84 @@ export default class CustomScrollbars extends React.Component<Props, State> { ); } - componentWillUnmount() { - this._stopAutoHide(); + public componentWillUnmount() { + this.stopAutoHide(); document.removeEventListener('mousemove', this.handleMouseMove); document.removeEventListener('mouseup', this.handleMouseUp); document.removeEventListener('mousedown', this.handleMouseDown); } - componentDidUpdate() { - this._updateScrollbarsHelper({ + public componentDidUpdate() { + this.updateScrollbarsHelper({ position: true, size: true, }); } - handleEnterTrack = () => { - this._stopAutoHide(); + public render() { + const { + autoHide: _autoHide, + trackPadding: _trackPadding, + onScroll: _onScroll, + children, + ...otherProps + } = this.props; + const showScrollbars = this.state.canScroll && this.state.showScrollIndicators; + const thumbAnimationClass = showScrollbars ? ' custom-scrollbars__thumb--visible' : ''; + const thumbActiveClass = + this.state.isTrackHovered || this.state.isDragging ? ' custom-scrollbars__thumb--active' : ''; + const thumbWideClass = this.state.isWide ? ' custom-scrollbars__thumb--wide' : ''; + const trackClass = + showScrollbars && this.state.showTrack ? ' custom-scrollbars__track--visible' : ''; + + return ( + <div {...otherProps} className="custom-scrollbars"> + <div className={`custom-scrollbars__track ${trackClass}`} ref={this.trackRef} /> + <div + className={`custom-scrollbars__thumb ${thumbWideClass} ${thumbActiveClass} ${thumbAnimationClass}`} + style={{ position: 'absolute', top: 0, right: 0 }} + ref={this.thumbRef} + /> + <div + className="custom-scrollbars__scrollable" + style={{ overflow: 'auto' }} + onScroll={this.onScroll} + ref={this.scrollableRef}> + {children} + </div> + </div> + ); + } + + private onScroll = () => { + this.updateScrollbarsHelper({ position: true }); + + if (this.props.autoHide) { + this.ensureScrollbarsVisible(); + + // only auto-hide when scrolling with mousewheel + if (!this.state.isDragging) { + this.startAutoHide(); + } + } else { + // only auto-shrink when scrolling with mousewheel + if (!this.state.isDragging) { + this.startAutoShrink(); + } + } + + const scrollView = this.scrollableRef.current; + if (scrollView && this.props.onScroll) { + this.props.onScroll({ + scrollLeft: scrollView.scrollLeft, + scrollTop: scrollView.scrollTop, + }); + } + }; + + private handleEnterTrack = () => { + this.stopAutoHide(); this.setState({ isTrackHovered: true, showScrollIndicators: true, @@ -135,7 +199,7 @@ export default class CustomScrollbars extends React.Component<Props, State> { }); }; - handleLeaveTrack = () => { + private handleLeaveTrack = () => { this.setState({ isTrackHovered: false, }); @@ -143,30 +207,30 @@ export default class CustomScrollbars extends React.Component<Props, State> { // do not hide the scrollbar if user is dragging a thumb but left the track area. if (!this.state.isDragging) { if (this.props.autoHide) { - this._startAutoHide(); + this.startAutoHide(); } else { - this._startAutoShrink(); + this.startAutoShrink(); } } }; - handleMouseDown = (event: MouseEvent) => { - const thumb = this._thumbRef.current; + private handleMouseDown = (event: MouseEvent) => { + const thumb = this.thumbRef.current; const cursorPosition = { x: event.clientX, y: event.clientY, }; // initiate dragging when user clicked inside of thumb - if (thumb && this._isPointInsideOfElement(thumb, cursorPosition)) { + if (thumb && this.isPointInsideOfElement(thumb, cursorPosition)) { this.setState({ isDragging: true, - dragStart: this._getPointRelativeToElement(thumb, cursorPosition), + dragStart: this.getPointRelativeToElement(thumb, cursorPosition), }); } }; - handleMouseUp = (event: MouseEvent) => { + private handleMouseUp = (event: MouseEvent) => { if (!this.state.isDragging) { return; } @@ -175,7 +239,7 @@ export default class CustomScrollbars extends React.Component<Props, State> { isDragging: false, }); - const track = this._trackRef.current; + const track = this.trackRef.current; if (track) { // Make sure to auto-hide the scrollbar if cursor ended up outside of scroll track const cursorPosition = { @@ -183,20 +247,20 @@ export default class CustomScrollbars extends React.Component<Props, State> { y: event.clientY, }; - if (!this._isPointInsideOfElement(track, cursorPosition)) { + if (!this.isPointInsideOfElement(track, cursorPosition)) { if (this.props.autoHide) { - this._startAutoHide(); + this.startAutoHide(); } else { - this._startAutoShrink(); + this.startAutoShrink(); } } } }; - handleMouseMove = (event: MouseEvent) => { - const scrollable = this._scrollableRef.current; - const thumb = this._thumbRef.current; - const track = this._trackRef.current; + private handleMouseMove = (event: MouseEvent) => { + const scrollable = this.scrollableRef.current; + const thumb = this.thumbRef.current; + const track = this.trackRef.current; const cursorPosition = { x: event.clientX, @@ -214,11 +278,11 @@ export default class CustomScrollbars extends React.Component<Props, State> { const maxScrollTop = scrollHeight - visibleHeight; // Map absolute cursor coordinate to point in scroll container - const pointInScrollContainer = this._getPointRelativeToElement(scrollable, cursorPosition); + const pointInScrollContainer = this.getPointRelativeToElement(scrollable, cursorPosition); // calculate the thumb boundary to make sure that the visual appearance of // a thumb at the lowest point matches the bottom of scrollable view - const thumbBoundary = this._computeTrackLength(scrollable) - thumb.clientHeight; + const thumbBoundary = this.computeTrackLength(scrollable) - thumb.clientHeight; const thumbTop = pointInScrollContainer.y - this.state.dragStart.y - this.props.trackPadding.y; const newScrollTop = (thumbTop / thumbBoundary) * maxScrollTop; @@ -227,7 +291,7 @@ export default class CustomScrollbars extends React.Component<Props, State> { } if (scrollable && track) { - const intersectsTrack = this._isPointInsideOfElement(track, cursorPosition); + const intersectsTrack = this.isPointInsideOfElement(track, cursorPosition); if (!this.state.isTrackHovered && intersectsTrack) { this.handleEnterTrack(); @@ -237,68 +301,7 @@ export default class CustomScrollbars extends React.Component<Props, State> { } }; - render() { - const { - autoHide: _autoHide, - trackPadding: _trackPadding, - onScroll: _onScroll, - children, - ...otherProps - } = this.props; - const showScrollbars = this.state.canScroll && this.state.showScrollIndicators; - const thumbAnimationClass = showScrollbars ? ' custom-scrollbars__thumb--visible' : ''; - const thumbActiveClass = - this.state.isTrackHovered || this.state.isDragging ? ' custom-scrollbars__thumb--active' : ''; - const thumbWideClass = this.state.isWide ? ' custom-scrollbars__thumb--wide' : ''; - const trackClass = - showScrollbars && this.state.showTrack ? ' custom-scrollbars__track--visible' : ''; - - return ( - <div {...otherProps} className="custom-scrollbars"> - <div className={`custom-scrollbars__track ${trackClass}`} ref={this._trackRef} /> - <div - className={`custom-scrollbars__thumb ${thumbWideClass} ${thumbActiveClass} ${thumbAnimationClass}`} - style={{ position: 'absolute', top: 0, right: 0 }} - ref={this._thumbRef} - /> - <div - className="custom-scrollbars__scrollable" - style={{ overflow: 'auto' }} - onScroll={this._onScroll} - ref={this._scrollableRef}> - {children} - </div> - </div> - ); - } - - _onScroll = () => { - this._updateScrollbarsHelper({ position: true }); - - if (this.props.autoHide) { - this._ensureScrollbarsVisible(); - - // only auto-hide when scrolling with mousewheel - if (!this.state.isDragging) { - this._startAutoHide(); - } - } else { - // only auto-shrink when scrolling with mousewheel - if (!this.state.isDragging) { - this._startAutoShrink(); - } - } - - const scrollView = this._scrollableRef.current; - if (scrollView && this.props.onScroll) { - this.props.onScroll({ - scrollLeft: scrollView.scrollLeft, - scrollTop: scrollView.scrollTop, - }); - } - }; - - _ensureScrollbarsVisible() { + private ensureScrollbarsVisible() { if (!this.state.showScrollIndicators) { this.setState({ showScrollIndicators: true, @@ -306,12 +309,12 @@ export default class CustomScrollbars extends React.Component<Props, State> { } } - _startAutoHide() { - if (this._autoHideTimer) { - clearTimeout(this._autoHideTimer); + private startAutoHide() { + if (this.autoHideTimer) { + clearTimeout(this.autoHideTimer); } - this._autoHideTimer = setTimeout(() => { + this.autoHideTimer = setTimeout(() => { this.setState({ showScrollIndicators: false, showTrack: false, @@ -320,12 +323,12 @@ export default class CustomScrollbars extends React.Component<Props, State> { }, AUTOHIDE_TIMEOUT); } - _startAutoShrink() { - if (this._autoHideTimer) { - clearTimeout(this._autoHideTimer); + private startAutoShrink() { + if (this.autoHideTimer) { + clearTimeout(this.autoHideTimer); } - this._autoHideTimer = setTimeout(() => { + this.autoHideTimer = setTimeout(() => { this.setState({ showTrack: false, isWide: false, @@ -333,21 +336,21 @@ export default class CustomScrollbars extends React.Component<Props, State> { }, AUTOHIDE_TIMEOUT); } - _stopAutoHide() { - if (this._autoHideTimer) { - clearTimeout(this._autoHideTimer); - this._autoHideTimer = undefined; + private stopAutoHide() { + if (this.autoHideTimer) { + clearTimeout(this.autoHideTimer); + this.autoHideTimer = undefined; } } - _isPointInsideOfElement(element: HTMLElement, point: { x: number; y: number }) { + private isPointInsideOfElement(element: HTMLElement, point: { x: number; y: number }) { const rect = element.getBoundingClientRect(); return ( point.x >= rect.left && point.x <= rect.right && point.y >= rect.top && point.y <= rect.bottom ); } - _getPointRelativeToElement(element: HTMLElement, point: { x: number; y: number }) { + private getPointRelativeToElement(element: HTMLElement, point: { x: number; y: number }) { const rect = element.getBoundingClientRect(); return { x: point.x - rect.left, @@ -355,12 +358,12 @@ export default class CustomScrollbars extends React.Component<Props, State> { }; } - _computeTrackLength(scrollable: HTMLElement) { + private computeTrackLength(scrollable: HTMLElement) { return scrollable.offsetHeight - this.props.trackPadding.y * 2; } // Computes the position of child element within scrollable container - _computeOffsetTop(scrollable: HTMLElement, child: HTMLElement) { + private computeOffsetTop(scrollable: HTMLElement, child: HTMLElement) { let offsetTop = 0; let node = child; @@ -376,8 +379,12 @@ export default class CustomScrollbars extends React.Component<Props, State> { return offsetTop; } - _computeScrollTop(scrollable: HTMLElement, child: HTMLElement, scrollPosition: ScrollPosition) { - const offsetTop = this._computeOffsetTop(scrollable, child); + private computeScrollTop( + scrollable: HTMLElement, + child: HTMLElement, + scrollPosition: ScrollPosition, + ) { + const offsetTop = this.computeOffsetTop(scrollable, child); switch (scrollPosition) { case 'top': @@ -391,7 +398,7 @@ export default class CustomScrollbars extends React.Component<Props, State> { } } - _computeThumbPosition(scrollable: HTMLElement, thumb: HTMLElement) { + private computeThumbPosition(scrollable: HTMLElement, thumb: HTMLElement) { // the content height of the scroll view const scrollHeight = scrollable.scrollHeight; @@ -409,7 +416,7 @@ export default class CustomScrollbars extends React.Component<Props, State> { // calculate the thumb boundary to make sure that the visual appearance of // a thumb at the lowest point matches the bottom of scrollable view - const thumbBoundary = this._computeTrackLength(scrollable) - thumb.clientHeight; + const thumbBoundary = this.computeTrackLength(scrollable) - thumb.clientHeight; // calculate thumb position based on scroll progress and thumb boundary // adding vertical inset to adjust the thumb's appearance @@ -421,7 +428,7 @@ export default class CustomScrollbars extends React.Component<Props, State> { }; } - _computeThumbHeight(scrollable: HTMLElement) { + private computeThumbHeight(scrollable: HTMLElement) { const scrollHeight = scrollable.scrollHeight; const visibleHeight = scrollable.offsetHeight; @@ -431,21 +438,21 @@ export default class CustomScrollbars extends React.Component<Props, State> { return Math.max(thumbHeight, 8); } - _updateScrollbarsHelper(updateFlags: Partial<ScrollbarUpdateContext>) { - const scrollable = this._scrollableRef.current; - const thumb = this._thumbRef.current; + private updateScrollbarsHelper(updateFlags: Partial<IScrollbarUpdateContext>) { + const scrollable = this.scrollableRef.current; + const thumb = this.thumbRef.current; if (scrollable && thumb) { - this._updateScrollbars(scrollable, thumb, updateFlags); + this.updateScrollbars(scrollable, thumb, updateFlags); } } - _updateScrollbars( + private updateScrollbars( scrollable: HTMLElement, thumb: HTMLElement, - context: Partial<ScrollbarUpdateContext>, + context: Partial<IScrollbarUpdateContext>, ) { if (context.size) { - const thumbHeight = this._computeThumbHeight(scrollable); + const thumbHeight = this.computeThumbHeight(scrollable); thumb.style.setProperty('height', thumbHeight + 'px'); // hide thumb when there is nothing to scroll @@ -455,14 +462,14 @@ export default class CustomScrollbars extends React.Component<Props, State> { // flash the scroll indicators when the view becomes scrollable if (this.props.autoHide && canScroll) { - this._startAutoHide(); - this._ensureScrollbarsVisible(); + this.startAutoHide(); + this.ensureScrollbarsVisible(); } } } if (context.position) { - const { x, y } = this._computeThumbPosition(scrollable, thumb); + const { x, y } = this.computeThumbPosition(scrollable, thumb); thumb.style.setProperty('transform', `translate(${x}px, ${y}px)`); } } diff --git a/gui/packages/desktop/src/renderer/components/Launch.tsx b/gui/packages/desktop/src/renderer/components/Launch.tsx index 5841113fce..facf5942e5 100644 --- a/gui/packages/desktop/src/renderer/components/Launch.tsx +++ b/gui/packages/desktop/src/renderer/components/Launch.tsx @@ -1,8 +1,8 @@ -import * as React from 'react'; -import { Component, Styles, View, Text } from 'reactxp'; import { ImageView, SettingsBarButton } from '@mullvad/components'; -import { Layout, Container, Header } from './Layout'; +import * as React from 'react'; +import { Component, Styles, Text, View } from 'reactxp'; import { colors } from '../../config.json'; +import { Container, Header, Layout } from './Layout'; const styles = { container: Styles.createViewStyle({ @@ -32,12 +32,12 @@ const styles = { }), }; -type Props = { +interface IProps { openSettings: () => void; -}; +} -export default class Launch extends Component<Props> { - render() { +export default class Launch extends Component<IProps> { + public render() { return ( <Layout> <Header> diff --git a/gui/packages/desktop/src/renderer/components/Layout.tsx b/gui/packages/desktop/src/renderer/components/Layout.tsx index efe3b3ff11..e1e07bd072 100644 --- a/gui/packages/desktop/src/renderer/components/Layout.tsx +++ b/gui/packages/desktop/src/renderer/components/Layout.tsx @@ -1,12 +1,12 @@ -import * as React from 'react'; -import { View, Component } from 'reactxp'; import { HeaderBar } from '@mullvad/components'; +import * as React from 'react'; +import { Component, View } from 'reactxp'; import styles from './LayoutStyles'; export class Header extends Component<HeaderBar['props']> { - static defaultProps = HeaderBar.defaultProps; + public static defaultProps = HeaderBar.defaultProps; - render() { + public render() { return ( <View style={[styles.header, this.props.style]}> <HeaderBar barStyle={this.props.barStyle}>{this.props.children}</HeaderBar> @@ -15,20 +15,20 @@ export class Header extends Component<HeaderBar['props']> { } } -type ContainerProps = { +interface IContainerProps { children: React.ReactNode; -}; -export class Container extends Component<ContainerProps> { - render() { +} +export class Container extends Component<IContainerProps> { + public render() { return <View style={styles.container}>{this.props.children}</View>; } } -type LayoutProps = { - children: Array<React.ReactNode> | React.ReactNode; -}; -export class Layout extends Component<LayoutProps> { - render() { +interface ILayoutProps { + children: React.ReactNode; +} +export class Layout extends Component<ILayoutProps> { + public render() { return <View style={styles.layout}>{this.props.children}</View>; } } diff --git a/gui/packages/desktop/src/renderer/components/Login.tsx b/gui/packages/desktop/src/renderer/components/Login.tsx index 3af814a347..4327bc1f60 100644 --- a/gui/packages/desktop/src/renderer/components/Login.tsx +++ b/gui/packages/desktop/src/renderer/components/Login.tsx @@ -1,18 +1,18 @@ +import { Accordion, Brand, ImageView, SettingsBarButton } from '@mullvad/components'; import * as React from 'react'; -import { Component, Text, TextInput, View, Animated, Styles, UserInterface, Types } from 'reactxp'; -import { Layout, Container, Header } from './Layout'; -import { Accordion, ImageView, SettingsBarButton, Brand } from '@mullvad/components'; -import * as Cell from './Cell'; +import { Animated, Component, Styles, Text, TextInput, Types, UserInterface, View } from 'reactxp'; +import { colors, links } from '../../config.json'; import * as AppButton from './AppButton'; +import * as Cell from './Cell'; +import { Container, Header, Layout } from './Layout'; import styles from './LoginStyles'; -import { colors, links } from '../../config.json'; -import { LoginState } from '../redux/account/reducers'; import { AccountToken } from '../../shared/daemon-rpc-types'; +import { LoginState } from '../redux/account/reducers'; -type Props = { +interface IProps { accountToken?: AccountToken; - accountHistory: Array<AccountToken>; + accountHistory: AccountToken[]; loginError?: Error; loginState: LoginState; openSettings?: () => void; @@ -21,77 +21,77 @@ type Props = { resetLoginError: () => void; updateAccountToken: (accountToken: AccountToken) => void; removeAccountTokenFromHistory: (accountToken: AccountToken) => Promise<void>; -}; +} -type State = { +interface IState { isActive: boolean; -}; +} const MIN_ACCOUNT_TOKEN_LENGTH = 10; -export default class Login extends Component<Props, State> { - state = { +export default class Login extends Component<IProps, IState> { + public state: IState = { isActive: true, }; - _accountInput = React.createRef<TextInput>(); - _shouldResetLoginError = false; + private accountInput = React.createRef<TextInput>(); + private shouldResetLoginError = false; - _showsFooter = true; - _footerAnimatedValue = Animated.createValue(0); - _footerAnimation?: Types.Animated.CompositeAnimation; - _footerAnimationStyle: Types.AnimatedViewStyleRuleSet; - _footerRef = React.createRef<Animated.View>(); + private showsFooter = true; + private footerAnimatedValue = Animated.createValue(0); + private footerAnimation?: Types.Animated.CompositeAnimation; + private footerAnimationStyle: Types.AnimatedViewStyleRuleSet; + private footerRef = React.createRef<Animated.View>(); - _isLoginButtonActive = false; - _loginButtonAnimatedValue = Animated.createValue(0); - _loginButtonAnimation?: Types.Animated.CompositeAnimation; - _loginButtonAnimationStyle: Types.AnimatedViewStyleRuleSet; + private isLoginButtonActive = false; + private loginButtonAnimatedValue = Animated.createValue(0); + private loginButtonAnimation?: Types.Animated.CompositeAnimation; + private loginButtonAnimationStyle: Types.AnimatedViewStyleRuleSet; - constructor(props: Props) { + constructor(props: IProps) { super(props); if (props.loginState === 'failed') { - this._shouldResetLoginError = true; + this.shouldResetLoginError = true; } - this._footerAnimationStyle = Styles.createAnimatedViewStyle({ - transform: [{ translateY: this._footerAnimatedValue }], + this.footerAnimationStyle = Styles.createAnimatedViewStyle({ + transform: [{ translateY: this.footerAnimatedValue }], }); - this._loginButtonAnimationStyle = Styles.createAnimatedViewStyle({ + this.loginButtonAnimationStyle = Styles.createAnimatedViewStyle({ backgroundColor: Animated.interpolate( - this._loginButtonAnimatedValue, + this.loginButtonAnimatedValue, [0.0, 1.0], [colors.white, colors.green], ), }); } - componentDidMount() { - this._setFooterVisibility(this._shouldShowFooter()); + public componentDidMount() { + this.setFooterVisibility(this.shouldShowFooter()); } - componentDidUpdate(prevProps: Props, _prevState: State) { + public componentDidUpdate(prevProps: IProps, _prevState: IState) { if ( this.props.loginState !== prevProps.loginState && this.props.loginState === 'failed' && - !this._shouldResetLoginError + !this.shouldResetLoginError ) { - this._shouldResetLoginError = true; + this.shouldResetLoginError = true; // focus on login field when failed to log in - const accountInput = this._accountInput.current; + const accountInput = this.accountInput.current; if (accountInput) { accountInput.focus(); } } - this._setLoginButtonActive(this._shouldActivateLoginButton()); - this._setFooterVisibility(this._shouldShowFooter()); + this.setLoginButtonActive(this.shouldActivateLoginButton()); + this.setFooterVisibility(this.shouldShowFooter()); } - render() { + public render() { return ( <Layout> <Header> @@ -100,29 +100,29 @@ export default class Login extends Component<Props, State> { </Header> <Container> <View style={styles.login_form}> - {this._getStatusIcon()} - <Text style={styles.title}>{this._formTitle()}</Text> + {this.getStatusIcon()} + <Text style={styles.title}>{this.formTitle()}</Text> - {this._createLoginForm()} + {this.createLoginForm()} </View> <Animated.View - ref={this._footerRef} - style={[styles.login_footer, this._footerAnimationStyle]}> - {this._createFooter()} + ref={this.footerRef} + style={[styles.login_footer, this.footerAnimationStyle]}> + {this.createFooter()} </Animated.View> </Container> </Layout> ); } - _onCreateAccount = () => this.props.openExternalLink(links.createAccount); + private onCreateAccount = () => this.props.openExternalLink(links.createAccount); - _onFocus = () => { + private onFocus = () => { this.setState({ isActive: true }); }; - _onBlur = (e: Types.SyntheticEvent) => { + private onBlur = (e: Types.SyntheticEvent) => { // TOOD: relatedTarget is not exposed by ReactXP and may not work on non-web platforms. // Find a workaround. // @ts-ignore @@ -130,8 +130,8 @@ export default class Login extends Component<Props, State> { // restore focus if click happened within dropdown if (relatedTarget) { - if (this._accountInput.current) { - this._accountInput.current.focus(); + if (this.accountInput.current) { + this.accountInput.current.focus(); } return; } @@ -139,65 +139,65 @@ export default class Login extends Component<Props, State> { this.setState({ isActive: false }); }; - async _setLoginButtonActive(isActive: boolean) { - if (this._isLoginButtonActive === isActive) { + private async setLoginButtonActive(isActive: boolean) { + if (this.isLoginButtonActive === isActive) { return; } - const animation = Animated.timing(this._loginButtonAnimatedValue, { + const animation = Animated.timing(this.loginButtonAnimatedValue, { toValue: isActive ? 1 : 0, easing: Animated.Easing.Linear(), duration: 250, }); - const oldAnimation = this._loginButtonAnimation; + const oldAnimation = this.loginButtonAnimation; if (oldAnimation) { oldAnimation.stop(); } animation.start(); - this._loginButtonAnimation = animation; - this._isLoginButtonActive = isActive; + this.loginButtonAnimation = animation; + this.isLoginButtonActive = isActive; } - async _setFooterVisibility(show: boolean) { - if (this._showsFooter === show || !this._footerRef.current) { + private async setFooterVisibility(show: boolean) { + if (this.showsFooter === show || !this.footerRef.current) { return; } - this._showsFooter = show; + this.showsFooter = show; - const layout = await UserInterface.measureLayoutRelativeToWindow(this._footerRef.current); + const layout = await UserInterface.measureLayoutRelativeToWindow(this.footerRef.current); const value = show ? 0 : layout.height; - const animation = Animated.timing(this._footerAnimatedValue, { + const animation = Animated.timing(this.footerAnimatedValue, { toValue: value, easing: Animated.Easing.InOut(), duration: 250, }); - const oldAnimation = this._footerAnimation; + const oldAnimation = this.footerAnimation; if (oldAnimation) { oldAnimation.stop(); } animation.start(); - this._footerAnimation = animation; + this.footerAnimation = animation; } - _onSubmit = () => { + private onSubmit = () => { const accountToken = this.props.accountToken; if (accountToken && accountToken.length >= MIN_ACCOUNT_TOKEN_LENGTH) { this.props.login(accountToken); } }; - _onInputChange = (value: string) => { + private onInputChange = (value: string) => { // reset error when user types in the new account number - if (this._shouldResetLoginError) { - this._shouldResetLoginError = false; + if (this.shouldResetLoginError) { + this.shouldResetLoginError = false; this.props.resetLoginError(); } @@ -206,7 +206,7 @@ export default class Login extends Component<Props, State> { this.props.updateAccountToken(accountToken); }; - _formTitle() { + private formTitle() { switch (this.props.loginState) { case 'logging in': return 'Logging in...'; @@ -219,7 +219,7 @@ export default class Login extends Component<Props, State> { } } - _formSubtitle() { + private formSubtitle() { const { loginState, loginError } = this.props; switch (loginState) { case 'failed': @@ -233,8 +233,8 @@ export default class Login extends Component<Props, State> { } } - _getStatusIcon() { - const statusIconPath = this._getStatusIconPath(); + private getStatusIcon() { + const statusIconPath = this.getStatusIconPath(); return ( <View style={styles.status_icon}> {statusIconPath ? <ImageView source={statusIconPath} height={48} width={48} /> : null} @@ -242,7 +242,7 @@ export default class Login extends Component<Props, State> { ); } - _getStatusIconPath(): string | undefined { + private getStatusIconPath(): string | undefined { switch (this.props.loginState) { case 'logging in': return 'icon-spinner'; @@ -255,7 +255,7 @@ export default class Login extends Component<Props, State> { } } - _accountInputGroupStyles(): Types.ViewStyleRuleSet[] { + private accountInputGroupStyles(): Types.ViewStyleRuleSet[] { const classes = [styles.account_input_group]; if (this.state.isActive) { classes.push(styles.account_input_group__active); @@ -274,7 +274,7 @@ export default class Login extends Component<Props, State> { return classes; } - _accountInputButtonStyles() { + private accountInputButtonStyles() { const classes: Array< Types.StyleRuleSet<Types.AnimatedViewStyle> | Types.StyleRuleSet<Types.ViewStyle> > = [styles.input_button]; @@ -283,12 +283,12 @@ export default class Login extends Component<Props, State> { classes.push(styles.input_button__invisible); } - classes.push(this._loginButtonAnimationStyle); + classes.push(this.loginButtonAnimationStyle); return classes; } - _accountInputArrowStyles(): Types.ViewStyleRuleSet[] { + private accountInputArrowStyles(): Types.ViewStyleRuleSet[] { const { loginState } = this.props; const classes = [styles.input_arrow]; @@ -299,7 +299,7 @@ export default class Login extends Component<Props, State> { return classes; } - _shouldActivateLoginButton(): boolean { + private shouldActivateLoginButton(): boolean { const { accountToken } = this.props; if (accountToken && accountToken.length >= MIN_ACCOUNT_TOKEN_LENGTH) { return true; @@ -307,36 +307,34 @@ export default class Login extends Component<Props, State> { return false; } - _shouldEnableAccountInput() { + private shouldEnableAccountInput() { // enable account input always except when "logging in" or "logged in" return this.props.loginState !== 'logging in' && this.props.loginState !== 'ok'; } - _shouldShowAccountHistory() { + private shouldShowAccountHistory() { return ( - this._shouldEnableAccountInput() && - this.state.isActive && - this.props.accountHistory.length > 0 + this.shouldEnableAccountInput() && this.state.isActive && this.props.accountHistory.length > 0 ); } - _shouldShowFooter() { + private shouldShowFooter() { return ( (this.props.loginState === 'none' || this.props.loginState === 'failed') && - !this._shouldShowAccountHistory() + !this.shouldShowAccountHistory() ); } - _onSelectAccountFromHistory = (accountToken: string) => { + private onSelectAccountFromHistory = (accountToken: string) => { this.props.updateAccountToken(accountToken); this.props.login(accountToken); }; - _onRemoveAccountFromHistory = (accountToken: string) => { - this._removeAccountFromHistory(accountToken); + private onRemoveAccountFromHistory = (accountToken: string) => { + this.removeAccountFromHistory(accountToken); }; - async _removeAccountFromHistory(accountToken: AccountToken) { + private async removeAccountFromHistory(accountToken: AccountToken) { try { await this.props.removeAccountTokenFromHistory(accountToken); @@ -346,11 +344,11 @@ export default class Login extends Component<Props, State> { } } - _createLoginForm() { + private createLoginForm() { return ( <View> - <Text style={styles.subtitle}>{this._formSubtitle()}</Text> - <View style={this._accountInputGroupStyles()}> + <Text style={styles.subtitle}>{this.formSubtitle()}</Text> + <View style={this.accountInputGroupStyles()}> <View style={styles.account_input_backdrop}> <TextInput style={styles.account_input_textfield} @@ -358,19 +356,19 @@ export default class Login extends Component<Props, State> { placeholderTextColor={colors.blue40} value={this.props.accountToken || ''} autoCorrect={false} - editable={this._shouldEnableAccountInput()} - onFocus={this._onFocus} - onBlur={this._onBlur} - onChangeText={this._onInputChange} - onSubmitEditing={this._onSubmit} + editable={this.shouldEnableAccountInput()} + onFocus={this.onFocus} + onBlur={this.onBlur} + onChangeText={this.onInputChange} + onSubmitEditing={this.onSubmit} returnKeyType="done" keyboardType="numeric" autoFocus={true} - ref={this._accountInput} + ref={this.accountInput} /> - <Animated.View style={this._accountInputButtonStyles()} onPress={this._onSubmit}> + <Animated.View style={this.accountInputButtonStyles()} onPress={this.onSubmit}> <ImageView - style={this._accountInputArrowStyles()} + style={this.accountInputArrowStyles()} source="icon-arrow" height={16} width={24} @@ -378,12 +376,12 @@ export default class Login extends Component<Props, State> { /> </Animated.View> </View> - <Accordion expanded={this._shouldShowAccountHistory()}> + <Accordion expanded={this.shouldShowAccountHistory()}> { <AccountDropdown items={this.props.accountHistory.slice().reverse()} - onSelect={this._onSelectAccountFromHistory} - onRemove={this._onRemoveAccountFromHistory} + onSelect={this.onSelectAccountFromHistory} + onRemove={this.onRemoveAccountFromHistory} /> } </Accordion> @@ -392,11 +390,11 @@ export default class Login extends Component<Props, State> { ); } - _createFooter() { + private createFooter() { return ( <View> <Text style={styles.login_footer__prompt}>{"Don't have an account number?"}</Text> - <AppButton.BlueButton onPress={this._onCreateAccount}> + <AppButton.BlueButton onPress={this.onCreateAccount}> <AppButton.Label>Create account</AppButton.Label> <AppButton.Icon source="icon-extLink" height={16} width={16} /> </AppButton.BlueButton> @@ -405,14 +403,14 @@ export default class Login extends Component<Props, State> { } } -type AccountDropdownProps = { - items: Array<AccountToken>; +interface IAccountDropdownProps { + items: AccountToken[]; onSelect: (value: AccountToken) => void; onRemove: (value: AccountToken) => void; -}; +} -class AccountDropdown extends React.Component<AccountDropdownProps> { - render() { +class AccountDropdown extends Component<IAccountDropdownProps> { + public render() { const uniqueItems = [...new Set(this.props.items)]; return ( <View> @@ -430,15 +428,15 @@ class AccountDropdown extends React.Component<AccountDropdownProps> { } } -type AccountDropdownItemProps = { +interface IAccountDropdownItemProps { label: string; value: AccountToken; onRemove: (value: AccountToken) => void; onSelect: (value: AccountToken) => void; -}; +} -class AccountDropdownItem extends React.Component<AccountDropdownItemProps> { - render() { +class AccountDropdownItem extends Component<IAccountDropdownItemProps> { + public render() { return ( <View> <View style={styles.account_dropdown__spacer} /> @@ -449,7 +447,7 @@ class AccountDropdownItem extends React.Component<AccountDropdownItemProps> { textStyle={styles.account_dropdown__label} containerStyle={styles.account_dropdown__label_container} cellHoverTextStyle={styles.account_dropdown__label_hover} - onPress={() => this.props.onSelect(this.props.value)}> + onPress={this.handleSelect}> {this.props.label} </Cell.Label> <ImageView @@ -459,10 +457,18 @@ class AccountDropdownItem extends React.Component<AccountDropdownItemProps> { source="icon-close-sml" height={16} width={16} - onPress={() => this.props.onRemove(this.props.value)} + onPress={this.handleRemove} /> </Cell.CellButton> </View> ); } + + private handleSelect = () => { + this.props.onSelect(this.props.value); + }; + + private handleRemove = () => { + this.props.onRemove(this.props.value); + }; } diff --git a/gui/packages/desktop/src/renderer/components/Map.tsx b/gui/packages/desktop/src/renderer/components/Map.tsx index 5640c00676..4fc77cb739 100644 --- a/gui/packages/desktop/src/renderer/components/Map.tsx +++ b/gui/packages/desktop/src/renderer/components/Map.tsx @@ -31,34 +31,34 @@ interface IState { } export default class Map extends Component<IProps, IState> { - state: IState = { + public state: IState = { bounds: { width: 0, height: 0, }, }; - render() { + public render() { const { width, height } = this.state.bounds; const readyToRenderTheMap = width > 0 && height > 0; return ( - <View style={this.props.style} onLayout={this._onLayout}> + <View style={this.props.style} onLayout={this.onLayout}> {readyToRenderTheMap && ( <SvgMap width={width} height={height} center={this.props.center} offset={this.props.offset} - zoomLevel={this._zoomLevel(this.props.zoomLevel)} + zoomLevel={this.zoomLevel(this.props.zoomLevel)} showMarker={this.props.showMarker} - markerImagePath={this._markerImage(this.props.markerStyle)} + markerImagePath={this.markerImage(this.props.markerStyle)} /> )} </View> ); } - shouldComponentUpdate(nextProps: IProps, nextState: IState) { + public shouldComponentUpdate(nextProps: IProps, nextState: IState) { const oldProps = this.props; const oldState = this.state; return ( @@ -74,7 +74,7 @@ export default class Map extends Component<IProps, IState> { ); } - _onLayout = (layoutInfo: Types.ViewOnLayoutEvent) => { + private onLayout = (layoutInfo: Types.ViewOnLayoutEvent) => { this.setState({ bounds: { width: layoutInfo.width, @@ -84,7 +84,7 @@ export default class Map extends Component<IProps, IState> { }; // TODO: Remove zoom level in favor of center + coordinate span - _zoomLevel(variant: ZoomLevel) { + private zoomLevel(variant: ZoomLevel) { switch (variant) { case ZoomLevel.high: return 1; @@ -95,7 +95,7 @@ export default class Map extends Component<IProps, IState> { } } - _markerImage(style: MarkerStyle): string { + private markerImage(style: MarkerStyle): string { switch (style) { case MarkerStyle.secure: return '../../assets/images/location-marker-secure.svg'; diff --git a/gui/packages/desktop/src/renderer/components/NavigationBar.tsx b/gui/packages/desktop/src/renderer/components/NavigationBar.tsx index 6708e345f7..98e2a367c7 100644 --- a/gui/packages/desktop/src/renderer/components/NavigationBar.tsx +++ b/gui/packages/desktop/src/renderer/components/NavigationBar.tsx @@ -1,8 +1,8 @@ -import * as React from 'react'; -import { Animated, Button, Component, Text, Types, View, Styles, UserInterface } from 'reactxp'; import { ImageView } from '@mullvad/components'; -import CustomScrollbars, { ScrollEvent } from './CustomScrollbars'; +import * as React from 'react'; +import { Animated, Button, Component, Styles, Text, Types, UserInterface, View } from 'reactxp'; import { colors } from '../../config.json'; +import CustomScrollbars, { IScrollEvent } from './CustomScrollbars'; const styles = { navigationBar: { @@ -78,51 +78,53 @@ const styles = { }, }; -type NavigationScrollContextValue = { +interface INavigationScrollContextValue { scrollTop: number; - onScroll: (event: ScrollEvent) => void; -}; + onScroll: (event: IScrollEvent) => void; +} -const NavigationScrollContext = React.createContext<NavigationScrollContextValue>({ +const NavigationScrollContext = React.createContext<INavigationScrollContextValue>({ scrollTop: 0, - onScroll: (_event: ScrollEvent) => {}, + onScroll: (_event: IScrollEvent) => { + // no-op + }, }); export class NavigationContainer extends Component { - state = { + public state = { scrollTop: 0, }; - _onScroll = (event: ScrollEvent) => { - this.setState({ - scrollTop: event.scrollTop, - }); - }; - - render() { + public render() { return ( <NavigationScrollContext.Provider - value={{ scrollTop: this.state.scrollTop, onScroll: this._onScroll }}> + value={{ scrollTop: this.state.scrollTop, onScroll: this.onScroll }}> {this.props.children} </NavigationScrollContext.Provider> ); } + + private onScroll = (event: IScrollEvent) => { + this.setState({ + scrollTop: event.scrollTop, + }); + }; } -type NavigationScrollbarsProps = { - onScroll?: (value: ScrollEvent) => void; +interface INavigationScrollbarsProps { + onScroll?: (value: IScrollEvent) => void; style?: React.CSSProperties; children?: React.ReactNode; -}; +} export const NavigationScrollbars = React.forwardRef(function NavigationScrollbarsT( - props: NavigationScrollbarsProps, + props: INavigationScrollbarsProps, ref?: React.Ref<CustomScrollbars>, ) { return ( <NavigationScrollContext.Consumer> {(context) => { const { style, children, ...otherProps } = props; - const wrappedOnScroll = (scroll: ScrollEvent) => { + const wrappedOnScroll = (scroll: IScrollEvent) => { context.onScroll(scroll); if (otherProps.onScroll) { @@ -140,14 +142,14 @@ export const NavigationScrollbars = React.forwardRef(function NavigationScrollba ); }); -type PrivateTitleBarItemProps = { +interface IPrivateTitleBarItemProps { visible: boolean; titleAdjustment: number; children?: React.ReactText; -}; +} -class PrivateTitleBarItem extends Component<PrivateTitleBarItemProps> { - shouldComponentUpdate(nextProps: PrivateTitleBarItemProps) { +class PrivateTitleBarItem extends Component<IPrivateTitleBarItemProps> { + public shouldComponentUpdate(nextProps: IPrivateTitleBarItemProps) { return ( this.props.visible !== nextProps.visible || this.props.titleAdjustment !== nextProps.titleAdjustment || @@ -155,7 +157,7 @@ class PrivateTitleBarItem extends Component<PrivateTitleBarItemProps> { ); } - render() { + public render() { const titleAdjustment = this.props.titleAdjustment; const titleAdjustmentStyle = Styles.createViewStyle( { @@ -175,50 +177,50 @@ class PrivateTitleBarItem extends Component<PrivateTitleBarItemProps> { } } -type PrivateBarItemAnimationContainerProps = { +interface IPrivateBarItemAnimationContainerProps { visible: boolean; children?: React.ReactNode; -}; +} -class PrivateBarItemAnimationContainer extends Component<PrivateBarItemAnimationContainerProps> { - _opacityValue: Animated.Value; - _opacityStyle: Types.AnimatedViewStyleRuleSet; - _animation?: Types.Animated.CompositeAnimation; +class PrivateBarItemAnimationContainer extends Component<IPrivateBarItemAnimationContainerProps> { + private opacityValue: Animated.Value; + private opacityStyle: Types.AnimatedViewStyleRuleSet; + private animation?: Types.Animated.CompositeAnimation; - constructor(props: PrivateBarItemAnimationContainerProps) { + constructor(props: IPrivateBarItemAnimationContainerProps) { super(props); - this._opacityValue = Animated.createValue(props.visible ? 1 : 0); - this._opacityStyle = Styles.createAnimatedViewStyle({ - opacity: this._opacityValue, + this.opacityValue = Animated.createValue(props.visible ? 1 : 0); + this.opacityStyle = Styles.createAnimatedViewStyle({ + opacity: this.opacityValue, }); } - shouldComponentUpdate(nextProps: PrivateBarItemAnimationContainerProps) { + public shouldComponentUpdate(nextProps: IPrivateBarItemAnimationContainerProps) { return this.props.visible !== nextProps.visible || this.props.children !== nextProps.children; } - componentDidUpdate() { - this._animateOpacity(this.props.visible); + public componentDidUpdate() { + this.animateOpacity(this.props.visible); } - componentWillUnmount() { - if (this._animation) { - this._animation.stop(); + public componentWillUnmount() { + if (this.animation) { + this.animation.stop(); } } - render() { - return <Animated.View style={this._opacityStyle}>{this.props.children}</Animated.View>; + public render() { + return <Animated.View style={this.opacityStyle}>{this.props.children}</Animated.View>; } - _animateOpacity(visible: boolean) { - const oldAnimation = this._animation; + private animateOpacity(visible: boolean) { + const oldAnimation = this.animation; if (oldAnimation) { oldAnimation.stop(); } - const animation = Animated.timing(this._opacityValue, { + const animation = Animated.timing(this.opacityValue, { toValue: visible ? 1 : 0, easing: Animated.Easing.InOut(), duration: 250, @@ -226,16 +228,16 @@ class PrivateBarItemAnimationContainer extends Component<PrivateBarItemAnimation animation.start(); - this._animation = animation; + this.animation = animation; } } -type NavigationBarProps = { +interface INavigationBarProps { children?: React.ReactNode; -}; +} export const NavigationBar = React.forwardRef(function NavigationBarT( - props: NavigationBarProps, + props: INavigationBarProps, ref?: React.Ref<PrivateNavigationBar>, ) { return ( @@ -249,16 +251,16 @@ export const NavigationBar = React.forwardRef(function NavigationBarT( ); }); -type PrivateNavigationBarProps = { +interface IPrivateNavigationBarProps { scrollTop: number; children?: React.ReactNode; -}; +} -type PrivateNavigationBarState = { +interface IPrivateNavigationBarState { titleAdjustment: number; showsBarSeparator: boolean; showsBarTitle: boolean; -}; +} const PrivateTitleBarItemContext = React.createContext({ titleAdjustment: 0, @@ -266,22 +268,17 @@ const PrivateTitleBarItemContext = React.createContext({ titleRef: React.createRef<PrivateTitleBarItem>(), }); -class PrivateNavigationBar extends Component<PrivateNavigationBarProps, PrivateNavigationBarState> { - static defaultProps: Partial<PrivateNavigationBarProps> = { +class PrivateNavigationBar extends Component< + IPrivateNavigationBarProps, + IPrivateNavigationBarState +> { + public static defaultProps: Partial<IPrivateNavigationBarProps> = { scrollTop: 0, }; - state: PrivateNavigationBarState = { - titleAdjustment: 0, - showsBarSeparator: false, - showsBarTitle: false, - }; - - _titleViewRef = React.createRef<PrivateTitleBarItem>(); - - static getDerivedStateFromProps( - props: PrivateNavigationBarProps, - state: PrivateNavigationBarState, + public static getDerivedStateFromProps( + props: IPrivateNavigationBarProps, + state: IPrivateNavigationBarState, ) { // that's where SettingsHeader.HeaderTitle intersects the navigation bar const showsBarSeparator = props.scrollTop > 11; @@ -296,9 +293,17 @@ class PrivateNavigationBar extends Component<PrivateNavigationBarProps, PrivateN }; } - shouldComponentUpdate( - nextProps: PrivateNavigationBarProps, - nextState: PrivateNavigationBarState, + public state: IPrivateNavigationBarState = { + titleAdjustment: 0, + showsBarSeparator: false, + showsBarTitle: false, + }; + + private titleViewRef = React.createRef<PrivateTitleBarItem>(); + + public shouldComponentUpdate( + nextProps: IPrivateNavigationBarProps, + nextState: IPrivateNavigationBarState, ) { return ( this.props.children !== nextProps.children || @@ -308,20 +313,20 @@ class PrivateNavigationBar extends Component<PrivateNavigationBarProps, PrivateN ); } - render() { + public render() { return ( <View style={[ styles.navigationBar.default, this.state.showsBarSeparator ? styles.navigationBar.separator : undefined, - this._getPlatformStyle(), + this.getPlatformStyle(), ]} - onLayout={this._onLayout}> + onLayout={this.onLayout}> <PrivateTitleBarItemContext.Provider value={{ titleAdjustment: this.state.titleAdjustment, visible: this.state.showsBarTitle, - titleRef: this._titleViewRef, + titleRef: this.titleViewRef, }}> {this.props.children} </PrivateTitleBarItemContext.Provider> @@ -329,7 +334,7 @@ class PrivateNavigationBar extends Component<PrivateNavigationBarProps, PrivateN ); } - _getPlatformStyle(): Types.ViewStyleRuleSet | undefined { + private getPlatformStyle(): Types.ViewStyleRuleSet | undefined { switch (process.platform) { case 'darwin': return styles.navigationBar.darwin; @@ -342,8 +347,8 @@ class PrivateNavigationBar extends Component<PrivateNavigationBarProps, PrivateN } } - _onLayout = async (containerLayout: Types.ViewOnLayoutEvent) => { - const titleView = this._titleViewRef.current; + private onLayout = async (containerLayout: Types.ViewOnLayoutEvent) => { + const titleView = this.titleViewRef.current; if (titleView) { // calculate the title layout frame const titleLayout = await UserInterface.measureLayoutRelativeToAncestor(titleView, this); @@ -358,10 +363,10 @@ class PrivateNavigationBar extends Component<PrivateNavigationBarProps, PrivateN }; } -type TitleBarItemProps = { +interface ITitleBarItemProps { children?: React.ReactText; -}; -export function TitleBarItem(props: TitleBarItemProps) { +} +export function TitleBarItem(props: ITitleBarItemProps) { return ( <PrivateTitleBarItemContext.Consumer> {(context) => ( @@ -376,10 +381,12 @@ export function TitleBarItem(props: TitleBarItemProps) { ); } -export class CloseBarItem extends Component<{ +interface ICloseBarItemProps { action: () => void; -}> { - render() { +} + +export class CloseBarItem extends Component<ICloseBarItemProps> { + public render() { return ( <Button style={[styles.closeBarItem.default]} onPress={this.props.action}> <ImageView height={24} width={24} style={[styles.closeBarItem.icon]} source="icon-close" /> @@ -388,11 +395,13 @@ export class CloseBarItem extends Component<{ } } -export class BackBarItem extends Component<{ +interface IBackBarItemProps { children?: React.ReactText; action: () => void; -}> { - render() { +} + +export class BackBarItem extends Component<IBackBarItemProps> { + public render() { return ( <Button style={styles.backBarButton.default} onPress={this.props.action}> <View style={styles.backBarButton.content}> diff --git a/gui/packages/desktop/src/renderer/components/NotificationArea.tsx b/gui/packages/desktop/src/renderer/components/NotificationArea.tsx index c5fedd97e0..0f339c2979 100644 --- a/gui/packages/desktop/src/renderer/components/NotificationArea.tsx +++ b/gui/packages/desktop/src/renderer/components/NotificationArea.tsx @@ -1,30 +1,30 @@ import moment from 'moment'; import * as React from 'react'; import { Component, Types } from 'reactxp'; +import { links } from '../../config.json'; import { + NotificationActions, NotificationBanner, - NotificationIndicator, NotificationContent, - NotificationActions, - NotificationTitle, - NotificationSubtitle, + NotificationIndicator, NotificationOpenLinkAction, + NotificationSubtitle, + NotificationTitle, } from './NotificationBanner'; -import { links } from '../../config.json'; -import { AuthFailure } from '../lib/auth-failure'; -import AccountExpiry from '../lib/account-expiry'; import { BlockReason, TunnelStateTransition } from '../../shared/daemon-rpc-types'; -import { VersionReduxState } from '../redux/version/reducers'; +import AccountExpiry from '../lib/account-expiry'; +import { AuthFailure } from '../lib/auth-failure'; +import { IVersionReduxState } from '../redux/version/reducers'; -type Props = { +interface IProps { style?: Types.ViewStyleRuleSet; accountExpiry?: AccountExpiry; tunnelState: TunnelStateTransition; - version: VersionReduxState; + version: IVersionReduxState; openExternalLink: (url: string) => void; blockWhenDisconnected: boolean; -}; +} type NotificationAreaPresentation = | { type: 'failure-unsecured'; reason: string } @@ -60,14 +60,12 @@ function getBlockReasonMessage(blockReason: BlockReason): string { } } -export default class NotificationArea extends Component<Props, State> { - state: State = { - type: 'blocking', - reason: '', - visible: false, - }; +function capitalizeFirstLetter(inputString: string): string { + return inputString.charAt(0).toUpperCase() + inputString.slice(1); +} - static getDerivedStateFromProps(props: Props, state: State) { +export default class NotificationArea extends Component<IProps, State> { + public static getDerivedStateFromProps(props: IProps, state: State) { const { accountExpiry, blockWhenDisconnected, tunnelState, version } = props; switch (tunnelState.state) { @@ -142,7 +140,7 @@ export default class NotificationArea extends Component<Props, State> { return { visible: true, type: 'expires-soon', - timeLeft: NotificationArea._capitalizeFirstLetter(accountExpiry.remainingTime()), + timeLeft: capitalizeFirstLetter(accountExpiry.remainingTime()), }; } @@ -153,13 +151,13 @@ export default class NotificationArea extends Component<Props, State> { } } - static _capitalizeFirstLetter(initialString: string): string { - return initialString.length > 0 - ? initialString.charAt(0).toUpperCase() + initialString.slice(1) - : ''; - } + public state: State = { + type: 'blocking', + reason: '', + visible: false, + }; - render() { + public render() { return ( <NotificationBanner style={this.props.style} visible={this.state.visible}> {this.state.type === 'failure-unsecured' && ( @@ -204,11 +202,7 @@ export default class NotificationArea extends Component<Props, State> { } now to ensure your security`}</NotificationSubtitle> </NotificationContent> <NotificationActions> - <NotificationOpenLinkAction - onPress={() => { - this.props.openExternalLink(links.download); - }} - /> + <NotificationOpenLinkAction onPress={this.handleOpenDownloadLink} /> </NotificationActions> </React.Fragment> )} @@ -223,11 +217,7 @@ export default class NotificationArea extends Component<Props, State> { }) to stay up to date`}</NotificationSubtitle> </NotificationContent> <NotificationActions> - <NotificationOpenLinkAction - onPress={() => { - this.props.openExternalLink(links.download); - }} - /> + <NotificationOpenLinkAction onPress={this.handleOpenDownloadLink} /> </NotificationActions> </React.Fragment> )} @@ -240,15 +230,19 @@ export default class NotificationArea extends Component<Props, State> { <NotificationSubtitle>{this.state.timeLeft}</NotificationSubtitle> </NotificationContent> <NotificationActions> - <NotificationOpenLinkAction - onPress={() => { - this.props.openExternalLink(links.purchase); - }} - /> + <NotificationOpenLinkAction onPress={this.handleOpenBuyMoreLink} /> </NotificationActions> </React.Fragment> )} </NotificationBanner> ); } + + private handleOpenDownloadLink = () => { + this.props.openExternalLink(links.download); + }; + + private handleOpenBuyMoreLink = () => { + this.props.openExternalLink(links.purchase); + }; } diff --git a/gui/packages/desktop/src/renderer/components/NotificationBanner.tsx b/gui/packages/desktop/src/renderer/components/NotificationBanner.tsx index 3b31a1a16a..7048cb0aa6 100644 --- a/gui/packages/desktop/src/renderer/components/NotificationBanner.tsx +++ b/gui/packages/desktop/src/renderer/components/NotificationBanner.tsx @@ -1,6 +1,6 @@ -import * as React from 'react'; -import { Animated, View, Button, Text, Component, UserInterface, Styles, Types } from 'reactxp'; import { ImageView } from '@mullvad/components'; +import * as React from 'react'; +import { Animated, Button, Component, Styles, Text, Types, UserInterface, View } from 'reactxp'; import { colors } from '../../config.json'; const styles = { @@ -70,13 +70,13 @@ const styles = { }; export class NotificationTitle extends Component { - render() { + public render() { return <Text style={styles.title}>{this.props.children}</Text>; } } export class NotificationSubtitle extends Component { - render() { + public render() { return React.Children.count(this.props.children) > 0 ? ( <Text style={styles.subtitle}>{this.props.children}</Text> ) : null; @@ -84,17 +84,17 @@ export class NotificationSubtitle extends Component { } export class NotificationOpenLinkAction extends Component<{ onPress: () => void }> { - state = { + public state = { hovered: false, }; - render() { + public render() { return ( <Button style={styles.actionButton} onPress={this.props.onPress} - onHoverStart={this._onHoverStart} - onHoverEnd={this._onHoverEnd}> + onHoverStart={this.onHoverStart} + onHoverEnd={this.onHoverEnd}> <ImageView height={12} width={12} @@ -105,72 +105,75 @@ export class NotificationOpenLinkAction extends Component<{ onPress: () => void ); } - _onHoverStart = () => { + private onHoverStart = () => { this.setState({ hovered: true }); }; - _onHoverEnd = () => { + private onHoverEnd = () => { this.setState({ hovered: false }); }; } export class NotificationContent extends Component { - render() { + public render() { return <View style={styles.textContainer}>{this.props.children}</View>; } } export class NotificationActions extends Component { - render() { + public render() { return <View style={styles.actionContainer}>{this.props.children}</View>; } } export class NotificationIndicator extends Component<{ type: 'success' | 'warning' | 'error' }> { - render() { + public render() { return <View style={[styles.indicator.base, styles.indicator[this.props.type]]} />; } } -type NotificationBannerProps = { +interface INotificationBannerProps { children: React.ReactNode; // Array<NotificationContent | NotificationActions>, style?: Types.ViewStyleRuleSet; visible: boolean; animationDuration: number; -}; +} -type NotificationBannerState = { +interface INotificationBannerState { contentPinnedToBottom: boolean; -}; +} export class NotificationBanner extends Component< - NotificationBannerProps, - NotificationBannerState + INotificationBannerProps, + INotificationBannerState > { - static defaultProps = { + public static defaultProps = { animationDuration: 350, }; - _containerRef = React.createRef<Animated.View>(); - _contentHeight = 0; - _heightValue = Animated.createValue(0); - _animationStyle: Types.AnimatedViewStyleRuleSet; - _animation?: Types.Animated.CompositeAnimation; - _didFinishFirstLayoutPass = false; - - state = { + public state = { contentPinnedToBottom: false, }; - constructor(props: NotificationBannerProps) { + private containerRef = React.createRef<Animated.View>(); + private contentHeight = 0; + private heightValue = Animated.createValue(0); + private animationStyle: Types.AnimatedViewStyleRuleSet; + private animation?: Types.Animated.CompositeAnimation; + private didFinishFirstLayoutPass = false; + + constructor(props: INotificationBannerProps) { super(props); - this._animationStyle = Styles.createAnimatedViewStyle({ - height: this._heightValue, + this.animationStyle = Styles.createAnimatedViewStyle({ + height: this.heightValue, }); } - shouldComponentUpdate(nextProps: NotificationBannerProps, nextState: NotificationBannerState) { + public shouldComponentUpdate( + nextProps: INotificationBannerProps, + nextState: INotificationBannerState, + ) { return ( this.props.children !== nextProps.children || this.props.visible !== nextProps.visible || @@ -178,79 +181,79 @@ export class NotificationBanner extends Component< ); } - componentDidUpdate(prevProps: NotificationBannerProps) { + public componentDidUpdate(prevProps: INotificationBannerProps) { if (prevProps.visible !== this.props.visible) { // enable drawer-like animation when changing banner's visibility this.setState({ contentPinnedToBottom: true }, () => { - this._animateHeightChanges(); + this.animateHeightChanges(); }); } } - componentWillUnmount() { - if (this._animation) { - this._animation.stop(); + public componentWillUnmount() { + if (this.animation) { + this.animation.stop(); } } - render() { + public render() { return ( <Animated.View style={[ styles.collapsible, this.state.contentPinnedToBottom ? styles.drawer : undefined, - this._animationStyle, + this.animationStyle, this.props.style, ]} - ref={this._containerRef}> - <View onLayout={this._onLayout}> + ref={this.containerRef}> + <View onLayout={this.onLayout}> <View style={styles.container}>{this.props.children}</View> </View> </Animated.View> ); } - _onLayout = ({ height }: Types.ViewOnLayoutEvent) => { - const oldHeight = this._contentHeight; - this._contentHeight = height; + private onLayout = ({ height }: Types.ViewOnLayoutEvent) => { + const oldHeight = this.contentHeight; + this.contentHeight = height; // The first layout pass should not be animated because this would cause the initially visible // notification banner to slide down each time the component is mounted. - if (this._didFinishFirstLayoutPass) { + if (this.didFinishFirstLayoutPass) { if (oldHeight !== height) { - this._animateHeightChanges(); + this.animateHeightChanges(); } } else { - this._didFinishFirstLayoutPass = true; + this.didFinishFirstLayoutPass = true; if (this.props.visible) { - this._stopAnimation(); - this._heightValue.setValue(height); + this.stopAnimation(); + this.heightValue.setValue(height); } } }; - async _animateHeightChanges() { - const containerView = this._containerRef.current; + private async animateHeightChanges() { + const containerView = this.containerRef.current; if (!containerView) { return; } - this._stopAnimation(); + this.stopAnimation(); // calculate the animation duration based on travel distance const layout = await UserInterface.measureLayoutRelativeToWindow(containerView); - const toValue = this.props.visible ? this._contentHeight : 0; - const multiplier = Math.abs(toValue - layout.height) / Math.max(1, this._contentHeight); + const toValue = this.props.visible ? this.contentHeight : 0; + const multiplier = Math.abs(toValue - layout.height) / Math.max(1, this.contentHeight); const duration = Math.ceil(this.props.animationDuration * multiplier); - const animation = Animated.timing(this._heightValue, { + const animation = Animated.timing(this.heightValue, { toValue, easing: Animated.Easing.InOut(), duration, useNativeDriver: true, }); - this._animation = animation; + this.animation = animation; animation.start(({ finished }) => { if (finished) { @@ -260,10 +263,10 @@ export class NotificationBanner extends Component< }); } - _stopAnimation() { - if (this._animation) { - this._animation.stop(); - this._animation = undefined; + private stopAnimation() { + if (this.animation) { + this.animation.stop(); + this.animation = undefined; } } } diff --git a/gui/packages/desktop/src/renderer/components/PlatformWindow.tsx b/gui/packages/desktop/src/renderer/components/PlatformWindow.tsx index e8d4330ef1..baf446fd5b 100644 --- a/gui/packages/desktop/src/renderer/components/PlatformWindow.tsx +++ b/gui/packages/desktop/src/renderer/components/PlatformWindow.tsx @@ -1,13 +1,13 @@ import * as React from 'react'; -import { Component, View, Styles } from 'reactxp'; +import { Component, Styles, View } from 'reactxp'; -type Props = { +interface IProps { arrowPosition?: number; -}; +} -export default class PlatformWindow extends Component<Props> { - render() { - let style = undefined; +export default class PlatformWindow extends Component<IProps> { + public render() { + let style; if (process.platform === 'darwin') { const arrowPosition = this.props.arrowPosition; diff --git a/gui/packages/desktop/src/renderer/components/Preferences.tsx b/gui/packages/desktop/src/renderer/components/Preferences.tsx index 256390b899..91c2c6b050 100644 --- a/gui/packages/desktop/src/renderer/components/Preferences.tsx +++ b/gui/packages/desktop/src/renderer/components/Preferences.tsx @@ -1,19 +1,19 @@ +import { HeaderTitle, SettingsHeader } from '@mullvad/components'; import * as React from 'react'; import { Component, View } from 'reactxp'; -import { SettingsHeader, HeaderTitle } from '@mullvad/components'; import * as Cell from './Cell'; -import { Layout, Container } from './Layout'; +import { Container, Layout } from './Layout'; import { + BackBarItem, NavigationBar, NavigationContainer, NavigationScrollbars, - BackBarItem, TitleBarItem, } from './NavigationBar'; -import Switch from './Switch'; import styles from './PreferencesStyles'; +import Switch from './Switch'; -export type PreferencesProps = { +export interface IPreferencesProps { autoStart: boolean; autoConnect: boolean; allowLan: boolean; @@ -27,10 +27,10 @@ export type PreferencesProps = { setStartMinimized: (startMinimized: boolean) => void; setMonochromaticIcon: (monochromaticIcon: boolean) => void; onClose: () => void; -}; +} -export default class Preferences extends Component<PreferencesProps> { - render() { +export default class Preferences extends Component<IPreferencesProps> { + public render() { return ( <Layout> <Container> @@ -50,7 +50,7 @@ export default class Preferences extends Component<PreferencesProps> { <View style={styles.preferences__content}> <Cell.Container> <Cell.Label>Launch app on start-up</Cell.Label> - <Switch isOn={this.props.autoStart} onChange={this._onChangeAutoStart} /> + <Switch isOn={this.props.autoStart} onChange={this.onChangeAutoStart} /> </Cell.Container> <View style={styles.preferences__separator} /> @@ -91,19 +91,19 @@ export default class Preferences extends Component<PreferencesProps> { ); } - _onChangeAutoStart = (autoStart: boolean) => { + private onChangeAutoStart = (autoStart: boolean) => { this.props.setAutoStart(autoStart); }; } -type MonochromaticIconProps = { +interface IMonochromaticIconProps { enable: boolean; monochromaticIcon: boolean; onChange: (value: boolean) => void; -}; +} -class MonochromaticIconToggle extends Component<MonochromaticIconProps> { - render() { +class MonochromaticIconToggle extends Component<IMonochromaticIconProps> { + public render() { if (this.props.enable) { return ( <View> @@ -120,14 +120,14 @@ class MonochromaticIconToggle extends Component<MonochromaticIconProps> { } } -type StartMinimizedProps = { +interface IStartMinimizedProps { enable: boolean; startMinimized: boolean; onChange: (value: boolean) => void; -}; +} -class StartMinimizedToggle extends Component<StartMinimizedProps> { - render() { +class StartMinimizedToggle extends Component<IStartMinimizedProps> { + public render() { if (this.props.enable) { return ( <View> diff --git a/gui/packages/desktop/src/renderer/components/RelayRow.tsx b/gui/packages/desktop/src/renderer/components/RelayRow.tsx index 1170961458..69f632e04e 100644 --- a/gui/packages/desktop/src/renderer/components/RelayRow.tsx +++ b/gui/packages/desktop/src/renderer/components/RelayRow.tsx @@ -1,14 +1,16 @@ import * as React from 'react'; import { Component, Styles } from 'reactxp'; +import { colors } from '../../config.json'; +import { compareRelayLocation, RelayLocation } from '../../shared/daemon-rpc-types'; import * as Cell from './Cell'; import RelayStatusIndicator from './RelayStatusIndicator'; -import { colors } from '../../config.json'; -type Props = { +interface IProps { + location: RelayLocation; hostname: string; selected: boolean; - onSelect?: () => void; -}; + onSelect?: (location: RelayLocation) => void; +} const styles = { base: Styles.createViewStyle({ @@ -23,19 +25,23 @@ const styles = { }), }; -export default class RelayRow extends Component<Props> { - shouldComponentUpdate(nextProps: Props) { - return !RelayRow.compareProps(this.props, nextProps); +export default class RelayRow extends Component<IProps> { + public static compareProps(oldProps: IProps, nextProps: IProps) { + return ( + oldProps.hostname === nextProps.hostname && + oldProps.selected === nextProps.selected && + compareRelayLocation(oldProps.location, nextProps.location) + ); } - static compareProps(oldProps: Props, nextProps: Props) { - return oldProps.hostname === nextProps.hostname && oldProps.selected === nextProps.selected; + public shouldComponentUpdate(nextProps: IProps) { + return !RelayRow.compareProps(this.props, nextProps); } - render() { + public render() { return ( <Cell.CellButton - onPress={this._handlePress} + onPress={this.handlePress} cellHoverStyle={this.props.selected ? styles.selected : undefined} style={[styles.base, this.props.selected ? styles.selected : undefined]}> <RelayStatusIndicator isActive={true} isSelected={this.props.selected} /> @@ -45,9 +51,9 @@ export default class RelayRow extends Component<Props> { ); } - _handlePress = () => { + private handlePress = () => { if (this.props.onSelect) { - this.props.onSelect(); + this.props.onSelect(this.props.location); } }; } diff --git a/gui/packages/desktop/src/renderer/components/RelayStatusIndicator.tsx b/gui/packages/desktop/src/renderer/components/RelayStatusIndicator.tsx index d256352bf1..3409d78331 100644 --- a/gui/packages/desktop/src/renderer/components/RelayStatusIndicator.tsx +++ b/gui/packages/desktop/src/renderer/components/RelayStatusIndicator.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { Component, Styles, View } from 'reactxp'; -import * as Cell from './Cell'; import { colors } from '../../config.json'; +import * as Cell from './Cell'; const styles = { relay_status: Styles.createViewStyle({ @@ -23,13 +23,13 @@ const styles = { }), }; -type Props = { +interface IProps { isActive: boolean; isSelected: boolean; -}; +} -export default class RelayStatusIndicator extends Component<Props> { - render() { +export default class RelayStatusIndicator extends Component<IProps> { + public render() { return this.props.isSelected ? ( <Cell.Icon style={styles.tick_icon} diff --git a/gui/packages/desktop/src/renderer/components/SelectLocation.tsx b/gui/packages/desktop/src/renderer/components/SelectLocation.tsx index 620ef504b0..ba7a24a79e 100644 --- a/gui/packages/desktop/src/renderer/components/SelectLocation.tsx +++ b/gui/packages/desktop/src/renderer/components/SelectLocation.tsx @@ -1,46 +1,49 @@ +import { HeaderSubTitle, HeaderTitle, SettingsHeader } from '@mullvad/components'; import * as React from 'react'; import ReactDOM from 'react-dom'; -import { View, Component } from 'reactxp'; -import { SettingsHeader, HeaderTitle, HeaderSubTitle } from '@mullvad/components'; -import { Layout, Container } from './Layout'; +import { Component, View } from 'reactxp'; import CustomScrollbars from './CustomScrollbars'; +import { Container, Layout } from './Layout'; import { + CloseBarItem, + NavigationBar, NavigationContainer, NavigationScrollbars, - NavigationBar, - CloseBarItem, TitleBarItem, } from './NavigationBar'; import styles from './SelectLocationStyles'; -import CountryRow from './CountryRow'; import CityRow from './CityRow'; +import CountryRow from './CountryRow'; import RelayRow from './RelayRow'; -import { RelaySettingsRedux, RelayLocationRedux } from '../redux/settings/reducers'; -import { RelayLocation } from '../../shared/daemon-rpc-types'; +import { + compareRelayLocation, + compareRelayLocationLoose, + RelayLocation, +} from '../../shared/daemon-rpc-types'; +import { IRelayLocationRedux, RelaySettingsRedux } from '../redux/settings/reducers'; -type Props = { +interface IProps { relaySettings: RelaySettingsRedux; - relayLocations: RelayLocationRedux[]; + relayLocations: IRelayLocationRedux[]; onClose: () => void; onSelect: (location: RelayLocation) => void; -}; +} -type State = { +interface IState { selectedLocation?: RelayLocation; expandedItems: RelayLocation[]; -}; - -export default class SelectLocation extends Component<Props, State> { - _selectedCellRef = React.createRef<React.ReactNode>(); - _scrollViewRef = React.createRef<CustomScrollbars>(); +} - state: State = { +export default class SelectLocation extends Component<IProps, IState> { + public state: IState = { expandedItems: [], }; + private selectedCellRef = React.createRef<React.ReactNode>(); + private scrollViewRef = React.createRef<CustomScrollbars>(); - constructor(props: Props) { + constructor(props: IProps) { super(props); if ('normal' in this.props.relaySettings) { @@ -64,7 +67,7 @@ export default class SelectLocation extends Component<Props, State> { } } - componentDidUpdate(oldProps: Props) { + public componentDidUpdate(oldProps: IProps) { const currentLocation = this.state.selectedLocation; let newLocation = 'normal' in this.props.relaySettings ? this.props.relaySettings.normal.location : undefined; @@ -81,17 +84,17 @@ export default class SelectLocation extends Component<Props, State> { } if ( - !compareLocationLoose(oldLocation, newLocation) && - !compareLocationLoose(currentLocation, newLocation) + !compareRelayLocationLoose(oldLocation, newLocation) && + !compareRelayLocationLoose(currentLocation, newLocation) ) { this.setState({ selectedLocation: newLocation }); } } - componentDidMount() { + public componentDidMount() { // restore scroll to the selected cell - const cell = this._selectedCellRef.current; - const scrollView = this._scrollViewRef.current; + const cell = this.selectedCellRef.current; + const scrollView = this.scrollViewRef.current; if (scrollView && cell) { // TODO: Fix the browser specific code const cellDOMNode = ReactDOM.findDOMNode(cell as Element); @@ -101,7 +104,7 @@ export default class SelectLocation extends Component<Props, State> { } } - render() { + public render() { return ( <Layout> <Container> @@ -112,7 +115,7 @@ export default class SelectLocation extends Component<Props, State> { <TitleBarItem>{'Select location'}</TitleBarItem> </NavigationBar> <View style={styles.container}> - <NavigationScrollbars ref={this._scrollViewRef}> + <NavigationScrollbars ref={this.scrollViewRef}> <View style={styles.content}> <SettingsHeader style={styles.subtitle_header}> <HeaderTitle>Select location</HeaderTitle> @@ -123,42 +126,42 @@ export default class SelectLocation extends Component<Props, State> { </SettingsHeader> {this.props.relayLocations.map((relayCountry) => { - const location: RelayLocation = { country: relayCountry.code }; + const countryLocation: RelayLocation = { country: relayCountry.code }; return ( <CountryRow - key={getLocationKey(location)} + key={getLocationKey(countryLocation)} name={relayCountry.name} hasActiveRelays={relayCountry.hasActiveRelays} - expanded={this._isExpanded(location)} - onSelect={() => this._handleSelection(location)} - onExpand={(expand) => this._handleExpand(location, expand)} - {...this._getCommonCellProps(location)}> + expanded={this.isExpanded(countryLocation)} + onSelect={this.handleSelection} + onExpand={this.handleExpand} + {...this.getCommonCellProps(countryLocation)}> {relayCountry.cities.map((relayCity) => { - const location: RelayLocation = { + const cityLocation: RelayLocation = { city: [relayCountry.code, relayCity.code], }; return ( <CityRow - key={getLocationKey(location)} + key={getLocationKey(cityLocation)} name={relayCity.name} hasActiveRelays={relayCity.hasActiveRelays} - expanded={this._isExpanded(location)} - onSelect={() => this._handleSelection(location)} - onExpand={(expand) => this._handleExpand(location, expand)} - {...this._getCommonCellProps(location)}> + expanded={this.isExpanded(cityLocation)} + onSelect={this.handleSelection} + onExpand={this.handleExpand} + {...this.getCommonCellProps(cityLocation)}> {relayCity.relays.map((relay) => { - const location: RelayLocation = { + const relayLocation: RelayLocation = { hostname: [relayCountry.code, relayCity.code, relay.hostname], }; return ( <RelayRow - key={getLocationKey(location)} + key={getLocationKey(relayLocation)} hostname={relay.hostname} - onSelect={() => this._handleSelection(location)} - {...this._getCommonCellProps(location)} + onSelect={this.handleSelection} + {...this.getCommonCellProps(relayLocation)} /> ); })} @@ -178,25 +181,29 @@ export default class SelectLocation extends Component<Props, State> { ); } - _isExpanded(relayLocation: RelayLocation) { - return this.state.expandedItems.some((location) => compareLocation(location, relayLocation)); + private isExpanded(relayLocation: RelayLocation) { + return this.state.expandedItems.some((location) => + compareRelayLocation(location, relayLocation), + ); } - _isSelected(relayLocation: RelayLocation) { - return compareLocationLoose(this.state.selectedLocation, relayLocation); + private isSelected(relayLocation: RelayLocation) { + return compareRelayLocationLoose(this.state.selectedLocation, relayLocation); } - _handleSelection = (location: RelayLocation) => { - if (!compareLocationLoose(this.state.selectedLocation, location)) { + private handleSelection = (location: RelayLocation) => { + if (!compareRelayLocationLoose(this.state.selectedLocation, location)) { this.setState({ selectedLocation: location }, () => { this.props.onSelect(location); }); } }; - _handleExpand = (location: RelayLocation, expand: boolean) => { + private handleExpand = (location: RelayLocation, expand: boolean) => { this.setState((state) => { - const expandedItems = state.expandedItems.filter((item) => !compareLocation(item, location)); + const expandedItems = state.expandedItems.filter( + (item) => !compareRelayLocation(item, location), + ); if (expand) { expandedItems.push(location); @@ -209,11 +216,13 @@ export default class SelectLocation extends Component<Props, State> { }); }; - _getCommonCellProps<T>(location: RelayLocation): { selected: boolean; ref?: React.RefObject<T> } { - const selected = this._isSelected(location); - const ref = selected ? (this._selectedCellRef as React.RefObject<T>) : undefined; + private getCommonCellProps<T>( + location: RelayLocation, + ): { location: RelayLocation; selected: boolean; ref?: React.RefObject<T> } { + const selected = this.isSelected(location); + const ref = selected ? (this.selectedCellRef as React.RefObject<T>) : undefined; - return { ref, selected }; + return { ref, selected, location }; } } @@ -230,27 +239,3 @@ function getLocationKey(location: RelayLocation): string { return ([] as string[]).concat(components).join('-'); } - -function compareLocation(lhs: RelayLocation, rhs: RelayLocation) { - if ('country' in lhs && 'country' in rhs && lhs.country && rhs.country) { - return lhs.country === rhs.country; - } else if ('city' in lhs && 'city' in rhs && lhs.city && rhs.city) { - return lhs.city[0] === rhs.city[0] && lhs.city[1] === rhs.city[1]; - } else if ('hostname' in lhs && 'hostname' in rhs && lhs.hostname && rhs.hostname) { - return ( - lhs.hostname[0] === rhs.hostname[0] && - lhs.hostname[1] === rhs.hostname[1] && - lhs.hostname[2] === rhs.hostname[2] - ); - } else { - return false; - } -} - -function compareLocationLoose(lhs?: RelayLocation, rhs?: RelayLocation) { - if (lhs && rhs) { - return compareLocation(lhs, rhs); - } else { - return lhs === rhs; - } -} diff --git a/gui/packages/desktop/src/renderer/components/Settings.tsx b/gui/packages/desktop/src/renderer/components/Settings.tsx index 555ad94954..913911c410 100644 --- a/gui/packages/desktop/src/renderer/components/Settings.tsx +++ b/gui/packages/desktop/src/renderer/components/Settings.tsx @@ -1,19 +1,19 @@ +import { HeaderTitle, ImageView, SettingsHeader } from '@mullvad/components'; import * as React from 'react'; import { Component, Text, View } from 'reactxp'; -import { ImageView, SettingsHeader, HeaderTitle } from '@mullvad/components'; +import { colors, links } from '../../config.json'; +import AccountExpiry from '../lib/account-expiry'; import * as AppButton from './AppButton'; import * as Cell from './Cell'; -import { Layout, Container } from './Layout'; +import { Container, Layout } from './Layout'; import { + CloseBarItem, NavigationBar, NavigationContainer, NavigationScrollbars, - CloseBarItem, TitleBarItem, } from './NavigationBar'; import styles from './SettingsStyles'; -import AccountExpiry from '../lib/account-expiry'; -import { colors, links } from '../../config.json'; import { LoginState } from '../redux/account/reducers'; @@ -52,11 +52,11 @@ export default class Settings extends Component<IProps> { <HeaderTitle>Settings</HeaderTitle> </SettingsHeader> <View> - {this._renderTopButtons()} - {this._renderMiddleButtons()} - {this._renderBottomButtons()} + {this.renderTopButtons()} + {this.renderMiddleButtons()} + {this.renderBottomButtons()} </View> - {this._renderQuitButton()} + {this.renderQuitButton()} </View> </NavigationScrollbars> </View> @@ -67,7 +67,15 @@ export default class Settings extends Component<IProps> { ); } - private _renderTopButtons() { + private renderQuitButton() { + return ( + <View style={styles.settings__footer}> + <AppButton.RedButton onPress={this.props.onQuit}>{'Quit app'}</AppButton.RedButton> + </View> + ); + } + + private renderTopButtons() { const isLoggedIn = this.props.loginState === 'ok'; if (!isLoggedIn) { return null; @@ -111,7 +119,7 @@ export default class Settings extends Component<IProps> { ); } - private _renderMiddleButtons() { + private renderMiddleButtons() { let icon; let footer; if (!this.props.consistentVersion || !this.props.upToDateVersion) { @@ -137,7 +145,7 @@ export default class Settings extends Component<IProps> { return ( <View> - <Cell.CellButton disabled={this.props.isOffline} onPress={this._openDownloadLink}> + <Cell.CellButton disabled={this.props.isOffline} onPress={this.openDownloadLink}> {icon} <Cell.Label>App version</Cell.Label> <Cell.SubText>{this.props.appVersion}</Cell.SubText> @@ -148,10 +156,10 @@ export default class Settings extends Component<IProps> { ); } - private _openDownloadLink = () => this.props.onExternalLink(links.download); - private _openFaqLink = () => this.props.onExternalLink(links.faq); + private openDownloadLink = () => this.props.onExternalLink(links.download); + private openFaqLink = () => this.props.onExternalLink(links.faq); - private _renderBottomButtons() { + private renderBottomButtons() { return ( <View> <Cell.CellButton onPress={this.props.onViewSupport}> @@ -159,19 +167,11 @@ export default class Settings extends Component<IProps> { <Cell.Icon height={12} width={7} source="icon-chevron" /> </Cell.CellButton> - <Cell.CellButton disabled={this.props.isOffline} onPress={this._openFaqLink}> + <Cell.CellButton disabled={this.props.isOffline} onPress={this.openFaqLink}> <Cell.Label>{'FAQs & Guides'}</Cell.Label> <Cell.Icon height={16} width={16} source="icon-extLink" /> </Cell.CellButton> </View> ); } - - _renderQuitButton() { - return ( - <View style={styles.settings__footer}> - <AppButton.RedButton onPress={this.props.onQuit}>{'Quit app'}</AppButton.RedButton> - </View> - ); - } } diff --git a/gui/packages/desktop/src/renderer/components/Support.tsx b/gui/packages/desktop/src/renderer/components/Support.tsx index cb432839ca..95e96bebe8 100644 --- a/gui/packages/desktop/src/renderer/components/Support.tsx +++ b/gui/packages/desktop/src/renderer/components/Support.tsx @@ -1,21 +1,21 @@ -import * as React from 'react'; -import { Component, Text, View, TextInput } from 'reactxp'; import { - ImageView, - SettingsHeader, - HeaderTitle, HeaderSubTitle, + HeaderTitle, + ImageView, + ModalAlert, ModalContainer, ModalContent, - ModalAlert, + SettingsHeader, } from '@mullvad/components'; +import * as React from 'react'; +import { Component, Text, TextInput, View } from 'reactxp'; import * as AppButton from './AppButton'; -import { Layout, Container } from './Layout'; -import { NavigationBar, BackBarItem } from './NavigationBar'; +import { Container, Layout } from './Layout'; +import { BackBarItem, NavigationBar } from './NavigationBar'; import styles from './SupportStyles'; import { AccountToken } from '../../shared/daemon-rpc-types'; -import { SupportReportForm } from '../redux/support/actions'; +import { ISupportReportForm } from '../redux/support/actions'; enum SendState { Initial, @@ -25,37 +25,37 @@ enum SendState { Failed, } -type SupportState = { +interface ISupportState { email: string; message: string; savedReport?: string; sendState: SendState; -}; +} -type SupportProps = { +interface ISupportProps { defaultEmail: string; defaultMessage: string; - accountHistory: Array<AccountToken>; + accountHistory: AccountToken[]; isOffline: boolean; onClose: () => void; viewLog: (path: string) => void; - saveReportForm: (form: SupportReportForm) => void; + saveReportForm: (form: ISupportReportForm) => void; clearReportForm: () => void; - collectProblemReport: (accountsToRedact: Array<string>) => Promise<string>; + collectProblemReport: (accountsToRedact: string[]) => Promise<string>; sendProblemReport: (email: string, message: string, savedReport: string) => Promise<void>; -}; +} -export default class Support extends Component<SupportProps, SupportState> { - state = { +export default class Support extends Component<ISupportProps, ISupportState> { + public state = { email: '', message: '', savedReport: undefined, sendState: SendState.Initial, }; - _collectLogPromise?: Promise<string>; + private collectLogPromise?: Promise<string>; - constructor(props: SupportProps) { + constructor(props: ISupportProps) { super(props); // seed initial data from props @@ -63,68 +63,39 @@ export default class Support extends Component<SupportProps, SupportState> { this.state.message = props.defaultMessage; } - validate() { + public validate() { return this.state.message.trim().length > 0; } - onChangeEmail = (email: string) => { - this.setState({ email: email }, () => { - this._saveFormData(); + public onChangeEmail = (email: string) => { + this.setState({ email }, () => { + this.saveFormData(); }); }; - onChangeDescription = (description: string) => { + public onChangeDescription = (description: string) => { this.setState({ message: description }, () => { - this._saveFormData(); + this.saveFormData(); }); }; - onViewLog = async (): Promise<void> => { + public onViewLog = async (): Promise<void> => { try { - const reportPath = await this._collectLog(); + const reportPath = await this.collectLog(); this.props.viewLog(reportPath); } catch (error) { // TODO: handle error } }; - _saveFormData() { - this.props.saveReportForm({ - email: this.state.email, - message: this.state.message, - }); - } - - async _collectLog(): Promise<string> { - if (this._collectLogPromise) { - return this._collectLogPromise; - } else { - const collectPromise = this.props.collectProblemReport(this.props.accountHistory); - - // save promise to prevent subsequent requests - this._collectLogPromise = collectPromise; - - try { - const reportPath = await collectPromise; - return new Promise((resolve) => { - this.setState({ savedReport: reportPath }, () => resolve(reportPath)); - }); - } catch (error) { - this._collectLogPromise = undefined; - - throw error; - } - } - } - - onSend = async (): Promise<void> => { + public onSend = async (): Promise<void> => { switch (this.state.sendState) { case SendState.Initial: if (this.state.email.length === 0) { this.setState({ sendState: SendState.Confirm }); } else { try { - await this._sendReport(); + await this.sendReport(); } catch (error) { // No-op } @@ -133,7 +104,7 @@ export default class Support extends Component<SupportProps, SupportState> { case SendState.Confirm: try { - await this._sendReport(); + await this.sendReport(); } catch (error) { // No-op } @@ -146,31 +117,11 @@ export default class Support extends Component<SupportProps, SupportState> { return Promise.resolve(); }; - onCancelConfirmation = () => { + public onCancelConfirmation = () => { this.setState({ sendState: SendState.Initial }); }; - _sendReport(): Promise<void> { - return new Promise((resolve, reject) => { - this.setState({ sendState: SendState.Loading }, async () => { - try { - const { email, message } = this.state; - const reportPath = await this._collectLog(); - await this.props.sendProblemReport(email, message, reportPath); - this.props.clearReportForm(); - this.setState({ sendState: SendState.Success }, () => { - resolve(); - }); - } catch (error) { - this.setState({ sendState: SendState.Failed }, () => { - reject(error); - }); - } - }); - }); - } - - render() { + public render() { const { sendState } = this.state; const header = ( <SettingsHeader> @@ -185,7 +136,7 @@ export default class Support extends Component<SupportProps, SupportState> { </SettingsHeader> ); - const content = this._renderContent(); + const content = this.renderContent(); return ( <Layout> @@ -203,7 +154,7 @@ export default class Support extends Component<SupportProps, SupportState> { </View> </ModalContent> {sendState === SendState.Confirm ? ( - <ModalAlert>{this._renderConfirm()}</ModalAlert> + <ModalAlert>{this.renderConfirm()}</ModalAlert> ) : ( undefined )} @@ -213,27 +164,76 @@ export default class Support extends Component<SupportProps, SupportState> { ); } - _renderContent() { + private saveFormData() { + this.props.saveReportForm({ + email: this.state.email, + message: this.state.message, + }); + } + + private async collectLog(): Promise<string> { + if (this.collectLogPromise) { + return this.collectLogPromise; + } else { + const collectPromise = this.props.collectProblemReport(this.props.accountHistory); + + // save promise to prevent subsequent requests + this.collectLogPromise = collectPromise; + + try { + const reportPath = await collectPromise; + return new Promise((resolve) => { + this.setState({ savedReport: reportPath }, () => resolve(reportPath)); + }); + } catch (error) { + this.collectLogPromise = undefined; + + throw error; + } + } + } + + private sendReport(): Promise<void> { + return new Promise((resolve, reject) => { + this.setState({ sendState: SendState.Loading }, async () => { + try { + const { email, message } = this.state; + const reportPath = await this.collectLog(); + await this.props.sendProblemReport(email, message, reportPath); + this.props.clearReportForm(); + this.setState({ sendState: SendState.Success }, () => { + resolve(); + }); + } catch (error) { + this.setState({ sendState: SendState.Failed }, () => { + reject(error); + }); + } + }); + }); + } + + private renderContent() { switch (this.state.sendState) { case SendState.Initial: case SendState.Confirm: - return this._renderForm(); + return this.renderForm(); case SendState.Loading: - return this._renderLoading(); + return this.renderLoading(); case SendState.Success: - return this._renderSent(); + return this.renderSent(); case SendState.Failed: - return this._renderFailed(); + return this.renderFailed(); default: return null; } } - _renderConfirm() { + private renderConfirm() { return <ConfirmNoEmailDialog onConfirm={this.onSend} onDismiss={this.onCancelConfirmation} />; } - _renderForm() { + private renderForm() { return ( <View style={styles.support__content}> <View style={styles.support__form}> @@ -271,7 +271,7 @@ export default class Support extends Component<SupportProps, SupportState> { ); } - _renderLoading() { + private renderLoading() { return ( <View style={styles.support__content}> <View style={styles.support__form}> @@ -287,7 +287,7 @@ export default class Support extends Component<SupportProps, SupportState> { ); } - _renderSent() { + private renderSent() { return ( <View style={styles.support__content}> <View style={styles.support__form}> @@ -311,7 +311,7 @@ export default class Support extends Component<SupportProps, SupportState> { ); } - _renderFailed() { + private renderFailed() { return ( <View style={styles.support__content}> <View style={styles.support__form}> @@ -329,9 +329,7 @@ export default class Support extends Component<SupportProps, SupportState> { </View> </View> <View style={styles.support__footer}> - <AppButton.BlueButton - style={styles.edit_message_button} - onPress={() => this.setState({ sendState: SendState.Initial })}> + <AppButton.BlueButton style={styles.edit_message_button} onPress={this.handleEditMessage}> {'Edit message'} </AppButton.BlueButton> <AppButton.GreenButton onPress={this.onSend}>Try again</AppButton.GreenButton> @@ -339,15 +337,19 @@ export default class Support extends Component<SupportProps, SupportState> { </View> ); } + + private handleEditMessage = () => { + this.setState({ sendState: SendState.Initial }); + }; } -type ConfirmNoEmailDialogProps = { +interface IConfirmNoEmailDialogProps { onConfirm: () => void; onDismiss: () => void; -}; +} -class ConfirmNoEmailDialog extends Component<ConfirmNoEmailDialogProps> { - render() { +class ConfirmNoEmailDialog extends Component<IConfirmNoEmailDialogProps> { + public render() { return ( <View style={styles.confirm_no_email_background}> <View style={styles.confirm_no_email_dialog}> @@ -355,10 +357,8 @@ class ConfirmNoEmailDialog extends Component<ConfirmNoEmailDialogProps> { You are about to send the problem report without a way for us to get back to you. If you want an answer to your report you will have to enter an email address. </Text> - <AppButton.GreenButton onPress={this.props.onConfirm}> - {'Send anyway'} - </AppButton.GreenButton> - <AppButton.RedButton onPress={this._dismiss} style={styles.confirm_no_email_back_button}> + <AppButton.GreenButton onPress={this.confirm}>{'Send anyway'}</AppButton.GreenButton> + <AppButton.RedButton onPress={this.dismiss} style={styles.confirm_no_email_back_button}> {'Back'} </AppButton.RedButton> </View> @@ -366,11 +366,11 @@ class ConfirmNoEmailDialog extends Component<ConfirmNoEmailDialogProps> { ); } - _confirm = () => { + private confirm = () => { this.props.onConfirm(); }; - _dismiss = () => { + private dismiss = () => { this.props.onDismiss(); }; } diff --git a/gui/packages/desktop/src/renderer/components/SvgMap.tsx b/gui/packages/desktop/src/renderer/components/SvgMap.tsx index eb07d74677..25c77de7f2 100644 --- a/gui/packages/desktop/src/renderer/components/SvgMap.tsx +++ b/gui/packages/desktop/src/renderer/components/SvgMap.tsx @@ -1,27 +1,27 @@ +import { geoTimes } from 'd3-geo-projection'; +import rbush from 'rbush'; import * as React from 'react'; import { ComposableMap, - ZoomableGroup, Geographies, Geography, - Markers, Marker, + Markers, + ZoomableGroup, } from 'react-simple-maps'; -import rbush from 'rbush'; -import { geoTimes } from 'd3-geo-projection'; import geographyData from '../../../assets/geo/geometry.json'; import statesProvincesLinesData from '../../../assets/geo/states-provinces-lines.json'; -import countryTreeData from '../../../assets/geo/countries.rbush.json'; import cityTreeData from '../../../assets/geo/cities.rbush.json'; +import countryTreeData from '../../../assets/geo/countries.rbush.json'; import geometryTreeData from '../../../assets/geo/geometry.rbush.json'; import statesProvincesLinesTreeData from '../../../assets/geo/states-provinces-lines.rbush.json'; // Infer the GeoProjection type from the `geoTimes()` return value type GeoProjection = ReturnType<typeof geoTimes>; -interface CountryLeaf extends rbush.BBox { +interface ICountryLeaf extends rbush.BBox { id: string; properties: { name: string; @@ -32,7 +32,7 @@ interface CountryLeaf extends rbush.BBox { }; } -interface CityLeaf extends rbush.BBox { +interface ICityLeaf extends rbush.BBox { id: string; properties: { scalerank: number; @@ -46,24 +46,24 @@ interface CityLeaf extends rbush.BBox { }; } -interface GeometryLeaf extends rbush.BBox { +interface IGeometryLeaf extends rbush.BBox { id: string; } -interface ProvinceAndStateLineLeaf extends rbush.BBox { +interface IProvinceAndStateLineLeaf extends rbush.BBox { id: string; } -const countryTree = rbush<CountryLeaf>().fromJSON(countryTreeData); -const cityTree = rbush<CityLeaf>().fromJSON(cityTreeData); -const geometryTree = rbush<GeometryLeaf>().fromJSON(geometryTreeData); -const provincesStatesLinesTree = rbush<ProvinceAndStateLineLeaf>().fromJSON( +const countryTree = rbush<ICountryLeaf>().fromJSON(countryTreeData); +const cityTree = rbush<ICityLeaf>().fromJSON(cityTreeData); +const geometryTree = rbush<IGeometryLeaf>().fromJSON(geometryTreeData); +const provincesStatesLinesTree = rbush<IProvinceAndStateLineLeaf>().fromJSON( statesProvincesLinesTreeData, ); type BBox = [number, number, number, number]; -export type Props = { +export interface IProps { width: number; height: number; center: [number, number]; // longitude, latitude @@ -71,23 +71,23 @@ export type Props = { zoomLevel: number; showMarker: boolean; markerImagePath: string; -}; +} -type State = { +interface IState { zoomCenter: [number, number]; zoomLevel: number; - visibleCities: CityLeaf[]; - visibleCountries: CountryLeaf[]; - visibleGeometry: GeometryLeaf[]; - visibleStatesProvincesLines: ProvinceAndStateLineLeaf[]; + visibleCities: ICityLeaf[]; + visibleCountries: ICountryLeaf[]; + visibleGeometry: IGeometryLeaf[]; + visibleStatesProvincesLines: IProvinceAndStateLineLeaf[]; viewportBbox: BBox; -}; +} const MOVE_SPEED = 2000; // @TODO: Calculate zoom level based on (center + span) (aka MKCoordinateSpan) -export default class SvgMap extends React.Component<Props, State> { - state: State = { +export default class SvgMap extends React.Component<IProps, IState> { + public state: IState = { zoomCenter: [0, 0], zoomLevel: 1, visibleCities: [], @@ -97,23 +97,23 @@ export default class SvgMap extends React.Component<Props, State> { viewportBbox: [0, 0, 0, 0], }; - _projectionConfig = { + private projectionConfig = { scale: 160, }; - constructor(props: Props) { + constructor(props: IProps) { super(props); - this.state = this._getNextState(null, props); + this.state = this.getNextState(null, props); } - UNSAFE_componentWillReceiveProps(nextProps: Props) { - if (this._shouldInvalidateState(nextProps)) { - this.setState((prevState) => this._getNextState(prevState, nextProps)); + public UNSAFE_componentWillReceiveProps(nextProps: IProps) { + if (this.shouldInvalidateState(nextProps)) { + this.setState((prevState) => this.getNextState(prevState, nextProps)); } } - shouldComponentUpdate(nextProps: Props, nextState: State) { + public shouldComponentUpdate(nextProps: IProps, nextState: IState) { return ( this.props.width !== nextProps.width || this.props.height !== nextProps.height || @@ -129,7 +129,7 @@ export default class SvgMap extends React.Component<Props, State> { ); } - render() { + public render() { const mapStyle = { width: '100%', height: '100%', @@ -140,7 +140,7 @@ export default class SvgMap extends React.Component<Props, State> { transition: `transform ${MOVE_SPEED}ms ease-in-out`, }; - const geographyStyle = this._mergeRsmStyle({ + const geographyStyle = this.mergeRsmStyle({ default: { fill: '#294d73', stroke: '#192e45', @@ -148,7 +148,7 @@ export default class SvgMap extends React.Component<Props, State> { }, }); - const stateProvinceLineStyle = this._mergeRsmStyle({ + const stateProvinceLineStyle = this.mergeRsmStyle({ default: { fill: 'transparent', stroke: '#192e45', @@ -156,7 +156,7 @@ export default class SvgMap extends React.Component<Props, State> { }, }); - const markerStyle = this._mergeRsmStyle({ + const markerStyle = this.mergeRsmStyle({ default: { transition: `transform ${MOVE_SPEED}ms ease-in-out`, }, @@ -201,8 +201,8 @@ export default class SvgMap extends React.Component<Props, State> { width={this.props.width} height={this.props.height} style={mapStyle} - projection={this._getProjection} - projectionConfig={this._projectionConfig}> + projection={this.getProjection} + projectionConfig={this.projectionConfig}> <ZoomableGroup center={this.state.zoomCenter} zoom={this.state.zoomLevel} @@ -213,7 +213,7 @@ export default class SvgMap extends React.Component<Props, State> { return this.state.visibleGeometry.map(({ id }) => ( <Geography key={id} - geography={geographies[parseInt(id)]} + geography={geographies[parseInt(id, 10)]} projection={projection} style={geographyStyle} /> @@ -225,7 +225,7 @@ export default class SvgMap extends React.Component<Props, State> { return this.state.visibleStatesProvincesLines.map(({ id }) => ( <Geography key={id} - geography={geographies[parseInt(id)]} + geography={geographies[parseInt(id, 10)]} projection={projection} style={stateProvinceLineStyle} /> @@ -238,7 +238,7 @@ export default class SvgMap extends React.Component<Props, State> { ); } - _mergeRsmStyle(style: { [key: string]: any }) { + private mergeRsmStyle(style: { [key: string]: any }) { const defaultStyle = style.default || {}; return { default: defaultStyle, @@ -247,7 +247,7 @@ export default class SvgMap extends React.Component<Props, State> { }; } - _getProjection( + private getProjection( width: number, height: number, config: { @@ -271,7 +271,7 @@ export default class SvgMap extends React.Component<Props, State> { .precision(precision); } - _getZoomCenter( + private getZoomCenter( center: [number, number], offset: [number, number], projection: GeoProjection, @@ -281,7 +281,7 @@ export default class SvgMap extends React.Component<Props, State> { return projection.invert!([pos[0] + offset[0] / zoom, pos[1] + offset[1] / zoom])!; } - _getViewportGeoBoundingBox( + private getViewportGeoBoundingBox( centerCoordinate: [number, number], width: number, height: number, @@ -304,7 +304,7 @@ export default class SvgMap extends React.Component<Props, State> { ]; } - _shouldInvalidateState(nextProps: Props) { + private shouldInvalidateState(nextProps: IProps) { const oldProps = this.props; return ( oldProps.width !== nextProps.width || @@ -317,12 +317,12 @@ export default class SvgMap extends React.Component<Props, State> { ); } - _getNextState(prevState: State | null, nextProps: Props): State { + private getNextState(prevState: IState | null, nextProps: IProps): IState { const { width, height, center, offset, zoomLevel } = nextProps; - const projection = this._getProjection(width, height, this._projectionConfig); - const zoomCenter = this._getZoomCenter(center, offset, projection, zoomLevel); - const viewportBbox = this._getViewportGeoBoundingBox( + const projection = this.getProjection(width, height, this.projectionConfig); + const zoomCenter = this.getZoomCenter(center, offset, projection, zoomLevel); + const viewportBbox = this.getViewportGeoBoundingBox( zoomCenter, width, height, diff --git a/gui/packages/desktop/src/renderer/components/Switch.tsx b/gui/packages/desktop/src/renderer/components/Switch.tsx index d74efd97a6..eef588f44b 100644 --- a/gui/packages/desktop/src/renderer/components/Switch.tsx +++ b/gui/packages/desktop/src/renderer/components/Switch.tsx @@ -3,34 +3,57 @@ import * as React from 'react'; const CLICK_TIMEOUT = 1000; const MOVE_THRESHOLD = 10; -export type SwitchProps = { +interface IProps { className?: string; isOn: boolean; onChange?: (isOn: boolean) => void; -}; +} -type State = { +interface IState { ignoreChange: boolean; initialPos: { x: number; y: number }; startTime?: number; -}; +} -export default class Switch extends React.Component<SwitchProps, State> { - static defaultProps: SwitchProps = { +export default class Switch extends React.Component<IProps, IState> { + public static defaultProps: Partial<IProps> = { isOn: false, onChange: undefined, }; - state = { + public state: IState = { ignoreChange: false, initialPos: { x: 0, y: 0 }, startTime: undefined, }; - isCapturingMouseEvents = false; - ref = React.createRef<HTMLInputElement>(); + public isCapturingMouseEvents = false; + public ref = React.createRef<HTMLInputElement>(); + + public componentWillUnmount() { + // guard from abrupt programmatic unmount + if (this.isCapturingMouseEvents) { + this.stopCapturingMouseEvents(); + } + } + + public render() { + const { isOn, onChange, ...otherProps } = this.props; + const className = ('switch ' + (otherProps.className || '')).trim(); + return ( + <input + {...otherProps} + type="checkbox" + ref={this.ref} + className={className} + checked={isOn} + onMouseDown={this.handleMouseDown} + onChange={this.handleChange} + /> + ); + } - handleMouseDown = (e: React.MouseEvent<HTMLInputElement>) => { + private handleMouseDown = (e: React.MouseEvent<HTMLInputElement>) => { const { clientX: x, clientY: y } = e; this.startCapturingMouseEvents(); this.setState({ @@ -39,7 +62,7 @@ export default class Switch extends React.Component<SwitchProps, State> { }); }; - handleMouseMove = (e: MouseEvent) => { + private handleMouseMove = (e: MouseEvent) => { const inputElement = this.ref.current; const { x: x0 } = this.state.initialPos; const { clientX: x, clientY: y } = e; @@ -72,11 +95,11 @@ export default class Switch extends React.Component<SwitchProps, State> { } }; - handleMouseUp = () => { + private handleMouseUp = () => { this.stopCapturingMouseEvents(); }; - handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { + private handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { const startTime = this.state.startTime; const eventTarget = e.target; @@ -96,14 +119,14 @@ export default class Switch extends React.Component<SwitchProps, State> { } }; - notify(isOn: boolean) { + private notify(isOn: boolean) { const onChange = this.props.onChange; if (onChange) { onChange(isOn); } } - startCapturingMouseEvents() { + private startCapturingMouseEvents() { if (this.isCapturingMouseEvents) { throw new Error('startCapturingMouseEvents() is called out of order.'); } @@ -112,7 +135,7 @@ export default class Switch extends React.Component<SwitchProps, State> { this.isCapturingMouseEvents = true; } - stopCapturingMouseEvents() { + private stopCapturingMouseEvents() { if (!this.isCapturingMouseEvents) { throw new Error('stopCapturingMouseEvents() is called out of order.'); } @@ -120,27 +143,4 @@ export default class Switch extends React.Component<SwitchProps, State> { document.removeEventListener('mouseup', this.handleMouseUp); this.isCapturingMouseEvents = false; } - - componentWillUnmount() { - // guard from abrupt programmatic unmount - if (this.isCapturingMouseEvents) { - this.stopCapturingMouseEvents(); - } - } - - render() { - const { isOn, onChange, ...otherProps } = this.props; - const className = ('switch ' + (otherProps.className || '')).trim(); - return ( - <input - {...otherProps} - type="checkbox" - ref={this.ref} - className={className} - checked={isOn} - onMouseDown={this.handleMouseDown} - onChange={this.handleChange} - /> - ); - } } diff --git a/gui/packages/desktop/src/renderer/components/TransitionContainer.tsx b/gui/packages/desktop/src/renderer/components/TransitionContainer.tsx index 112b4c8dff..e7db17b880 100644 --- a/gui/packages/desktop/src/renderer/components/TransitionContainer.tsx +++ b/gui/packages/desktop/src/renderer/components/TransitionContainer.tsx @@ -1,17 +1,17 @@ import * as React from 'react'; -import { Styles, Component, Animated, View, Types, UserInterface } from 'reactxp'; -import { TransitionGroupProps } from '../transitions'; +import { Animated, Component, Styles, Types, UserInterface, View } from 'reactxp'; +import { ITransitionGroupProps } from '../transitions'; -type Props = { +interface IProps extends ITransitionGroupProps { children: React.ReactNode; -} & TransitionGroupProps; +} -type State = { +interface IState { previousChildren?: React.ReactNode; childrenAnimation?: Types.AnimatedViewStyleRuleSet; previousChildrenAnimation?: Types.AnimatedViewStyleRuleSet; dimensions: Types.Dimensions; -}; +} const dimensions = UserInterface.measureWindow(); const styles = { @@ -31,16 +31,12 @@ const styles = { }), }; -export default class TransitionContainer extends Component<Props, State> { - constructor(props: Props) { - super(props); - - this.state = { - dimensions: UserInterface.measureWindow(), - }; - } +export default class TransitionContainer extends Component<IProps, IState> { + public state: IState = { + dimensions: UserInterface.measureWindow(), + }; - UNSAFE_componentWillReceiveProps(nextProps: Props) { + public UNSAFE_componentWillReceiveProps(nextProps: IProps) { switch (nextProps.name) { case 'slide-up': this.slideUpTransition(nextProps); @@ -59,14 +55,14 @@ export default class TransitionContainer extends Component<Props, State> { } } - onFinishedAnimation() { + public onFinishedAnimation() { this.setState({ childrenAnimation: styles.allowPointerEventsStyle, previousChildren: null, }); } - slideUpTransition(nextProps: Props) { + public slideUpTransition(nextProps: IProps) { const currentTranslationValue = Animated.createValue(this.state.dimensions.height); this.setState( { @@ -94,7 +90,7 @@ export default class TransitionContainer extends Component<Props, State> { ); } - slideDownTransition(nextProps: Props) { + public slideDownTransition(nextProps: IProps) { const previousTranslationValue = Animated.createValue(0); this.setState( { @@ -122,7 +118,7 @@ export default class TransitionContainer extends Component<Props, State> { ); } - pushTransition(nextProps: Props) { + public pushTransition(nextProps: IProps) { const currentTranslationValue = Animated.createValue(this.state.dimensions.width); const previousTranslationValue = Animated.createValue(0); this.setState( @@ -159,7 +155,7 @@ export default class TransitionContainer extends Component<Props, State> { ); } - popTransition(nextProps: Props) { + public popTransition(nextProps: IProps) { const currentTranslationValue = Animated.createValue(-this.state.dimensions.width / 2); const previousTranslationValue = Animated.createValue(0); this.setState( @@ -196,7 +192,7 @@ export default class TransitionContainer extends Component<Props, State> { ); } - render() { + public render() { const { children } = this.props; const { previousChildren, childrenAnimation, previousChildrenAnimation } = this.state; diff --git a/gui/packages/desktop/src/renderer/components/TunnelControl.tsx b/gui/packages/desktop/src/renderer/components/TunnelControl.tsx index 8418f64086..aa21556fae 100644 --- a/gui/packages/desktop/src/renderer/components/TunnelControl.tsx +++ b/gui/packages/desktop/src/renderer/components/TunnelControl.tsx @@ -1,36 +1,36 @@ +import { ConnectionInfo, SecuredDisplayStyle, SecuredLabel } from '@mullvad/components'; import * as React from 'react'; -import { Component, Text, View, Styles, Types } from 'reactxp'; -import { ConnectionInfo, SecuredLabel, SecuredDisplayStyle } from '@mullvad/components'; -import * as AppButton from './AppButton'; +import { Component, Styles, Text, Types, View } from 'reactxp'; import { colors } from '../../config.json'; +import * as AppButton from './AppButton'; -import { TunnelStateTransition, RelayProtocol } from '../../shared/daemon-rpc-types'; +import { RelayProtocol, TunnelStateTransition } from '../../shared/daemon-rpc-types'; -export type RelayInAddress = { +export interface IRelayInAddress { ip: string; port: number; protocol: RelayProtocol; -}; +} -export type RelayOutAddress = { +export interface IRelayOutAddress { ipv4?: string; ipv6?: string; -}; +} -type TunnelControlProps = { +interface ITunnelControlProps { tunnelState: TunnelStateTransition; selectedRelayName: string; city?: string; country?: string; hostname?: string; defaultConnectionInfoOpen?: boolean; - relayInAddress?: RelayInAddress; - relayOutAddress?: RelayOutAddress; + relayInAddress?: IRelayInAddress; + relayOutAddress?: IRelayOutAddress; onConnect: () => void; onDisconnect: () => void; onSelectLocation: () => void; onToggleConnectionInfo: (value: boolean) => void; -}; +} const styles = { body: Styles.createViewStyle({ @@ -75,8 +75,8 @@ const styles = { }), }; -export default class TunnelControl extends Component<TunnelControlProps> { - render() { +export default class TunnelControl extends Component<ITunnelControlProps> { + public render() { const Location = ({ children }: { children?: React.ReactNode }) => ( <View style={styles.status_location}>{children}</View> ); @@ -253,18 +253,18 @@ export default class TunnelControl extends Component<TunnelControlProps> { } } -type ContainerProps = { +interface IContainerProps { children?: Types.ReactNode; -}; +} -class Wrapper extends Component<ContainerProps> { - render() { +class Wrapper extends Component<IContainerProps> { + public render() { return <View style={styles.wrapper}>{this.props.children}</View>; } } -class Body extends Component<ContainerProps> { - render() { +class Body extends Component<IContainerProps> { + public render() { return <View style={styles.body}>{this.props.children}</View>; } } diff --git a/gui/packages/desktop/src/renderer/containers/AccountPage.tsx b/gui/packages/desktop/src/renderer/containers/AccountPage.tsx index c9725147b5..33dad1e1cb 100644 --- a/gui/packages/desktop/src/renderer/containers/AccountPage.tsx +++ b/gui/packages/desktop/src/renderer/containers/AccountPage.tsx @@ -1,20 +1,20 @@ +import { goBack } from 'connected-react-router'; import { remote, shell } from 'electron'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; -import { goBack } from 'connected-react-router'; -import Account from '../components/Account'; import { links } from '../../config.json'; +import Account from '../components/Account'; -import { ReduxState, ReduxDispatch } from '../redux/store'; -import { SharedRouteProps } from '../routes'; +import { IReduxState, ReduxDispatch } from '../redux/store'; +import { ISharedRouteProps } from '../routes'; -const mapStateToProps = (state: ReduxState) => ({ +const mapStateToProps = (state: IReduxState) => ({ accountToken: state.account.accountToken, accountExpiry: state.account.expiry, expiryLocale: remote.app.getLocale(), isOffline: state.connection.isBlocked, }); -const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) => { +const mapDispatchToProps = (dispatch: ReduxDispatch, props: ISharedRouteProps) => { const history = bindActionCreators({ goBack }, dispatch); return { onLogout: () => { @@ -23,7 +23,7 @@ const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) => onClose: () => { history.goBack(); }, - onBuyMore: () => shell.openExternal(links['purchase']), + onBuyMore: () => shell.openExternal(links.purchase), }; }; diff --git a/gui/packages/desktop/src/renderer/containers/AdvancedSettingsPage.tsx b/gui/packages/desktop/src/renderer/containers/AdvancedSettingsPage.tsx index cc32d6556d..d70c0a99ee 100644 --- a/gui/packages/desktop/src/renderer/containers/AdvancedSettingsPage.tsx +++ b/gui/packages/desktop/src/renderer/containers/AdvancedSettingsPage.tsx @@ -1,16 +1,16 @@ +import { goBack } from 'connected-react-router'; import log from 'electron-log'; import { connect } from 'react-redux'; -import { goBack } from 'connected-react-router'; import { bindActionCreators } from 'redux'; +import { RelayProtocol } from '../../shared/daemon-rpc-types'; import { AdvancedSettings } from '../components/AdvancedSettings'; import RelaySettingsBuilder from '../lib/relay-settings-builder'; -import { RelayProtocol } from '../../shared/daemon-rpc-types'; -import { ReduxState, ReduxDispatch } from '../redux/store'; import { RelaySettingsRedux } from '../redux/settings/reducers'; -import { SharedRouteProps } from '../routes'; +import { IReduxState, ReduxDispatch } from '../redux/store'; +import { ISharedRouteProps } from '../routes'; -const mapStateToProps = (state: ReduxState) => { +const mapStateToProps = (state: IReduxState) => { const protocolAndPort = mapRelaySettingsToProtocolAndPort(state.settings.relaySettings); return { @@ -36,7 +36,7 @@ const mapRelaySettingsToProtocolAndPort = (relaySettings: RelaySettingsRedux) => } }; -const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) => { +const mapDispatchToProps = (dispatch: ReduxDispatch, props: ISharedRouteProps) => { const history = bindActionCreators({ goBack }, dispatch); return { onClose: () => { diff --git a/gui/packages/desktop/src/renderer/containers/ConnectPage.tsx b/gui/packages/desktop/src/renderer/containers/ConnectPage.tsx index f20927ab29..75071e3a1f 100644 --- a/gui/packages/desktop/src/renderer/containers/ConnectPage.tsx +++ b/gui/packages/desktop/src/renderer/containers/ConnectPage.tsx @@ -1,20 +1,20 @@ +import { push } from 'connected-react-router'; import { shell } from 'electron'; import log from 'electron-log'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; -import { push } from 'connected-react-router'; import Connect from '../components/Connect'; import AccountExpiry from '../lib/account-expiry'; import userInterfaceActions from '../redux/userinterface/actions'; -import { ReduxState, ReduxDispatch } from '../redux/store'; -import { SharedRouteProps } from '../routes'; +import { IReduxState, ReduxDispatch } from '../redux/store'; +import { ISharedRouteProps } from '../routes'; -import { RelaySettingsRedux, RelayLocationRedux } from '../redux/settings/reducers'; +import { IRelayLocationRedux, RelaySettingsRedux } from '../redux/settings/reducers'; function getRelayName( relaySettings: RelaySettingsRedux, - relayLocations: Array<RelayLocationRedux>, + relayLocations: IRelayLocationRedux[], ): string { if ('normal' in relaySettings) { const location = relaySettings.normal.location; @@ -54,7 +54,7 @@ function getRelayName( } } -const mapStateToProps = (state: ReduxState) => { +const mapStateToProps = (state: IReduxState) => { return { accountExpiry: state.account.expiry ? new AccountExpiry(state.account.expiry) : undefined, selectedRelayName: getRelayName(state.settings.relaySettings, state.settings.relayLocations), @@ -65,7 +65,7 @@ const mapStateToProps = (state: ReduxState) => { }; }; -const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) => { +const mapDispatchToProps = (dispatch: ReduxDispatch, props: ISharedRouteProps) => { const userInterface = bindActionCreators(userInterfaceActions, dispatch); const history = bindActionCreators({ push }, dispatch); diff --git a/gui/packages/desktop/src/renderer/containers/LaunchPage.tsx b/gui/packages/desktop/src/renderer/containers/LaunchPage.tsx index 00d42acdb6..8619b33d87 100644 --- a/gui/packages/desktop/src/renderer/containers/LaunchPage.tsx +++ b/gui/packages/desktop/src/renderer/containers/LaunchPage.tsx @@ -1,13 +1,13 @@ -import { bindActionCreators } from 'redux'; -import { connect } from 'react-redux'; import { push } from 'connected-react-router'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; import Launch from '../components/Launch'; -import { ReduxState, ReduxDispatch } from '../redux/store'; -import { SharedRouteProps } from '../routes'; +import { IReduxState, ReduxDispatch } from '../redux/store'; +import { ISharedRouteProps } from '../routes'; -const mapStateToProps = (_state: ReduxState) => ({}); -const mapDispatchToProps = (dispatch: ReduxDispatch, _props: SharedRouteProps) => { +const mapStateToProps = (_state: IReduxState) => ({}); +const mapDispatchToProps = (dispatch: ReduxDispatch, _props: ISharedRouteProps) => { const history = bindActionCreators({ push }, dispatch); return { openSettings: () => { diff --git a/gui/packages/desktop/src/renderer/containers/LoginPage.tsx b/gui/packages/desktop/src/renderer/containers/LoginPage.tsx index b73883c7fc..e1f70fbedb 100644 --- a/gui/packages/desktop/src/renderer/containers/LoginPage.tsx +++ b/gui/packages/desktop/src/renderer/containers/LoginPage.tsx @@ -1,14 +1,14 @@ +import { push } from 'connected-react-router'; import { shell } from 'electron'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; -import { push } from 'connected-react-router'; import Login from '../components/Login'; import accountActions from '../redux/account/actions'; -import { ReduxState, ReduxDispatch } from '../redux/store'; -import { SharedRouteProps } from '../routes'; +import { IReduxState, ReduxDispatch } from '../redux/store'; +import { ISharedRouteProps } from '../routes'; -const mapStateToProps = (state: ReduxState) => { +const mapStateToProps = (state: IReduxState) => { const { accountToken, accountHistory, error, status } = state.account; return { accountToken, @@ -17,7 +17,7 @@ const mapStateToProps = (state: ReduxState) => { loginState: status, }; }; -const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) => { +const mapDispatchToProps = (dispatch: ReduxDispatch, props: ISharedRouteProps) => { const history = bindActionCreators({ push }, dispatch); const { resetLoginError, updateAccountToken } = bindActionCreators(accountActions, dispatch); return { @@ -31,7 +31,7 @@ const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) => resetLoginError(); }, openExternalLink: (url: string) => shell.openExternal(url), - updateAccountToken: updateAccountToken, + updateAccountToken, removeAccountTokenFromHistory: (token: string) => props.app.removeAccountFromHistory(token), }; }; diff --git a/gui/packages/desktop/src/renderer/containers/PlatformWindowContainer.tsx b/gui/packages/desktop/src/renderer/containers/PlatformWindowContainer.tsx index 844dd74253..f39cd82600 100644 --- a/gui/packages/desktop/src/renderer/containers/PlatformWindowContainer.tsx +++ b/gui/packages/desktop/src/renderer/containers/PlatformWindowContainer.tsx @@ -1,9 +1,9 @@ import { connect } from 'react-redux'; import PlatformWindow from '../components/PlatformWindow'; -import { ReduxState } from '../redux/store'; +import { IReduxState } from '../redux/store'; -const mapStateToProps = (state: ReduxState) => ({ +const mapStateToProps = (state: IReduxState) => ({ arrowPosition: state.userInterface.arrowPosition, }); diff --git a/gui/packages/desktop/src/renderer/containers/PreferencesPage.tsx b/gui/packages/desktop/src/renderer/containers/PreferencesPage.tsx index 9701a2ff0f..4147b42cfc 100644 --- a/gui/packages/desktop/src/renderer/containers/PreferencesPage.tsx +++ b/gui/packages/desktop/src/renderer/containers/PreferencesPage.tsx @@ -1,13 +1,13 @@ +import { goBack } from 'connected-react-router'; import log from 'electron-log'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; -import { goBack } from 'connected-react-router'; import Preferences from '../components/Preferences'; -import { ReduxState, ReduxDispatch } from '../redux/store'; -import { SharedRouteProps } from '../routes'; +import { IReduxState, ReduxDispatch } from '../redux/store'; +import { ISharedRouteProps } from '../routes'; -const mapStateToProps = (state: ReduxState) => ({ +const mapStateToProps = (state: IReduxState) => ({ autoStart: state.settings.autoStart, autoConnect: state.settings.guiSettings.autoConnect, allowLan: state.settings.allowLan, @@ -15,7 +15,7 @@ const mapStateToProps = (state: ReduxState) => ({ startMinimized: state.settings.guiSettings.startMinimized, }); -const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) => { +const mapDispatchToProps = (dispatch: ReduxDispatch, props: ISharedRouteProps) => { const history = bindActionCreators({ goBack }, dispatch); return { onClose: () => { diff --git a/gui/packages/desktop/src/renderer/containers/SelectLocationPage.tsx b/gui/packages/desktop/src/renderer/containers/SelectLocationPage.tsx index c15feea88a..a517c34b3f 100644 --- a/gui/packages/desktop/src/renderer/containers/SelectLocationPage.tsx +++ b/gui/packages/desktop/src/renderer/containers/SelectLocationPage.tsx @@ -1,19 +1,19 @@ +import { goBack } from 'connected-react-router'; import log from 'electron-log'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; -import { goBack } from 'connected-react-router'; +import { RelayLocation } from '../../shared/daemon-rpc-types'; import SelectLocation from '../components/SelectLocation'; import RelaySettingsBuilder from '../lib/relay-settings-builder'; -import { RelayLocation } from '../../shared/daemon-rpc-types'; -import { ReduxState, ReduxDispatch } from '../redux/store'; -import { SharedRouteProps } from '../routes'; +import { IReduxState, ReduxDispatch } from '../redux/store'; +import { ISharedRouteProps } from '../routes'; -const mapStateToProps = (state: ReduxState) => ({ +const mapStateToProps = (state: IReduxState) => ({ relaySettings: state.settings.relaySettings, relayLocations: state.settings.relayLocations, }); -const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) => { +const mapDispatchToProps = (dispatch: ReduxDispatch, props: ISharedRouteProps) => { const history = bindActionCreators({ goBack }, dispatch); return { onClose: () => history.goBack(), diff --git a/gui/packages/desktop/src/renderer/containers/SettingsPage.tsx b/gui/packages/desktop/src/renderer/containers/SettingsPage.tsx index abe6fa0e93..da4a10cdb2 100644 --- a/gui/packages/desktop/src/renderer/containers/SettingsPage.tsx +++ b/gui/packages/desktop/src/renderer/containers/SettingsPage.tsx @@ -1,13 +1,13 @@ +import { goBack, push } from 'connected-react-router'; import { remote, shell } from 'electron'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; -import { push, goBack } from 'connected-react-router'; import Settings from '../components/Settings'; -import { ReduxState, ReduxDispatch } from '../redux/store'; -import { SharedRouteProps } from '../routes'; +import { IReduxState, ReduxDispatch } from '../redux/store'; +import { ISharedRouteProps } from '../routes'; -const mapStateToProps = (state: ReduxState) => ({ +const mapStateToProps = (state: IReduxState) => ({ loginState: state.account.status, accountExpiry: state.account.expiry, appVersion: state.version.current, @@ -15,7 +15,7 @@ const mapStateToProps = (state: ReduxState) => ({ upToDateVersion: state.version.upToDate, isOffline: state.connection.isBlocked, }); -const mapDispatchToProps = (dispatch: ReduxDispatch, _props: SharedRouteProps) => { +const mapDispatchToProps = (dispatch: ReduxDispatch, _props: ISharedRouteProps) => { const history = bindActionCreators({ push, goBack }, dispatch); return { onQuit: () => remote.app.quit(), diff --git a/gui/packages/desktop/src/renderer/containers/SupportPage.tsx b/gui/packages/desktop/src/renderer/containers/SupportPage.tsx index 2746734aee..00be7fee44 100644 --- a/gui/packages/desktop/src/renderer/containers/SupportPage.tsx +++ b/gui/packages/desktop/src/renderer/containers/SupportPage.tsx @@ -1,22 +1,22 @@ +import { goBack } from 'connected-react-router'; import { shell } from 'electron'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; -import { goBack } from 'connected-react-router'; import Support from '../components/Support'; import { collectProblemReport, sendProblemReport } from '../lib/problem-report'; -import { ReduxState, ReduxDispatch } from '../redux/store'; -import { SharedRouteProps } from '../routes'; +import { IReduxState, ReduxDispatch } from '../redux/store'; import supportActions from '../redux/support/actions'; +import { ISharedRouteProps } from '../routes'; -const mapStateToProps = (state: ReduxState) => ({ +const mapStateToProps = (state: IReduxState) => ({ defaultEmail: state.support.email, defaultMessage: state.support.message, accountHistory: state.account.accountHistory, isOffline: state.connection.isBlocked, }); -const mapDispatchToProps = (dispatch: ReduxDispatch, _props: SharedRouteProps) => { +const mapDispatchToProps = (dispatch: ReduxDispatch, _props: ISharedRouteProps) => { const { saveReportForm, clearReportForm } = bindActionCreators(supportActions, dispatch); const history = bindActionCreators({ goBack }, dispatch); diff --git a/gui/packages/desktop/src/renderer/lib/account-expiry.ts b/gui/packages/desktop/src/renderer/lib/account-expiry.ts index 4f4f51ce05..f7f85afbda 100644 --- a/gui/packages/desktop/src/renderer/lib/account-expiry.ts +++ b/gui/packages/desktop/src/renderer/lib/account-expiry.ts @@ -1,21 +1,21 @@ import moment from 'moment'; export default class AccountExpiry { - _expiry: moment.Moment; + private expiry: moment.Moment; constructor(expiry: string) { - this._expiry = moment(expiry); + this.expiry = moment(expiry); } - hasExpired(): boolean { + public hasExpired(): boolean { return this.willHaveExpiredIn(moment()); } - willHaveExpiredIn(time: moment.Moment): boolean { - return this._expiry.isSameOrBefore(time); + public willHaveExpiredIn(time: moment.Moment): boolean { + return this.expiry.isSameOrBefore(time); } - remainingTime(): string { - return this._expiry.fromNow(true) + ' left'; + public remainingTime(): string { + return this.expiry.fromNow(true) + ' left'; } } diff --git a/gui/packages/desktop/src/renderer/lib/auth-failure.ts b/gui/packages/desktop/src/renderer/lib/auth-failure.ts index efee82b08c..3f6f8b4609 100644 --- a/gui/packages/desktop/src/renderer/lib/auth-failure.ts +++ b/gui/packages/desktop/src/renderer/lib/auth-failure.ts @@ -16,14 +16,14 @@ const TOO_MANY_CONNECTIONS_MSG = 'This account has too many simultaneous connections. Disconnect another device or try connecting again shortly.'; export class AuthFailure { - _reasonId: AuthFailureKind; - _message: string; + private reasonId: AuthFailureKind; + private message: string; constructor(reason?: string) { if (!reason) { log.error('Received invalid auth_failed reason: ', reason); - this._reasonId = 'UNKNOWN'; - this._message = GENERIC_FAILURE_MSG; + this.reasonId = 'UNKNOWN'; + this.message = GENERIC_FAILURE_MSG; return; } @@ -31,18 +31,18 @@ export class AuthFailure { if (!results || results.length < 3) { log.error(`Received invalid auth_failed message - "${reason}"`); - this._reasonId = 'UNKNOWN'; - this._message = reason; + this.reasonId = 'UNKNOWN'; + this.message = reason; return; } - const id_string = results[1]; - this._reasonId = strToFailureKind(id_string); - this._message = results[2] || GENERIC_FAILURE_MSG; + const idString = results[1]; + this.reasonId = strToFailureKind(idString); + this.message = results[2] || GENERIC_FAILURE_MSG; } - show(): string { - switch (this._reasonId) { + public show(): string { + switch (this.reasonId) { case 'INVALID_ACCOUNT': return INVALID_ACCOUNT_MSG; case 'EXPIRED_ACCOUNT': @@ -50,7 +50,7 @@ export class AuthFailure { case 'TOO_MANY_CONNECTIONS': return TOO_MANY_CONNECTIONS_MSG; case 'UNKNOWN': - return this._message; + return this.message; } } } diff --git a/gui/packages/desktop/src/renderer/lib/problem-report.ts b/gui/packages/desktop/src/renderer/lib/problem-report.ts index 0afcf30091..7213ed8d07 100644 --- a/gui/packages/desktop/src/renderer/lib/problem-report.ts +++ b/gui/packages/desktop/src/renderer/lib/problem-report.ts @@ -1,11 +1,14 @@ import { ipcRenderer } from 'electron'; import * as uuid from 'uuid'; -type ErrorResult = { success: false; error: string }; -type CollectResult = { success: true; reportPath: string } | ErrorResult; -type SendResult = { success: true } | ErrorResult; +interface IErrorResult { + success: false; + error: string; +} +type CollectResult = { success: true; reportPath: string } | IErrorResult; +type SendResult = { success: true } | IErrorResult; -const collectProblemReport = (toRedact: Array<string>): Promise<string> => { +const collectProblemReport = (toRedact: string[]): Promise<string> => { return new Promise((resolve, reject) => { const requestId = uuid.v4(); const responseListener = ( diff --git a/gui/packages/desktop/src/renderer/lib/relay-settings-builder.ts b/gui/packages/desktop/src/renderer/lib/relay-settings-builder.ts index 949b02d35f..90cec5d3c6 100644 --- a/gui/packages/desktop/src/renderer/lib/relay-settings-builder.ts +++ b/gui/packages/desktop/src/renderer/lib/relay-settings-builder.ts @@ -1,64 +1,64 @@ import { + IOpenVpnConstraints, RelayLocation, RelayProtocol, - RelaySettingsUpdate, RelaySettingsNormalUpdate, - OpenVpnConstraints, + RelaySettingsUpdate, } from '../../shared/daemon-rpc-types'; -type LocationBuilder<Self> = { +interface ILocationBuilder<Self> { country: (country: string) => Self; city: (country: string, city: string) => Self; hostname: (country: string, city: string, hostname: string) => Self; any: () => Self; fromRaw: (location: 'any' | RelayLocation) => Self; -}; +} -interface ExactOrAny<T, Self> { +interface IExactOrAny<T, Self> { exact(value: T): Self; any(): Self; } -interface OpenVPNConfigurator { - port: ExactOrAny<number, OpenVPNConfigurator>; - protocol: ExactOrAny<RelayProtocol, OpenVPNConfigurator>; +interface IOpenVPNConfigurator { + port: IExactOrAny<number, IOpenVPNConfigurator>; + protocol: IExactOrAny<RelayProtocol, IOpenVPNConfigurator>; } -interface TunnelBuilder { +interface ITunnelBuilder { openvpn( - configurator: (openVpnConfigurator: OpenVPNConfigurator) => void, + configurator: (openVpnConfigurator: IOpenVPNConfigurator) => void, ): NormalRelaySettingsBuilder; any(): NormalRelaySettingsBuilder; } class NormalRelaySettingsBuilder { - _payload: RelaySettingsNormalUpdate = {}; + private payload: RelaySettingsNormalUpdate = {}; - build(): RelaySettingsUpdate { + public build(): RelaySettingsUpdate { return { - normal: this._payload, + normal: this.payload, }; } - get location(): LocationBuilder<NormalRelaySettingsBuilder> { + get location(): ILocationBuilder<NormalRelaySettingsBuilder> { return { country: (country: string) => { - this._payload.location = { only: { country } }; + this.payload.location = { only: { country } }; return this; }, city: (country: string, city: string) => { - this._payload.location = { only: { city: [country, city] } }; + this.payload.location = { only: { city: [country, city] } }; return this; }, hostname: (country: string, city: string, hostname: string) => { - this._payload.location = { only: { hostname: [country, city, hostname] } }; + this.payload.location = { only: { hostname: [country, city, hostname] } }; return this; }, any: () => { - this._payload.location = 'any'; + this.payload.location = 'any'; return this; }, - fromRaw: function(location: 'any' | RelayLocation) { + fromRaw(location: 'any' | RelayLocation) { if (location === 'any') { return this.any(); } else if ('hostname' in location) { @@ -78,18 +78,18 @@ class NormalRelaySettingsBuilder { }; } - get tunnel(): TunnelBuilder { - const updateOpenvpn = (next: Partial<OpenVpnConstraints>) => { - const tunnel = this._payload.tunnel; + get tunnel(): ITunnelBuilder { + const updateOpenvpn = (next: Partial<IOpenVpnConstraints>) => { + const tunnel = this.payload.tunnel; if (typeof tunnel === 'string' || typeof tunnel === 'undefined') { - this._payload.tunnel = { + this.payload.tunnel = { only: { openvpn: next, }, }; } else if (typeof tunnel === 'object') { const prev = (tunnel.only && tunnel.only.openvpn) || {}; - this._payload.tunnel = { + this.payload.tunnel = { only: { openvpn: { ...prev, ...next }, }, @@ -98,8 +98,8 @@ class NormalRelaySettingsBuilder { }; return { - openvpn: (configurator: (configurator: OpenVPNConfigurator) => void) => { - const openvpnBuilder: OpenVPNConfigurator = { + openvpn: (configurator: (configurator: IOpenVPNConfigurator) => void) => { + const openvpnBuilder: IOpenVPNConfigurator = { get port() { const apply = (port: 'any' | { only: number }) => { updateOpenvpn({ port }); @@ -127,7 +127,7 @@ class NormalRelaySettingsBuilder { return this; }, any: () => { - this._payload.tunnel = 'any'; + this.payload.tunnel = 'any'; return this; }, }; diff --git a/gui/packages/desktop/src/renderer/lib/transition-rule.ts b/gui/packages/desktop/src/renderer/lib/transition-rule.ts index 5e3a9138c4..ae0e2ad5b7 100644 --- a/gui/packages/desktop/src/renderer/lib/transition-rule.ts +++ b/gui/packages/desktop/src/renderer/lib/transition-rule.ts @@ -1,41 +1,33 @@ -export type TransitionDescriptor = { +export interface ITransitionDescriptor { name: string; duration: number; -}; +} -export type TransitionFork = { - forward: TransitionDescriptor; - backward: TransitionDescriptor; -}; +export interface ITransitionFork { + forward: ITransitionDescriptor; + backward: ITransitionDescriptor; +} -export type TransitionMatch = { +export interface ITransitionMatch { direction: 'forward' | 'backward'; - descriptor: TransitionDescriptor; -}; + descriptor: ITransitionDescriptor; +} export default class TransitionRule { - _from: string | null; - _to: string; - _fork: TransitionFork; - - constructor(from: string | null, to: string, fork: TransitionFork) { - this._from = from; - this._to = to; - this._fork = fork; - } + constructor(private from: string | null, private to: string, private fork: ITransitionFork) {} - match(fromRoute: string | null, toRoute: string): TransitionMatch | null { - if ((!this._from || this._from === fromRoute) && this._to === toRoute) { + public match(fromRoute: string | null, toRoute: string): ITransitionMatch | null { + if ((!this.from || this.from === fromRoute) && this.to === toRoute) { return { direction: 'forward', - descriptor: this._fork['forward'], + descriptor: this.fork.forward, }; } - if ((!this._from || this._from === toRoute) && this._to === fromRoute) { + if ((!this.from || this.from === toRoute) && this.to === fromRoute) { return { direction: 'backward', - descriptor: this._fork['backward'], + descriptor: this.fork.backward, }; } diff --git a/gui/packages/desktop/src/renderer/redux/account/actions.ts b/gui/packages/desktop/src/renderer/redux/account/actions.ts index 0084b5a9d7..8dba590737 100644 --- a/gui/packages/desktop/src/renderer/redux/account/actions.ts +++ b/gui/packages/desktop/src/renderer/redux/account/actions.ts @@ -1,99 +1,99 @@ import { AccountToken } from '../../../shared/daemon-rpc-types'; -type StartLoginAction = { +interface IStartLoginAction { type: 'START_LOGIN'; accountToken: AccountToken; -}; +} -type LoggedInAction = { +interface ILoggedInAction { type: 'LOGGED_IN'; -}; +} -type LoginFailedAction = { +interface ILoginFailedAction { type: 'LOGIN_FAILED'; error: Error; -}; +} -type LoggedOutAction = { +interface ILoggedOutAction { type: 'LOGGED_OUT'; -}; +} -type ResetLoginErrorAction = { +interface IResetLoginErrorAction { type: 'RESET_LOGIN_ERROR'; -}; +} -type UpdateAccountTokenAction = { +interface IUpdateAccountTokenAction { type: 'UPDATE_ACCOUNT_TOKEN'; token: AccountToken; -}; +} -type UpdateAccountHistoryAction = { +interface IUpdateAccountHistoryAction { type: 'UPDATE_ACCOUNT_HISTORY'; - accountHistory: Array<AccountToken>; -}; + accountHistory: AccountToken[]; +} -type UpdateAccountExpiryAction = { +interface IUpdateAccountExpiryAction { type: 'UPDATE_ACCOUNT_EXPIRY'; expiry: string; -}; +} export type AccountAction = - | StartLoginAction - | LoggedInAction - | LoginFailedAction - | LoggedOutAction - | ResetLoginErrorAction - | UpdateAccountTokenAction - | UpdateAccountHistoryAction - | UpdateAccountExpiryAction; + | IStartLoginAction + | ILoggedInAction + | ILoginFailedAction + | ILoggedOutAction + | IResetLoginErrorAction + | IUpdateAccountTokenAction + | IUpdateAccountHistoryAction + | IUpdateAccountExpiryAction; -function startLogin(accountToken: AccountToken): StartLoginAction { +function startLogin(accountToken: AccountToken): IStartLoginAction { return { type: 'START_LOGIN', - accountToken: accountToken, + accountToken, }; } -function loggedIn(): LoggedInAction { +function loggedIn(): ILoggedInAction { return { type: 'LOGGED_IN', }; } -function loginFailed(error: Error): LoginFailedAction { +function loginFailed(error: Error): ILoginFailedAction { return { type: 'LOGIN_FAILED', error, }; } -function loggedOut(): LoggedOutAction { +function loggedOut(): ILoggedOutAction { return { type: 'LOGGED_OUT', }; } -function resetLoginError(): ResetLoginErrorAction { +function resetLoginError(): IResetLoginErrorAction { return { type: 'RESET_LOGIN_ERROR', }; } -function updateAccountToken(token: AccountToken): UpdateAccountTokenAction { +function updateAccountToken(token: AccountToken): IUpdateAccountTokenAction { return { type: 'UPDATE_ACCOUNT_TOKEN', token, }; } -function updateAccountHistory(accountHistory: Array<AccountToken>): UpdateAccountHistoryAction { +function updateAccountHistory(accountHistory: AccountToken[]): IUpdateAccountHistoryAction { return { type: 'UPDATE_ACCOUNT_HISTORY', accountHistory, }; } -function updateAccountExpiry(expiry: string): UpdateAccountExpiryAction { +function updateAccountExpiry(expiry: string): IUpdateAccountExpiryAction { return { type: 'UPDATE_ACCOUNT_EXPIRY', expiry, diff --git a/gui/packages/desktop/src/renderer/redux/account/reducers.ts b/gui/packages/desktop/src/renderer/redux/account/reducers.ts index 75f4fcd997..754ea09d13 100644 --- a/gui/packages/desktop/src/renderer/redux/account/reducers.ts +++ b/gui/packages/desktop/src/renderer/redux/account/reducers.ts @@ -1,16 +1,16 @@ -import { ReduxAction } from '../store'; import { AccountToken } from '../../../shared/daemon-rpc-types'; +import { ReduxAction } from '../store'; export type LoginState = 'none' | 'logging in' | 'failed' | 'ok'; -export type AccountReduxState = { +export interface IAccountReduxState { accountToken?: AccountToken; - accountHistory: Array<AccountToken>; + accountHistory: AccountToken[]; expiry?: string; // ISO8601 status: LoginState; error?: Error; -}; +} -const initialState: AccountReduxState = { +const initialState: IAccountReduxState = { accountToken: undefined, accountHistory: [], expiry: undefined, @@ -19,9 +19,9 @@ const initialState: AccountReduxState = { }; export default function( - state: AccountReduxState = initialState, + state: IAccountReduxState = initialState, action: ReduxAction, -): AccountReduxState { +): IAccountReduxState { switch (action.type) { case 'START_LOGIN': return { diff --git a/gui/packages/desktop/src/renderer/redux/connection/actions.ts b/gui/packages/desktop/src/renderer/redux/connection/actions.ts index e700c8918c..f24d080a57 100644 --- a/gui/packages/desktop/src/renderer/redux/connection/actions.ts +++ b/gui/packages/desktop/src/renderer/redux/connection/actions.ts @@ -1,30 +1,30 @@ -import { AfterDisconnect, BlockReason, TunnelEndpoint } from '../../../shared/daemon-rpc-types'; +import { AfterDisconnect, BlockReason, ITunnelEndpoint } from '../../../shared/daemon-rpc-types'; -type ConnectingAction = { +interface IConnectingAction { type: 'CONNECTING'; - tunnelEndpoint?: TunnelEndpoint; -}; + tunnelEndpoint?: ITunnelEndpoint; +} -type ConnectedAction = { +interface IConnectedAction { type: 'CONNECTED'; - tunnelEndpoint: TunnelEndpoint; -}; + tunnelEndpoint: ITunnelEndpoint; +} -type DisconnectedAction = { +interface IDisconnectedAction { type: 'DISCONNECTED'; -}; +} -type DisconnectingAction = { +interface IDisconnectingAction { type: 'DISCONNECTING'; afterDisconnect: AfterDisconnect; -}; +} -type BlockedAction = { +interface IBlockedAction { type: 'BLOCKED'; reason: BlockReason; -}; +} -type NewLocationAction = { +interface INewLocationAction { type: 'NEW_LOCATION'; newLocation: { country: string; @@ -34,74 +34,74 @@ type NewLocationAction = { mullvadExitIp: boolean; hostname?: string; }; -}; +} -type OnlineAction = { +interface IOnlineAction { type: 'ONLINE'; -}; +} -type OfflineAction = { +interface IOfflineAction { type: 'OFFLINE'; -}; +} export type ConnectionAction = - | NewLocationAction - | ConnectingAction - | ConnectedAction - | DisconnectedAction - | DisconnectingAction - | BlockedAction - | OnlineAction - | OfflineAction; + | INewLocationAction + | IConnectingAction + | IConnectedAction + | IDisconnectedAction + | IDisconnectingAction + | IBlockedAction + | IOnlineAction + | IOfflineAction; -function connecting(tunnelEndpoint?: TunnelEndpoint): ConnectingAction { +function connecting(tunnelEndpoint?: ITunnelEndpoint): IConnectingAction { return { type: 'CONNECTING', tunnelEndpoint, }; } -function connected(tunnelEndpoint: TunnelEndpoint): ConnectedAction { +function connected(tunnelEndpoint: ITunnelEndpoint): IConnectedAction { return { type: 'CONNECTED', tunnelEndpoint, }; } -function disconnected(): DisconnectedAction { +function disconnected(): IDisconnectedAction { return { type: 'DISCONNECTED', }; } -function disconnecting(afterDisconnect: AfterDisconnect): DisconnectingAction { +function disconnecting(afterDisconnect: AfterDisconnect): IDisconnectingAction { return { type: 'DISCONNECTING', afterDisconnect, }; } -function blocked(reason: BlockReason): BlockedAction { +function blocked(reason: BlockReason): IBlockedAction { return { type: 'BLOCKED', reason, }; } -function newLocation(newLocation: NewLocationAction['newLocation']): NewLocationAction { +function newLocation(location: INewLocationAction['newLocation']): INewLocationAction { return { type: 'NEW_LOCATION', - newLocation, + newLocation: location, }; } -function online(): OnlineAction { +function online(): IOnlineAction { return { type: 'ONLINE', }; } -function offline(): OfflineAction { +function offline(): IOfflineAction { return { type: 'OFFLINE', }; diff --git a/gui/packages/desktop/src/renderer/redux/connection/reducers.ts b/gui/packages/desktop/src/renderer/redux/connection/reducers.ts index 4bca04fa7c..bd22a6f110 100644 --- a/gui/packages/desktop/src/renderer/redux/connection/reducers.ts +++ b/gui/packages/desktop/src/renderer/redux/connection/reducers.ts @@ -1,7 +1,7 @@ +import { Ip, TunnelStateTransition } from '../../../shared/daemon-rpc-types'; import { ReduxAction } from '../store'; -import { TunnelStateTransition, Ip } from '../../../shared/daemon-rpc-types'; -export type ConnectionReduxState = { +export interface IConnectionReduxState { status: TunnelStateTransition; isOnline: boolean; isBlocked: boolean; @@ -11,9 +11,9 @@ export type ConnectionReduxState = { longitude?: number; country?: string; city?: string; -}; +} -const initialState: ConnectionReduxState = { +const initialState: IConnectionReduxState = { status: { state: 'disconnected' }, isOnline: true, isBlocked: false, @@ -26,9 +26,9 @@ const initialState: ConnectionReduxState = { }; export default function( - state: ConnectionReduxState = initialState, + state: IConnectionReduxState = initialState, action: ReduxAction, -): ConnectionReduxState { +): IConnectionReduxState { switch (action.type) { case 'NEW_LOCATION': const { hostname, latitude, longitude, city, country } = action.newLocation; diff --git a/gui/packages/desktop/src/renderer/redux/settings/actions.ts b/gui/packages/desktop/src/renderer/redux/settings/actions.ts index e08f61fce5..4587a1c513 100644 --- a/gui/packages/desktop/src/renderer/redux/settings/actions.ts +++ b/gui/packages/desktop/src/renderer/redux/settings/actions.ts @@ -1,87 +1,85 @@ -import { RelaySettingsRedux, RelayLocationRedux } from './reducers'; -import { GuiSettingsState } from '../../../shared/gui-settings-state'; +import { IGuiSettingsState } from '../../../shared/gui-settings-state'; +import { IRelayLocationRedux, RelaySettingsRedux } from './reducers'; -export type UpdateGuiSettingsAction = { +export interface IUpdateGuiSettingsAction { type: 'UPDATE_GUI_SETTINGS'; - guiSettings: GuiSettingsState; -}; + guiSettings: IGuiSettingsState; +} -export type UpdateRelayAction = { +export interface IUpdateRelayAction { type: 'UPDATE_RELAY'; relay: RelaySettingsRedux; -}; +} -export type UpdateRelayLocationsAction = { +export interface IUpdateRelayLocationsAction { type: 'UPDATE_RELAY_LOCATIONS'; - relayLocations: Array<RelayLocationRedux>; -}; + relayLocations: IRelayLocationRedux[]; +} -export type UpdateAllowLanAction = { +export interface IUpdateAllowLanAction { type: 'UPDATE_ALLOW_LAN'; allowLan: boolean; -}; +} -export type UpdateEnableIpv6Action = { +export interface IUpdateEnableIpv6Action { type: 'UPDATE_ENABLE_IPV6'; enableIpv6: boolean; -}; +} -export type UpdateBlockWhenDisconnectedAction = { +export interface IUpdateBlockWhenDisconnectedAction { type: 'UPDATE_BLOCK_WHEN_DISCONNECTED'; blockWhenDisconnected: boolean; -}; +} -export type UpdateOpenVpnMssfixAction = { +export interface IUpdateOpenVpnMssfixAction { type: 'UPDATE_OPENVPN_MSSFIX'; mssfix?: number; -}; +} -export type UpdateAutoStartAction = { +export interface IUpdateAutoStartAction { type: 'UPDATE_AUTO_START'; autoStart: boolean; -}; +} export type SettingsAction = - | UpdateGuiSettingsAction - | UpdateRelayAction - | UpdateRelayLocationsAction - | UpdateAllowLanAction - | UpdateEnableIpv6Action - | UpdateBlockWhenDisconnectedAction - | UpdateOpenVpnMssfixAction - | UpdateAutoStartAction; + | IUpdateGuiSettingsAction + | IUpdateRelayAction + | IUpdateRelayLocationsAction + | IUpdateAllowLanAction + | IUpdateEnableIpv6Action + | IUpdateBlockWhenDisconnectedAction + | IUpdateOpenVpnMssfixAction + | IUpdateAutoStartAction; -function updateGuiSettings(guiSettings: GuiSettingsState): UpdateGuiSettingsAction { +function updateGuiSettings(guiSettings: IGuiSettingsState): IUpdateGuiSettingsAction { return { type: 'UPDATE_GUI_SETTINGS', guiSettings, }; } -function updateRelay(relay: RelaySettingsRedux): UpdateRelayAction { +function updateRelay(relay: RelaySettingsRedux): IUpdateRelayAction { return { type: 'UPDATE_RELAY', - relay: relay, + relay, }; } -function updateRelayLocations( - relayLocations: Array<RelayLocationRedux>, -): UpdateRelayLocationsAction { +function updateRelayLocations(relayLocations: IRelayLocationRedux[]): IUpdateRelayLocationsAction { return { type: 'UPDATE_RELAY_LOCATIONS', - relayLocations: relayLocations, + relayLocations, }; } -function updateAllowLan(allowLan: boolean): UpdateAllowLanAction { +function updateAllowLan(allowLan: boolean): IUpdateAllowLanAction { return { type: 'UPDATE_ALLOW_LAN', allowLan, }; } -function updateEnableIpv6(enableIpv6: boolean): UpdateEnableIpv6Action { +function updateEnableIpv6(enableIpv6: boolean): IUpdateEnableIpv6Action { return { type: 'UPDATE_ENABLE_IPV6', enableIpv6, @@ -90,21 +88,21 @@ function updateEnableIpv6(enableIpv6: boolean): UpdateEnableIpv6Action { function updateBlockWhenDisconnected( blockWhenDisconnected: boolean, -): UpdateBlockWhenDisconnectedAction { +): IUpdateBlockWhenDisconnectedAction { return { type: 'UPDATE_BLOCK_WHEN_DISCONNECTED', blockWhenDisconnected, }; } -function updateOpenVpnMssfix(mssfix?: number): UpdateOpenVpnMssfixAction { +function updateOpenVpnMssfix(mssfix?: number): IUpdateOpenVpnMssfixAction { return { type: 'UPDATE_OPENVPN_MSSFIX', mssfix, }; } -function updateAutoStart(autoStart: boolean): UpdateAutoStartAction { +function updateAutoStart(autoStart: boolean): IUpdateAutoStartAction { return { type: 'UPDATE_AUTO_START', autoStart, diff --git a/gui/packages/desktop/src/renderer/redux/settings/reducers.ts b/gui/packages/desktop/src/renderer/redux/settings/reducers.ts index a4e269626a..60a610ece0 100644 --- a/gui/packages/desktop/src/renderer/redux/settings/reducers.ts +++ b/gui/packages/desktop/src/renderer/redux/settings/reducers.ts @@ -1,6 +1,6 @@ +import { RelayLocation, RelayProtocol } from '../../../shared/daemon-rpc-types'; +import { IGuiSettingsState } from '../../../shared/gui-settings-state'; import { ReduxAction } from '../store'; -import { RelayProtocol, RelayLocation } from '../../../shared/daemon-rpc-types'; -import { GuiSettingsState } from '../../../shared/gui-settings-state'; export type RelaySettingsRedux = | { @@ -18,44 +18,44 @@ export type RelaySettingsRedux = }; }; -export type RelayLocationRelayRedux = { +export interface IRelayLocationRelayRedux { hostname: string; ipv4AddrIn: string; ipv4AddrExit: string; includeInCountry: boolean; weight: number; -}; +} -export type RelayLocationCityRedux = { +export interface IRelayLocationCityRedux { name: string; code: string; latitude: number; longitude: number; hasActiveRelays: boolean; - relays: RelayLocationRelayRedux[]; -}; + relays: IRelayLocationRelayRedux[]; +} -export type RelayLocationRedux = { +export interface IRelayLocationRedux { name: string; code: string; hasActiveRelays: boolean; - cities: RelayLocationCityRedux[]; -}; + cities: IRelayLocationCityRedux[]; +} -export type SettingsReduxState = { +export interface ISettingsReduxState { autoStart: boolean; - guiSettings: GuiSettingsState; + guiSettings: IGuiSettingsState; relaySettings: RelaySettingsRedux; - relayLocations: RelayLocationRedux[]; + relayLocations: IRelayLocationRedux[]; allowLan: boolean; enableIpv6: boolean; blockWhenDisconnected: boolean; openVpn: { mssfix?: number; }; -}; +} -const initialState: SettingsReduxState = { +const initialState: ISettingsReduxState = { autoStart: false, guiSettings: { autoConnect: true, @@ -77,9 +77,9 @@ const initialState: SettingsReduxState = { }; export default function( - state: SettingsReduxState = initialState, + state: ISettingsReduxState = initialState, action: ReduxAction, -): SettingsReduxState { +): ISettingsReduxState { switch (action.type) { case 'UPDATE_GUI_SETTINGS': return { diff --git a/gui/packages/desktop/src/renderer/redux/store.ts b/gui/packages/desktop/src/renderer/redux/store.ts index 6bd38f45ea..e1324607dc 100644 --- a/gui/packages/desktop/src/renderer/redux/store.ts +++ b/gui/packages/desktop/src/renderer/redux/store.ts @@ -1,42 +1,29 @@ -import { createStore, applyMiddleware, combineReducers, compose, Dispatch, Store } from 'redux'; -import { routerMiddleware, connectRouter, push, replace } from 'connected-react-router'; +import { connectRouter, push, replace, routerMiddleware } from 'connected-react-router'; +import { applyMiddleware, combineReducers, compose, createStore, Dispatch, Store } from 'redux'; -import accountReducer from './account/reducers'; -import accountActions from './account/actions'; -import connectionReducer from './connection/reducers'; -import connectionActions from './connection/actions'; -import settingsReducer from './settings/reducers'; -import settingsActions from './settings/actions'; -import supportReducer from './support/reducers'; -import supportActions from './support/actions'; -import versionReducer from './version/reducers'; -import versionActions from './version/actions'; -import userInterfaceReducer from './userinterface/reducers'; -import userInterfaceActions from './userinterface/actions'; +import accountActions, { AccountAction } from './account/actions'; +import accountReducer, { IAccountReduxState } from './account/reducers'; +import connectionActions, { ConnectionAction } from './connection/actions'; +import connectionReducer, { IConnectionReduxState } from './connection/reducers'; +import settingsActions, { SettingsAction } from './settings/actions'; +import settingsReducer, { ISettingsReduxState } from './settings/reducers'; +import supportActions, { SupportAction } from './support/actions'; +import supportReducer, { ISupportReduxState } from './support/reducers'; +import userInterfaceActions, { UserInterfaceAction } from './userinterface/actions'; +import userInterfaceReducer, { IUserInterfaceReduxState } from './userinterface/reducers'; +import versionActions, { VersionAction } from './version/actions'; +import versionReducer, { IVersionReduxState } from './version/reducers'; import { History } from 'history'; -import { AccountReduxState } from './account/reducers'; -import { ConnectionReduxState } from './connection/reducers'; -import { SettingsReduxState } from './settings/reducers'; -import { SupportReduxState } from './support/reducers'; -import { VersionReduxState } from './version/reducers'; -import { UserInterfaceReduxState } from './userinterface/reducers'; -import { AccountAction } from './account/actions'; -import { ConnectionAction } from './connection/actions'; -import { SettingsAction } from './settings/actions'; -import { SupportAction } from './support/actions'; -import { VersionAction } from './version/actions'; -import { UserInterfaceAction } from './userinterface/actions'; - -export type ReduxState = { - account: AccountReduxState; - connection: ConnectionReduxState; - settings: SettingsReduxState; - support: SupportReduxState; - version: VersionReduxState; - userInterface: UserInterfaceReduxState; -}; +export interface IReduxState { + account: IAccountReduxState; + connection: IConnectionReduxState; + settings: ISettingsReduxState; + support: ISupportReduxState; + version: IVersionReduxState; + userInterface: IUserInterfaceReduxState; +} export type ReduxAction = | AccountAction @@ -45,14 +32,14 @@ export type ReduxAction = | SupportAction | VersionAction | UserInterfaceAction; -export type ReduxStore = Store<ReduxState, ReduxAction>; +export type ReduxStore = Store<IReduxState, ReduxAction>; export type ReduxDispatch = Dispatch<ReduxAction>; export default function configureStore( - initialState: ReduxState | null, routerHistory: History, + initialState?: IReduxState, ): ReduxStore { - const actionCreators: { [key: string]: Function } = { + const actionCreators = { ...accountActions, ...connectionActions, ...settingsActions, diff --git a/gui/packages/desktop/src/renderer/redux/support/actions.ts b/gui/packages/desktop/src/renderer/redux/support/actions.ts index 1ab40a775a..23cb57daf3 100644 --- a/gui/packages/desktop/src/renderer/redux/support/actions.ts +++ b/gui/packages/desktop/src/renderer/redux/support/actions.ts @@ -1,27 +1,27 @@ -export type SupportReportForm = { +export interface ISupportReportForm { email: string; message: string; -}; +} -export type KeepReportFormAction = { +export interface IKeepReportFormAction { type: 'SAVE_REPORT_FORM'; - form: SupportReportForm; -}; + form: ISupportReportForm; +} -export type ClearReportFormAction = { +export interface IClearReportFormAction { type: 'CLEAR_REPORT_FORM'; -}; +} -export type SupportAction = KeepReportFormAction | ClearReportFormAction; +export type SupportAction = IKeepReportFormAction | IClearReportFormAction; -function saveReportForm(form: SupportReportForm): KeepReportFormAction { +function saveReportForm(form: ISupportReportForm): IKeepReportFormAction { return { type: 'SAVE_REPORT_FORM', form, }; } -function clearReportForm(): ClearReportFormAction { +function clearReportForm(): IClearReportFormAction { return { type: 'CLEAR_REPORT_FORM', }; diff --git a/gui/packages/desktop/src/renderer/redux/support/reducers.ts b/gui/packages/desktop/src/renderer/redux/support/reducers.ts index 19bb395782..7a300cf2ca 100644 --- a/gui/packages/desktop/src/renderer/redux/support/reducers.ts +++ b/gui/packages/desktop/src/renderer/redux/support/reducers.ts @@ -1,19 +1,19 @@ import { ReduxAction } from '../store'; -export type SupportReduxState = { +export interface ISupportReduxState { email: string; message: string; -}; +} -const initialState: SupportReduxState = { +const initialState: ISupportReduxState = { email: '', message: '', }; export default function( - state: SupportReduxState = initialState, + state: ISupportReduxState = initialState, action: ReduxAction, -): SupportReduxState { +): ISupportReduxState { switch (action.type) { case 'SAVE_REPORT_FORM': return { diff --git a/gui/packages/desktop/src/renderer/redux/userinterface/actions.ts b/gui/packages/desktop/src/renderer/redux/userinterface/actions.ts index 701639f884..724f53883d 100644 --- a/gui/packages/desktop/src/renderer/redux/userinterface/actions.ts +++ b/gui/packages/desktop/src/renderer/redux/userinterface/actions.ts @@ -1,23 +1,25 @@ -export type UpdateWindowArrowPositionAction = { +export interface IUpdateWindowArrowPositionAction { type: 'UPDATE_WINDOW_ARROW_POSITION'; arrowPosition: number; -}; +} -export type UpdateConnectionInfoOpenAction = { +export interface IUpdateConnectionInfoOpenAction { type: 'UPDATE_CONNECTION_INFO_OPEN'; isOpen: boolean; -}; +} -export type UserInterfaceAction = UpdateWindowArrowPositionAction | UpdateConnectionInfoOpenAction; +export type UserInterfaceAction = + | IUpdateWindowArrowPositionAction + | IUpdateConnectionInfoOpenAction; -function updateWindowArrowPosition(arrowPosition: number): UpdateWindowArrowPositionAction { +function updateWindowArrowPosition(arrowPosition: number): IUpdateWindowArrowPositionAction { return { type: 'UPDATE_WINDOW_ARROW_POSITION', arrowPosition, }; } -function updateConnectionInfoOpen(isOpen: boolean): UpdateConnectionInfoOpenAction { +function updateConnectionInfoOpen(isOpen: boolean): IUpdateConnectionInfoOpenAction { return { type: 'UPDATE_CONNECTION_INFO_OPEN', isOpen, diff --git a/gui/packages/desktop/src/renderer/redux/userinterface/reducers.ts b/gui/packages/desktop/src/renderer/redux/userinterface/reducers.ts index d46ea0a2e2..75005fd423 100644 --- a/gui/packages/desktop/src/renderer/redux/userinterface/reducers.ts +++ b/gui/packages/desktop/src/renderer/redux/userinterface/reducers.ts @@ -1,18 +1,18 @@ import { ReduxAction } from '../store'; -export type UserInterfaceReduxState = { +export interface IUserInterfaceReduxState { arrowPosition?: number; connectionInfoOpen: boolean; -}; +} -const initialState: UserInterfaceReduxState = { +const initialState: IUserInterfaceReduxState = { connectionInfoOpen: false, }; export default function( - state: UserInterfaceReduxState = initialState, + state: IUserInterfaceReduxState = initialState, action: ReduxAction, -): UserInterfaceReduxState { +): IUserInterfaceReduxState { switch (action.type) { case 'UPDATE_WINDOW_ARROW_POSITION': return { ...state, arrowPosition: action.arrowPosition }; diff --git a/gui/packages/desktop/src/renderer/redux/version/actions.ts b/gui/packages/desktop/src/renderer/redux/version/actions.ts index 9faadf963e..1eefb7e62c 100644 --- a/gui/packages/desktop/src/renderer/redux/version/actions.ts +++ b/gui/packages/desktop/src/renderer/redux/version/actions.ts @@ -1,31 +1,31 @@ -import { AppVersionInfo } from '../../../shared/daemon-rpc-types'; +import { IAppVersionInfo } from '../../../shared/daemon-rpc-types'; -type UpdateLatestActionPayload = { +interface IUpdateLatestActionPayload extends IAppVersionInfo { upToDate: boolean; nextUpgrade?: string; -} & AppVersionInfo; +} -export type UpdateLatestAction = { +export interface IUpdateLatestAction { type: 'UPDATE_LATEST'; - latestInfo: UpdateLatestActionPayload; -}; + latestInfo: IUpdateLatestActionPayload; +} -export type UpdateVersionAction = { +export interface IUpdateVersionAction { type: 'UPDATE_VERSION'; version: string; consistent: boolean; -}; +} -export type VersionAction = UpdateLatestAction | UpdateVersionAction; +export type VersionAction = IUpdateLatestAction | IUpdateVersionAction; -function updateLatest(latestInfo: UpdateLatestActionPayload): UpdateLatestAction { +function updateLatest(latestInfo: IUpdateLatestActionPayload): IUpdateLatestAction { return { type: 'UPDATE_LATEST', latestInfo, }; } -function updateVersion(version: string, consistent: boolean): UpdateVersionAction { +function updateVersion(version: string, consistent: boolean): IUpdateVersionAction { return { type: 'UPDATE_VERSION', version, diff --git a/gui/packages/desktop/src/renderer/redux/version/reducers.ts b/gui/packages/desktop/src/renderer/redux/version/reducers.ts index e496b61563..775b605ded 100644 --- a/gui/packages/desktop/src/renderer/redux/version/reducers.ts +++ b/gui/packages/desktop/src/renderer/redux/version/reducers.ts @@ -1,6 +1,6 @@ import { ReduxAction } from '../store'; -export type VersionReduxState = { +export interface IVersionReduxState { current: string; currentIsSupported: boolean; latest?: string; @@ -8,9 +8,9 @@ export type VersionReduxState = { nextUpgrade?: string; upToDate: boolean; consistent: boolean; -}; +} -const initialState: VersionReduxState = { +const initialState: IVersionReduxState = { current: '', currentIsSupported: true, latest: undefined, @@ -21,9 +21,9 @@ const initialState: VersionReduxState = { }; export default function( - state: VersionReduxState = initialState, + state: IVersionReduxState = initialState, action: ReduxAction, -): VersionReduxState { +): IVersionReduxState { switch (action.type) { case 'UPDATE_LATEST': { const { latest, ...other } = action.latestInfo; diff --git a/gui/packages/desktop/src/renderer/routes.tsx b/gui/packages/desktop/src/renderer/routes.tsx index 74c9be5bee..7aaf15d1e7 100644 --- a/gui/packages/desktop/src/renderer/routes.tsx +++ b/gui/packages/desktop/src/renderer/routes.tsx @@ -1,61 +1,61 @@ import * as React from 'react'; -import { Switch, Route } from 'react-router'; +import { Route, RouteComponentProps, Switch } from 'react-router'; +import App from './app'; import TransitionContainer from './components/TransitionContainer'; -import PlatformWindowContainer from './containers/PlatformWindowContainer'; +import AccountPage from './containers/AccountPage'; +import AdvancedSettingsPage from './containers/AdvancedSettingsPage'; +import ConnectPage from './containers/ConnectPage'; import LaunchPage from './containers/LaunchPage'; import LoginPage from './containers/LoginPage'; -import ConnectPage from './containers/ConnectPage'; -import SettingsPage from './containers/SettingsPage'; -import AdvancedSettingsPage from './containers/AdvancedSettingsPage'; -import AccountPage from './containers/AccountPage'; +import PlatformWindowContainer from './containers/PlatformWindowContainer'; import PreferencesPage from './containers/PreferencesPage'; -import SupportPage from './containers/SupportPage'; import SelectLocationPage from './containers/SelectLocationPage'; +import SettingsPage from './containers/SettingsPage'; +import SupportPage from './containers/SupportPage'; import { getTransitionProps } from './transitions'; -import App from './app'; -export type SharedRouteProps = { +export interface ISharedRouteProps { app: App; -}; +} type CustomRouteProps = { - component: React.ComponentClass<SharedRouteProps>; + component: React.ComponentClass<ISharedRouteProps>; } & Route['props']; -export default function makeRoutes(componentProps: SharedRouteProps) { +export default function makeRoutes(componentProps: ISharedRouteProps) { // Renders a route extended with shared props - const CustomRoute = ({ component: ComponentClass, ...routeProps }: CustomRouteProps) => ( - <Route {...routeProps} render={() => <ComponentClass {...componentProps} />} /> - ); + function CustomRoute({ component: ComponentClass, ...routeProps }: CustomRouteProps) { + const renderOverride = () => <ComponentClass {...componentProps} />; + + return <Route {...routeProps} render={renderOverride} />; + } // store previous route let sourceRoute: string | null = null; - return ( - <Route - render={({ location }) => { - const destinationRoute = location.pathname; - const transitionProps = getTransitionProps(sourceRoute, destinationRoute); - sourceRoute = destinationRoute; + function renderRoute({ location }: RouteComponentProps) { + const destinationRoute = location.pathname; + const transitionProps = getTransitionProps(sourceRoute, destinationRoute); + sourceRoute = destinationRoute; + + return ( + <PlatformWindowContainer> + <TransitionContainer {...transitionProps}> + <Switch key={location.key} location={location}> + <CustomRoute exact={true} path="/" component={LaunchPage} /> + <CustomRoute exact={true} path="/login" component={LoginPage} /> + <CustomRoute exact={true} path="/connect" component={ConnectPage} /> + <CustomRoute exact={true} path="/settings" component={SettingsPage} /> + <CustomRoute exact={true} path="/settings/account" component={AccountPage} /> + <CustomRoute exact={true} path="/settings/preferences" component={PreferencesPage} /> + <CustomRoute exact={true} path="/settings/advanced" component={AdvancedSettingsPage} /> + <CustomRoute exact={true} path="/settings/support" component={SupportPage} /> + <CustomRoute exact={true} path="/select-location" component={SelectLocationPage} /> + </Switch> + </TransitionContainer> + </PlatformWindowContainer> + ); + } - return ( - <PlatformWindowContainer> - <TransitionContainer {...transitionProps}> - <Switch key={location.key} location={location}> - <CustomRoute exact path="/" component={LaunchPage} /> - <CustomRoute exact path="/login" component={LoginPage} /> - <CustomRoute exact path="/connect" component={ConnectPage} /> - <CustomRoute exact path="/settings" component={SettingsPage} /> - <CustomRoute exact path="/settings/account" component={AccountPage} /> - <CustomRoute exact path="/settings/preferences" component={PreferencesPage} /> - <CustomRoute exact path="/settings/advanced" component={AdvancedSettingsPage} /> - <CustomRoute exact path="/settings/support" component={SupportPage} /> - <CustomRoute exact path="/select-location" component={SelectLocationPage} /> - </Switch> - </TransitionContainer> - </PlatformWindowContainer> - ); - }} - /> - ); + return <Route render={renderRoute} />; } diff --git a/gui/packages/desktop/src/renderer/transitions.ts b/gui/packages/desktop/src/renderer/transitions.ts index 71585f9c24..7a8ac825c2 100644 --- a/gui/packages/desktop/src/renderer/transitions.ts +++ b/gui/packages/desktop/src/renderer/transitions.ts @@ -1,19 +1,18 @@ -import TransitionRule from './lib/transition-rule'; -import { TransitionFork, TransitionDescriptor } from './lib/transition-rule'; +import TransitionRule, { ITransitionDescriptor, ITransitionFork } from './lib/transition-rule'; -export type TransitionGroupProps = { +export interface ITransitionGroupProps { name: string; duration: number; -}; +} -type TransitionMap = { - [name: string]: TransitionFork; -}; +interface ITransitionMap { + [name: string]: ITransitionFork; +} /** * Transition descriptors */ -const transitions: TransitionMap = { +const transitions: ITransitionMap = { slide: { forward: { name: 'slide-up', @@ -58,7 +57,7 @@ const transitionRules = [ export function getTransitionProps( fromRoute: string | null, toRoute: string, -): TransitionGroupProps { +): ITransitionGroupProps { // ignore initial transition and transition between the same routes if (!fromRoute || fromRoute === toRoute) { return noTransitionProps(); @@ -75,21 +74,21 @@ export function getTransitionProps( } /** - * Integrate TransitionDescriptor into TransitionGroupProps - * @param {TransitionDescriptor} descriptor + * Integrate ITransitionDescriptor into ITransitionGroupProps + * @param {ITransitionDescriptor} descriptor */ -function toTransitionGroupProps(descriptor: TransitionDescriptor): TransitionGroupProps { +function toTransitionGroupProps(descriptor: ITransitionDescriptor): ITransitionGroupProps { const { name, duration } = descriptor; return { - name: name, - duration: duration, + name, + duration, }; } /** * Returns default props with no animation */ -function noTransitionProps(): TransitionGroupProps { +function noTransitionProps(): ITransitionGroupProps { return { name: '', duration: 0, @@ -99,6 +98,6 @@ function noTransitionProps(): TransitionGroupProps { /** * Shortcut to create TransitionRule */ -function r(from: string | null, to: string, fork: TransitionFork): TransitionRule { +function r(from: string | null, to: string, fork: ITransitionFork): TransitionRule { return new TransitionRule(from, to, fork); } diff --git a/gui/packages/desktop/src/shared/daemon-rpc-types.ts b/gui/packages/desktop/src/shared/daemon-rpc-types.ts index 2cfc30f884..894978cbca 100644 --- a/gui/packages/desktop/src/shared/daemon-rpc-types.ts +++ b/gui/packages/desktop/src/shared/daemon-rpc-types.ts @@ -1,7 +1,9 @@ -export type AccountData = { expiry: string }; +export interface IAccountData { + expiry: string; +} export type AccountToken = string; export type Ip = string; -export type Location = { +export interface ILocation { ip?: string; country: string; city?: string; @@ -9,7 +11,7 @@ export type Location = { longitude: number; mullvadExitIp: boolean; hostname?: string; -}; +} export type BlockReason = | { @@ -32,16 +34,16 @@ export type TunnelType = 'wireguard' | 'openvpn'; export type RelayProtocol = 'tcp' | 'udp'; -export type TunnelEndpoint = { +export interface ITunnelEndpoint { address: string; protocol: RelayProtocol; tunnel: TunnelType; -}; +} export type TunnelStateTransition = | { state: 'disconnected' } - | { state: 'connecting'; details?: TunnelEndpoint } - | { state: 'connected'; details: TunnelEndpoint } + | { state: 'connecting'; details?: ITunnelEndpoint } + | { state: 'connected'; details: ITunnelEndpoint } | { state: 'disconnecting'; details: AfterDisconnect } | { state: 'blocked'; details: BlockReason }; @@ -50,16 +52,16 @@ export type RelayLocation = | { city: [string, string] } | { country: string }; -export type OpenVpnConstraints = { +export interface IOpenVpnConstraints { port: 'any' | { only: number }; protocol: 'any' | { only: RelayProtocol }; -}; +} -type TunnelConstraints<TOpenVpnConstraints> = { +interface ITunnelConstraints<TOpenVpnConstraints> { openvpn: TOpenVpnConstraints; -}; +} -type RelaySettingsNormal<TTunnelConstraints> = { +interface IRelaySettingsNormal<TTunnelConstraints> { location: | 'any' | { @@ -70,7 +72,7 @@ type RelaySettingsNormal<TTunnelConstraints> = { | { only: TTunnelConstraints; }; -}; +} export type ConnectionConfig = | { @@ -87,11 +89,11 @@ export type ConnectionConfig = wireguard: { tunnel: { private_key: string; - addresses: Array<string>; + addresses: string[]; }; peer: { public_key: string; - addresses: Array<string>; + addresses: string[]; endpoint: string; }; gateway: string; @@ -99,56 +101,56 @@ export type ConnectionConfig = }; // types describing the structure of RelaySettings -export type RelaySettingsCustom = { +export interface IRelaySettingsCustom { host: string; config: ConnectionConfig; -}; +} export type RelaySettings = | { - normal: RelaySettingsNormal<TunnelConstraints<OpenVpnConstraints>>; + normal: IRelaySettingsNormal<ITunnelConstraints<IOpenVpnConstraints>>; } | { - customTunnelEndpoint: RelaySettingsCustom; + customTunnelEndpoint: IRelaySettingsCustom; }; // types describing the partial update of RelaySettings export type RelaySettingsNormalUpdate = Partial< - RelaySettingsNormal<TunnelConstraints<Partial<OpenVpnConstraints>>> + IRelaySettingsNormal<ITunnelConstraints<Partial<IOpenVpnConstraints>>> >; export type RelaySettingsUpdate = | { normal: RelaySettingsNormalUpdate; } | { - customTunnelEndpoint: RelaySettingsCustom; + customTunnelEndpoint: IRelaySettingsCustom; }; -export type RelayList = { - countries: Array<RelayListCountry>; -}; +export interface IRelayList { + countries: IRelayListCountry[]; +} -export type RelayListCountry = { +export interface IRelayListCountry { name: string; code: string; - cities: Array<RelayListCity>; -}; + cities: IRelayListCity[]; +} -export type RelayListCity = { +export interface IRelayListCity { name: string; code: string; latitude: number; longitude: number; - relays: Array<RelayListHostname>; -}; + relays: IRelayListHostname[]; +} -export type RelayListHostname = { +export interface IRelayListHostname { hostname: string; ipv4AddrIn: string; includeInCountry: boolean; weight: number; -}; +} -export type TunnelOptions = { +export interface ITunnelOptions { openvpn: { mssfix?: number; proxy?: ProxySettings; @@ -161,54 +163,81 @@ export type TunnelOptions = { generic: { enableIpv6: boolean; }; -}; +} -export type ProxySettings = LocalProxySettings | RemoteProxySettings; +export type ProxySettings = ILocalProxySettings | IRemoteProxySettings; -export type LocalProxySettings = { +export interface ILocalProxySettings { port: number; peer: string; -}; +} -export type RemoteProxySettings = { +export interface IRemoteProxySettings { address: string; - auth?: RemoteProxyAuth; -}; + auth?: IRemoteProxyAuth; +} -export type RemoteProxyAuth = { +export interface IRemoteProxyAuth { username: string; password: string; -}; +} -export type AppVersionInfo = { +export interface IAppVersionInfo { currentIsSupported: boolean; latest: { latestStable: string; latest: string; }; -}; +} -export type Settings = { +export interface ISettings { accountToken?: AccountToken; allowLan: boolean; autoConnect: boolean; blockWhenDisconnected: boolean; relaySettings: RelaySettings; - tunnelOptions: TunnelOptions; -}; + tunnelOptions: ITunnelOptions; +} -export type SocketAddress = { host: string; port: number }; +export interface ISocketAddress { + host: string; + port: number; +} -export function parseSocketAddress(socketAddrStr: string): SocketAddress { +export function parseSocketAddress(socketAddrStr: string): ISocketAddress { const re = new RegExp(/(.+):(\d+)$/); const matches = socketAddrStr.match(re); if (!matches || matches.length < 3) { throw new Error(`Failed to parse socket address from address string '${socketAddrStr}'`); } - const socketAddress: SocketAddress = { + const socketAddress: ISocketAddress = { host: matches[1], port: Number(matches[2]), }; return socketAddress; } + +export function compareRelayLocation(lhs: RelayLocation, rhs: RelayLocation) { + if ('country' in lhs && 'country' in rhs && lhs.country && rhs.country) { + return lhs.country === rhs.country; + } else if ('city' in lhs && 'city' in rhs && lhs.city && rhs.city) { + return lhs.city[0] === rhs.city[0] && lhs.city[1] === rhs.city[1]; + } else if ('hostname' in lhs && 'hostname' in rhs && lhs.hostname && rhs.hostname) { + return ( + lhs.hostname[0] === rhs.hostname[0] && + lhs.hostname[1] === rhs.hostname[1] && + lhs.hostname[2] === rhs.hostname[2] + ); + } else { + return false; + } +} + +export function compareRelayLocationLoose(lhs?: RelayLocation, rhs?: RelayLocation) { + if (lhs && rhs) { + return compareRelayLocation(lhs, rhs); + } else { + return lhs === rhs; + } +} diff --git a/gui/packages/desktop/src/shared/gui-settings-state.ts b/gui/packages/desktop/src/shared/gui-settings-state.ts index a94176c650..5bfb6e79c8 100644 --- a/gui/packages/desktop/src/shared/gui-settings-state.ts +++ b/gui/packages/desktop/src/shared/gui-settings-state.ts @@ -1,5 +1,5 @@ -export type GuiSettingsState = { +export interface IGuiSettingsState { autoConnect: boolean; monochromaticIcon: boolean; startMinimized: boolean; -}; +} diff --git a/gui/packages/desktop/src/shared/ipc-event-channel.ts b/gui/packages/desktop/src/shared/ipc-event-channel.ts index 3ddde3fc13..c8cb1bf4c8 100644 --- a/gui/packages/desktop/src/shared/ipc-event-channel.ts +++ b/gui/packages/desktop/src/shared/ipc-event-channel.ts @@ -1,54 +1,54 @@ import { ipcMain, ipcRenderer, WebContents } from 'electron'; import * as uuid from 'uuid'; -import { GuiSettingsState } from './gui-settings-state'; +import { IGuiSettingsState } from './gui-settings-state'; -import { AppUpgradeInfo, CurrentAppVersionInfo } from '../main/index'; +import { IAppUpgradeInfo, ICurrentAppVersionInfo } from '../main/index'; import { AccountToken, - AccountData, - Location, - RelayList, + IAccountData, + ILocation, + IRelayList, + ISettings, RelaySettingsUpdate, - Settings, TunnelStateTransition, } from './daemon-rpc-types'; -export type AppStateSnapshot = { +export interface IAppStateSnapshot { isConnected: boolean; autoStart: boolean; tunnelState: TunnelStateTransition; - settings: Settings; - location?: Location; - relays: RelayList; - currentVersion: CurrentAppVersionInfo; - upgradeVersion: AppUpgradeInfo; - guiSettings: GuiSettingsState; -}; + settings: ISettings; + location?: ILocation; + relays: IRelayList; + currentVersion: ICurrentAppVersionInfo; + upgradeVersion: IAppUpgradeInfo; + guiSettings: IGuiSettingsState; +} -interface Sender<T> { +interface ISender<T> { notify(webContents: WebContents, value: T): void; } -interface SenderVoid { +interface ISenderVoid { notify(webContents: WebContents): void; } -interface Receiver<T> { +interface IReceiver<T> { listen(fn: (value: T) => void): void; } -interface TunnelMethods { +interface ITunnelMethods { connect(): Promise<void>; disconnect(): Promise<void>; } -interface TunnelHandlers { +interface ITunnelHandlers { handleConnect(fn: () => Promise<void>): void; handleDisconnect(fn: () => Promise<void>): void; } -interface SettingsMethods { +interface ISettingsMethods { setAllowLan(allowLan: boolean): Promise<void>; setEnableIpv6(enableIpv6: boolean): Promise<void>; setBlockWhenDisconnected(block: boolean): Promise<void>; @@ -56,7 +56,7 @@ interface SettingsMethods { updateRelaySettings(update: RelaySettingsUpdate): Promise<void>; } -interface SettingsHandlers { +interface ISettingsHandlers { handleAllowLan(fn: (allowLan: boolean) => Promise<void>): void; handleEnableIpv6(fn: (enableIpv6: boolean) => Promise<void>): void; handleBlockWhenDisconnected(fn: (block: boolean) => Promise<void>): void; @@ -64,45 +64,45 @@ interface SettingsHandlers { handleUpdateRelaySettings(fn: (update: RelaySettingsUpdate) => Promise<void>): void; } -interface GuiSettingsMethods { +interface IGuiSettingsMethods { setAutoConnect(autoConnect: boolean): void; setStartMinimized(startMinimized: boolean): void; setMonochromaticIcon(monochromaticIcon: boolean): void; } -interface GuiSettingsHandlers { +interface IGuiSettingsHandlers { handleAutoConnect(fn: (autoConnect: boolean) => void): void; handleStartMinimized(fn: (startMinimized: boolean) => void): void; handleMonochromaticIcon(fn: (monochromaticIcon: boolean) => void): void; } -interface AccountHandlers { +interface IAccountHandlers { handleSet(fn: (token: AccountToken) => Promise<void>): void; handleUnset(fn: () => Promise<void>): void; - handleGetData(fn: (token: AccountToken) => Promise<AccountData>): void; + handleGetData(fn: (token: AccountToken) => Promise<IAccountData>): void; } -interface AccountMethods { +interface IAccountMethods { set(token: AccountToken): Promise<void>; unset(): Promise<void>; - getData(token: AccountToken): Promise<AccountData>; + getData(token: AccountToken): Promise<IAccountData>; } -interface AccountHistoryHandlers { - handleGet(fn: () => Promise<Array<AccountToken>>): void; +interface IAccountHistoryHandlers { + handleGet(fn: () => Promise<AccountToken[]>): void; handleRemoveItem(fn: (token: AccountToken) => Promise<void>): void; } -interface AccountHistoryMethods { - get(): Promise<Array<AccountToken>>; +interface IAccountHistoryMethods { + get(): Promise<AccountToken[]>; removeItem(token: AccountToken): Promise<void>; } -interface AutoStartMethods { +interface IAutoStartMethods { set(autoStart: boolean): Promise<void>; } -interface AutoStartHandlers { +interface IAutoStartHandlers { handleSet(fn: (value: boolean) => Promise<void>): void; } @@ -150,28 +150,28 @@ const SET_AUTO_START = 'set-auto-start'; /// instance methods are meant to be used from a main process. /// export class IpcRendererEventChannel { - static state = { + public static state = { /// Synchronously sends the IPC request and returns the app state snapshot - get(): AppStateSnapshot { + get(): IAppStateSnapshot { return ipcRenderer.sendSync(GET_APP_STATE); }, }; - static daemonConnected: Receiver<void> = { + public static daemonConnected: IReceiver<void> = { listen: listen(DAEMON_CONNECTED), }; - static daemonDisconnected: Receiver<string | undefined> = { + public static daemonDisconnected: IReceiver<string | undefined> = { listen: listen(DAEMON_DISCONNECTED), }; - static tunnel: Receiver<TunnelStateTransition> & TunnelMethods = { + public static tunnel: IReceiver<TunnelStateTransition> & ITunnelMethods = { listen: listen(TUNNEL_STATE_CHANGED), connect: requestSender(CONNECT_TUNNEL), disconnect: requestSender(DISCONNECT_TUNNEL), }; - static settings: Receiver<Settings> & SettingsMethods = { + public static settings: IReceiver<ISettings> & ISettingsMethods = { listen: listen(SETTINGS_CHANGED), setAllowLan: requestSender(SET_ALLOW_LAN), setEnableIpv6: requestSender(SET_ENABLE_IPV6), @@ -180,74 +180,74 @@ export class IpcRendererEventChannel { updateRelaySettings: requestSender(UPDATE_RELAY_SETTINGS), }; - static location: Receiver<Location> = { - listen: listen<Location>(LOCATION_CHANGED), + public static location: IReceiver<ILocation> = { + listen: listen(LOCATION_CHANGED), }; - static relays: Receiver<RelayList> = { + public static relays: IReceiver<IRelayList> = { listen: listen(RELAYS_CHANGED), }; - static currentVersion: Receiver<CurrentAppVersionInfo> = { + public static currentVersion: IReceiver<ICurrentAppVersionInfo> = { listen: listen(CURRENT_VERSION_CHANGED), }; - static upgradeVersion: Receiver<AppUpgradeInfo> = { + public static upgradeVersion: IReceiver<IAppUpgradeInfo> = { listen: listen(UPGRADE_VERSION_CHANGED), }; - static guiSettings: Receiver<GuiSettingsState> & GuiSettingsMethods = { + public static guiSettings: IReceiver<IGuiSettingsState> & IGuiSettingsMethods = { listen: listen(GUI_SETTINGS_CHANGED), setAutoConnect: set(SET_AUTO_CONNECT), setMonochromaticIcon: set(SET_MONOCHROMATIC_ICON), setStartMinimized: set(SET_START_MINIMIZED), }; - static autoStart: Receiver<boolean> & AutoStartMethods = { + public static autoStart: IReceiver<boolean> & IAutoStartMethods = { listen: listen(AUTO_START_CHANGED), set: requestSender(SET_AUTO_START), }; - static account: AccountMethods = { + public static account: IAccountMethods = { set: requestSender(SET_ACCOUNT), unset: requestSender(UNSET_ACCOUNT), getData: requestSender(GET_ACCOUNT_DATA), }; - static accountHistory: AccountHistoryMethods = { + public static accountHistory: IAccountHistoryMethods = { get: requestSender(GET_ACCOUNT_HISTORY), removeItem: requestSender(REMOVE_ACCOUNT_HISTORY_ITEM), }; } export class IpcMainEventChannel { - static state = { - handleGet(fn: () => AppStateSnapshot) { - ipcMain.on(GET_APP_STATE, (event: any) => { + public static state = { + handleGet(fn: () => IAppStateSnapshot) { + ipcMain.on(GET_APP_STATE, (event: Electron.Event) => { event.returnValue = fn(); }); }, }; - static daemonConnected: SenderVoid = { + public static daemonConnected: ISenderVoid = { notify: senderVoid(DAEMON_CONNECTED), }; - static daemonDisconnected: Sender<string | undefined> = { + public static daemonDisconnected: ISender<string | undefined> = { notify: sender(DAEMON_DISCONNECTED), }; - static tunnel: Sender<TunnelStateTransition> & TunnelHandlers = { + public static tunnel: ISender<TunnelStateTransition> & ITunnelHandlers = { notify: sender(TUNNEL_STATE_CHANGED), handleConnect: requestHandler(CONNECT_TUNNEL), handleDisconnect: requestHandler(DISCONNECT_TUNNEL), }; - static location: Sender<Location> = { + public static location: ISender<ILocation> = { notify: sender(LOCATION_CHANGED), }; - static settings: Sender<Settings> & SettingsHandlers = { + public static settings: ISender<ISettings> & ISettingsHandlers = { notify: sender(SETTINGS_CHANGED), handleAllowLan: requestHandler(SET_ALLOW_LAN), handleEnableIpv6: requestHandler(SET_ENABLE_IPV6), @@ -256,50 +256,50 @@ export class IpcMainEventChannel { handleUpdateRelaySettings: requestHandler(UPDATE_RELAY_SETTINGS), }; - static relays: Sender<RelayList> = { + public static relays: ISender<IRelayList> = { notify: sender(RELAYS_CHANGED), }; - static currentVersion: Sender<CurrentAppVersionInfo> = { + public static currentVersion: ISender<ICurrentAppVersionInfo> = { notify: sender(CURRENT_VERSION_CHANGED), }; - static upgradeVersion: Sender<AppUpgradeInfo> = { + public static upgradeVersion: ISender<IAppUpgradeInfo> = { notify: sender(UPGRADE_VERSION_CHANGED), }; - static guiSettings: Sender<GuiSettingsState> & GuiSettingsHandlers = { + public static guiSettings: ISender<IGuiSettingsState> & IGuiSettingsHandlers = { notify: sender(GUI_SETTINGS_CHANGED), handleAutoConnect: handler(SET_AUTO_CONNECT), handleMonochromaticIcon: handler(SET_MONOCHROMATIC_ICON), handleStartMinimized: handler(SET_START_MINIMIZED), }; - static autoStart: Sender<boolean> & AutoStartHandlers = { + public static autoStart: ISender<boolean> & IAutoStartHandlers = { notify: sender<boolean>(AUTO_START_CHANGED), handleSet: requestHandler(SET_AUTO_START), }; - static account: AccountHandlers = { + public static account: IAccountHandlers = { handleSet: requestHandler(SET_ACCOUNT), handleUnset: requestHandler(UNSET_ACCOUNT), handleGetData: requestHandler(GET_ACCOUNT_DATA), }; - static accountHistory: AccountHistoryHandlers = { + public static accountHistory: IAccountHistoryHandlers = { handleGet: requestHandler(GET_ACCOUNT_HISTORY), handleRemoveItem: requestHandler(REMOVE_ACCOUNT_HISTORY_ITEM), }; } function listen<T>(event: string): (fn: (value: T) => void) => void { - return function(fn: (value: T) => void) { - ipcRenderer.on(event, (_event: any, newState: T) => fn(newState)); + return (fn: (value: T) => void) => { + ipcRenderer.on(event, (_event: Electron.Event, newState: T) => fn(newState)); }; } function set<T>(event: string): (value: T) => void { - return function(newValue: T) { + return (newValue: T) => { ipcRenderer.send(event, newValue); }; } @@ -311,14 +311,14 @@ function sender<T>(event: string): (webContents: WebContents, value: T) => void } function senderVoid(event: string): (webContents: WebContents) => void { - return function(webContents: WebContents) { + return (webContents: WebContents) => { webContents.send(event); }; } function handler<T>(event: string): (handlerFn: (value: T) => void) => void { - return function(handlerFn: (value: T) => void) { - ipcMain.on(event, (_: any, newValue: T) => { + return (handlerFn: (value: T) => void) => { + ipcMain.on(event, (_event: Electron.Event, newValue: T) => { handlerFn(newValue); }); }; @@ -326,31 +326,30 @@ function handler<T>(event: string): (handlerFn: (value: T) => void) => void { type RequestResult<T> = { type: 'success'; value: T } | { type: 'error'; message: string }; -function requestHandler<T>(event: string): (fn: (...args: Array<any>) => Promise<T>) => void { - return function(fn: (...args: Array<any>) => Promise<T>) { - ipcMain.on(event, async (ipcEvent: any, requestId: string, ...args: Array<any>) => { - const sender = ipcEvent.sender; +function requestHandler<T>(event: string): (fn: (...args: any[]) => Promise<T>) => void { + return (fn: (...args: any[]) => Promise<T>) => { + ipcMain.on(event, async (ipcEvent: Electron.Event, requestId: string, ...args: any[]) => { const responseEvent = `${event}-${requestId}`; try { const result: RequestResult<T> = { type: 'success', value: await fn(...args) }; - sender.send(responseEvent, result); + ipcEvent.sender.send(responseEvent, result); } catch (error) { const result: RequestResult<T> = { type: 'error', message: error.message || '' }; - sender.send(responseEvent, result); + ipcEvent.sender.send(responseEvent, result); } }); }; } -function requestSender<T>(event: string): (...args: Array<any>) => Promise<T> { - return function(...args: Array<any>): Promise<T> { +function requestSender<T>(event: string): (...args: any[]) => Promise<T> { + return (...args: any[]): Promise<T> => { return new Promise((resolve: (result: T) => void, reject: (error: Error) => void) => { const requestId = uuid.v4(); const responseEvent = `${event}-${requestId}`; - ipcRenderer.once(responseEvent, (_ipcEvent: any, result: RequestResult<T>) => { + ipcRenderer.once(responseEvent, (_ipcEvent: Electron.Event, result: RequestResult<T>) => { switch (result.type) { case 'error': reject(new Error(result.message)); diff --git a/gui/packages/desktop/tslint.json b/gui/packages/desktop/tslint.json index 6b68bf43d6..b6c5e4e05b 100644 --- a/gui/packages/desktop/tslint.json +++ b/gui/packages/desktop/tslint.json @@ -1,3 +1,14 @@ { - "extends": "@mullvad/config/tslint.json" + "extends": "@mullvad/config/tslint.json", + "rules": { + "no-implicit-dependencies": [ + true, + "optional", + [ + "electron", + "electron-devtools-installer" + ] + ], + "no-submodule-imports": [true, "validated"] + } } |
