diff options
| author | Andrej Mihajlov <and@codeispoetry.ru> | 2017-06-21 14:05:33 +0300 |
|---|---|---|
| committer | Andrej Mihajlov <and@codeispoetry.ru> | 2017-06-21 14:05:33 +0300 |
| commit | 7950d83c2d70c72bb08415d8ecf823d85ce7bd35 (patch) | |
| tree | f280e95a6caff7fe5ecf493444b1f0319465a3df | |
| parent | 9fd74d9e39df895d8bc2c8856c9a6ad807012560 (diff) | |
| parent | b441f6bb8956436fa64e6707c310013c126b826b (diff) | |
| download | mullvadvpn-7950d83c2d70c72bb08415d8ecf823d85ce7bd35.tar.xz mullvadvpn-7950d83c2d70c72bb08415d8ecf823d85ce7bd35.zip | |
Merge branch 'feature/refactor-main'
| -rw-r--r-- | .travis.yml | 13 | ||||
| -rw-r--r-- | app/main.js | 399 | ||||
| -rw-r--r-- | flow-libs/electron.js.flow | 160 | ||||
| -rw-r--r-- | flow-libs/history.js.flow | 24 | ||||
| -rw-r--r-- | flow-libs/nseventmonitor.js.flow | 42 | ||||
| -rw-r--r-- | flow-typed/npm/electron-log_vx.x.x.js | 4 | ||||
| -rw-r--r-- | flow-typed/npm/sudo-prompt_vx.x.x.js | 4 |
7 files changed, 433 insertions, 213 deletions
diff --git a/.travis.yml b/.travis.yml index 167dfca444..a79ed8d260 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,19 @@ before_script: cache: yarn: true +install: + - yarn install + + # FIXME: Flow throws error for optional dependencies + # missing from node_modules on unsupported platforms + # see: https://github.com/facebook/flow/issues/4171 + - NSEVENTMON_INDEX_JS=node_modules/nseventmonitor/index.js; + if [ ! -f $NSEVENTMON_INDEX_JS ]; then + echo "Installing a stub for NSEventMonitor.."; + mkdir -p `dirname $NSEVENTMON_INDEX_JS`; + echo "module.exports = {};" > $NSEVENTMON_INDEX_JS; + fi + script: - yarn run lint - yarn run flow diff --git a/app/main.js b/app/main.js index 0360d43f05..62d98010ee 100644 --- a/app/main.js +++ b/app/main.js @@ -1,3 +1,4 @@ +// @flow import path from 'path'; import fs from 'fs'; import sudo from 'sudo-prompt'; @@ -5,241 +6,253 @@ import log from 'electron-log'; import { app, BrowserWindow, ipcMain, Tray, Menu, nativeImage } from 'electron'; import TrayIconManager from './lib/tray-icon-manager'; +import type { TrayIconType } from './lib/tray-icon-manager'; + const isDevelopment = (process.env.NODE_ENV === 'development'); const isMacOS = (process.platform === 'darwin'); -let window = null; -let tray = null; +const appDelegate = { + _window: (null: ?BrowserWindow), + _tray: (null: ?Tray), -// Override appData path to avoid collisions with old client -// New userData path, i.e on macOS: ~/Library/Application Support/mullvad.vpn -app.setPath('userData', path.join(app.getPath('appData'), 'mullvad.vpn')); + setup: () => { + // Override appData path to avoid collisions with old client + // New userData path, i.e on macOS: ~/Library/Application Support/mullvad.vpn + app.setPath('userData', path.join(app.getPath('appData'), 'mullvad.vpn')); -ipcMain.on('on-browser-window-ready', () => { - sendBackendInfo(); -}); + if (isDevelopment) { + log.transports.console.level = 'debug'; -const configureLogger = () => { + // Disable log file in development + log.transports.file.level = false; + } else { + log.transports.console.level = 'info'; + log.transports.file.level = 'info'; + } - if (isDevelopment) { - log.transports.console.level = 'debug'; + if (isDevelopment) { + appDelegate._startBackend(); + } - // Disable log file in development - log.transports.file.level = false; - } else { - log.transports.console.level = 'info'; - log.transports.file.level = 'info'; - } -}; -configureLogger(); + app.on('window-all-closed', () => appDelegate.onAllWindowsClosed()); + app.on('ready', () => appDelegate.onReady()); + }, -const installDevTools = async () => { - const installer = require('electron-devtools-installer'); - const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS']; - const forceDownload = !!process.env.UPGRADE_EXTENSIONS; - for(const name of extensions) { - try { - await installer.default(installer[name], forceDownload); - } catch (e) { - log.info(`Error installing ${name} extension: ${e.message}`); - } - } -}; + onReady: async () => { + const window = appDelegate._window = appDelegate._createWindow(); -const createWindow = () => { - const contentHeight = 568; - let options = { - width: 320, - height: contentHeight, - resizable: false, - maximizable: false, - fullscreenable: false, - show: true, - webPreferences: { - // prevents renderer process code from not running when window is hidden - backgroundThrottling: false, - // Enable experimental features - blinkFeatures: ['CSSBackdropFilter'].join(',') - } - }; + ipcMain.on('on-browser-window-ready', () => appDelegate._sendBackendInfo(window)); - // setup window flags to mimic popover on macOS - if(isMacOS) { - options = Object.assign({}, options, { - height: contentHeight + 12, // 12 is the size of transparent area around arrow - frame: false, - transparent: true, - show: false - }); - } + window.loadURL('file://' + path.join(__dirname, 'index.html')); - window = new BrowserWindow(options); - window.loadURL('file://' + path.join(__dirname, 'index.html')); -}; + // create tray icon on macOS + if(isMacOS) { + appDelegate._tray = appDelegate._createTray(window); + } -const createAppMenu = () => { - const template = [ - { - label: 'Mullvad', - submenu: [ - { role: 'about' }, - { type: 'separator' }, - { role: 'quit' } - ] - }, - { - label: 'Edit', - submenu: [ - { role: 'cut' }, - { role: 'copy' }, - { role: 'paste' }, - { type: 'separator' }, - { role: 'selectall' } - ] + appDelegate._setAppMenu(); + appDelegate._addContextMenu(window); + + if(isDevelopment) { + await appDelegate._installDevTools(); + window.openDevTools({ mode: 'detach' }); } - ]; + }, - Menu.setApplicationMenu(Menu.buildFromTemplate(template)); -}; + onAllWindowsClosed: () => { + app.quit(); + }, + + _startBackend: () => { + const pathToBackend = path.resolve(process.env.MULLVAD_BACKEND || '../talpid_core/target/debug/talpid_daemon'); + log.info('Starting the mullvad backend at', pathToBackend); -const createContextMenu = () => { - let menuTemplate = [ - { role: 'cut' }, - { role: 'copy' }, - { role: 'paste' }, - { type: 'separator' }, - { role: 'selectall' } - ]; + const options = { + name: 'mullvad backend', + }; - // add inspect element on right click menu - window.webContents.on('context-menu', (_e, props) => { - let inspectTemplate = [{ - label: 'Inspect element', - click() { - window.openDevTools({ mode: 'detach' }); - window.inspectElement(props.x, props.y); + sudo.exec(pathToBackend, options, (err) => { + if (err && err.signal !== 'SIGINT') { + log.info('Backend exited with error', err); + } else { + log.info('Backend exited'); } - }]; + }); + }, - if(props.isEditable) { - let inputMenu = menuTemplate; + _sendBackendInfo: (window: BrowserWindow) => { + const file = './.mullvad_rpc_address'; + log.info('reading the ipc connection info from', file); - // mixin 'inspect element' into standard menu when in development mode - if(isDevelopment) { - inputMenu = menuTemplate.concat([{type: 'separator'}], inspectTemplate); + fs.readFile(file, 'utf8', function (err,data) { + if (err) { + return log.info('Could not find backend connection info', err); } - Menu.buildFromTemplate(inputMenu).popup(window); - } else if(isDevelopment) { - // display inspect element for all non-editable - // elements when in development mode - Menu.buildFromTemplate(inspectTemplate).popup(window); + log.info('Read IPC connection info', data); + window.webContents.send('backend-info', { + addr: data, + }); + }); + }, + + _installDevTools: async () => { + const installer = require('electron-devtools-installer'); + const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS']; + const forceDownload = !!process.env.UPGRADE_EXTENSIONS; + for(const name of extensions) { + try { + await installer.default(installer[name], forceDownload); + } catch (e) { + log.info(`Error installing ${name} extension: ${e.message}`); + } } - }); -}; + }, -const toggleWindow = () => { - if (window.isVisible()) { - window.hide(); - } else { - showWindow(); - } -}; + _createWindow: (): BrowserWindow => { + const contentHeight = 568; + let options = { + width: 320, + height: contentHeight, + resizable: false, + maximizable: false, + fullscreenable: false, + webPreferences: { + // prevents renderer process code from not running when window is hidden + backgroundThrottling: false, + // Enable experimental features + blinkFeatures: 'CSSBackdropFilter' + } + }; -const showWindow = () => { - // position window based on tray icon location - if(tray) { - const { x, y } = getWindowPosition(); - window.setPosition(x, y, false); - } + // setup window flags to mimic popover on macOS + if(isMacOS) { + options = Object.assign({}, options, { + height: contentHeight + 12, // 12 is the size of transparent area around arrow + frame: false, + transparent: true, + show: false + }); + } - window.show(); - window.focus(); -}; + return new BrowserWindow(options); + }, -const getWindowPosition = () => { - const windowBounds = window.getBounds(); - const trayBounds = tray.getBounds(); + _setAppMenu: () => { + const template = [ + { + label: 'Mullvad', + submenu: [ + { role: 'about' }, + { type: 'separator' }, + { role: 'quit' } + ] + }, + { + label: 'Edit', + submenu: [ + { role: 'cut' }, + { role: 'copy' }, + { role: 'paste' }, + { type: 'separator' }, + { role: 'selectall' } + ] + } + ]; + Menu.setApplicationMenu(Menu.buildFromTemplate(template)); + }, - // center window horizontally below the tray icon - const x = Math.round(trayBounds.x + (trayBounds.width / 2) - (windowBounds.width / 2)); + _addContextMenu: (window: BrowserWindow) => { + let menuTemplate = [ + { role: 'cut' }, + { role: 'copy' }, + { role: 'paste' }, + { type: 'separator' }, + { role: 'selectall' } + ]; - // position window vertically below the tray icon - const y = Math.round(trayBounds.y + trayBounds.height); + // add inspect element on right click menu + window.webContents.on('context-menu', (_e: Event, props: { x: number, y: number }) => { + let inspectTemplate = [{ + label: 'Inspect element', + click() { + window.openDevTools({ mode: 'detach' }); + window.inspectElement(props.x, props.y); + } + }]; - return { x, y }; -}; + if(props.isEditable) { + let inputMenu = menuTemplate; -const createTray = () => { - tray = new Tray(nativeImage.createEmpty()); - tray.setHighlightMode('never'); - tray.on('click', toggleWindow); + // mixin 'inspect element' into standard menu when in development mode + if(isDevelopment) { + inputMenu = menuTemplate.concat([{type: 'separator'}], inspectTemplate); + } - // setup NSEvent monitor to fix inconsistent window.blur - // see https://github.com/electron/electron/issues/8689 - const { NSEventMonitor, NSEventMask } = require('nseventmonitor'); - const trayIconManager = new TrayIconManager(tray, 'unsecured'); - const macEventMonitor = new NSEventMonitor(); - const eventMask = NSEventMask.leftMouseDown | NSEventMask.rightMouseDown; + Menu.buildFromTemplate(inputMenu).popup(window); + } else if(isDevelopment) { + // display inspect element for all non-editable + // elements when in development mode + Menu.buildFromTemplate(inspectTemplate).popup(window); + } + }); + }, - // add IPC handler to change tray icon from renderer - ipcMain.on('changeTrayIcon', (_, type) => trayIconManager.iconType = type); + _toggleWindow: (window: BrowserWindow, tray: ?Tray) => { + if(window.isVisible()) { + window.hide(); + } else { + appDelegate._showWindow(window, tray); + } + }, - // setup event handlers - window.on('show', () => macEventMonitor.start(eventMask, () => window.hide())); - window.on('hide', () => macEventMonitor.stop()); - window.on('close', () => window.closeDevTools()); - window.on('blur', () => !window.isDevToolsOpened() && window.hide()); -}; + _showWindow: (window: BrowserWindow, tray: ?Tray) => { + // position window based on tray icon location + if(tray) { + const { x, y } = appDelegate._getWindowPosition(window, tray); + window.setPosition(x, y, false); + } -app.on('window-all-closed', () => { - app.quit(); -}); + window.show(); + window.focus(); + }, -app.on('ready', async () => { - createWindow(); + _getWindowPosition: (window: BrowserWindow, tray: Tray): { x: number, y: number } => { + const windowBounds = window.getBounds(); + const trayBounds = tray.getBounds(); - // create tray icon on macOS - isMacOS && createTray(); + // center window horizontally below the tray icon + const x = Math.round(trayBounds.x + (trayBounds.width / 2) - (windowBounds.width / 2)); - createAppMenu(); - createContextMenu(); + // position window vertically below the tray icon + const y = Math.round(trayBounds.y + trayBounds.height); - if(isDevelopment) { - await installDevTools(); - window.openDevTools({ mode: 'detach' }); - } -}); + return { x, y }; + }, -const sendBackendInfo = () => { - const file = './.mullvad_rpc_address'; - log.info('reading the ipc connection info from', file); + _createTray: (window: BrowserWindow): Tray => { + const tray = new Tray(nativeImage.createEmpty()); + tray.setHighlightMode('never'); + tray.on('click', () => appDelegate._toggleWindow(window, tray)); - fs.readFile(file, 'utf8', function (err,data) { - if (err) { - return log.info('Could not find backend connection info', err); - } + // setup NSEvent monitor to fix inconsistent window.blur + // see https://github.com/electron/electron/issues/8689 + const { NSEventMonitor, NSEventMask } = require('nseventmonitor'); + const trayIconManager = new TrayIconManager(tray, 'unsecured'); + const macEventMonitor = new NSEventMonitor(); + const eventMask = NSEventMask.leftMouseDown | NSEventMask.rightMouseDown; - log.info('Read IPC connection info', data); - window.webContents.send('backend-info', { - addr: data, - }); - }); -}; + // add IPC handler to change tray icon from renderer + ipcMain.on('changeTrayIcon', (_: Event, type: TrayIconType) => trayIconManager.iconType = type); -const startBackend = () => { - const pathToBackend = path.resolve(process.env.MULLVAD_BACKEND || '../talpid_core/target/debug/talpid_daemon'); - log.info('Starting the mullvad backend at', pathToBackend); + // setup event handlers + window.on('show', () => macEventMonitor.start(eventMask, () => window.hide())); + window.on('hide', () => macEventMonitor.stop()); + window.on('close', () => window.closeDevTools()); + window.on('blur', () => !window.isDevToolsOpened() && window.hide()); - const options = { - name: 'mullvad backend', - }; - sudo.exec(pathToBackend, options, (err) => { - if (err && err.signal !== 'SIGINT') { - log.info('Backend exited with error', err); - } else { - log.info('Backend exited'); - } - }); + return tray; + } }; -if (isDevelopment) startBackend(); + +appDelegate.setup(); diff --git a/flow-libs/electron.js.flow b/flow-libs/electron.js.flow index e1157020d5..98358003aa 100644 --- a/flow-libs/electron.js.flow +++ b/flow-libs/electron.js.flow @@ -1,7 +1,6 @@ -/** - * Flow annotations for Electron - * @flow - */ +// @flow + +import EventEmitter from 'events'; declare module 'electron' { @@ -19,6 +18,38 @@ declare module 'electron' { y: number; } + // http://electron.atom.io/docs/api/app + + declare class App extends EventEmitter { + getPath(name: string): string; + setPath(name: string, path: string): void; + quit(): void; + } + + // http://electron.atom.io/docs/api/shell + declare type OpenExternalOptions = { + activate: boolean; + } + + declare class Shell { + openExternal(url: string, options?: OpenExternalOptions, callback: (error: Error) => void): boolean; + } + + // http://electron.atom.io/docs/api/remote + + declare class Remote { + app: App; + getCurrentWindow(): BrowserWindow; + getCurrentWebContents(): WebContents; + getGlobal(name: string): ?mixed; + } + + // http://electron.atom.io/docs/api/clipboard + + declare class Clipboard { + writeText(text: string, type?: string): void; + } + // http://electron.atom.io/docs/api/native-image declare class NativeImage { @@ -26,26 +57,123 @@ declare module 'electron' { getSize(): Size; } - declare var nativeImage: { - createEmpty(): NativeImage, - createFromPath(path: string): NativeImage, - createFromBuffer(buffer: Buffer, scaleFactor?: number): NativeImage, - createFromDataURL(dataURL: string): NativeImage, - } - // http://electron.atom.io/docs/api/tray - declare type TrayEvent = 'click' | 'double-click' - declare class Tray { + declare class Tray extends EventEmitter { constructor(image: NativeImage | string): void; getBounds(): Rectangle; setHighlightMode(mode: 'selection' | 'always' | 'never'): void; setImage(image: NativeImage | string): void; setPressedImage(image: NativeImage | string): void; + } + + // http://electron.atom.io/docs/api/web-frame + + declare class WebFrame extends EventEmitter { + setZoomLevelLimits(minimumLevel: number, maximumLevel: number): void; + } + + // http://electron.atom.io/docs/api/ipc-renderer + + declare class IpcRenderer extends EventEmitter { + send(channel: string, ...args: Array<mixed>): void; + } + + // http://electron.atom.io/docs/api/ipc-main - on(event: TrayEvent, listener: Function): this; - once(event: TrayEvent, listener: Function): this; - removeEventListener(event: TrayEvent, listener: Function): this; + declare class IpcMain extends EventEmitter {} + + declare class WebContents extends EventEmitter {} + + // http://electron.atom.io/docs/api/browser-window + + declare type OpenDevToolsOptions = { + mode: 'right' | 'bottom' | 'undocked' | 'detach'; + } + + declare type WebPreferences = { + backgroundThrottling?: boolean; + scrollBounce?: boolean; + blinkFeatures?: string; + disableBlinkFeatures?: string; + } + + declare type BrowserWindowConstructorOptions = { + width?: number; + height?: number; + resizable?: boolean; + maximizable?: boolean; + fullscreenable?: boolean; + show?: boolean; + frame?: boolean; + transparent?: boolean; + webPreferences?: WebPreferences; + } + + declare type LoadURLOptions = { + userAgent?: string; + } + + declare class BrowserWindow extends EventEmitter { + constructor(options: ?BrowserWindowConstructorOptions): this; + isVisible(): boolean; + show(): void; + hide(): void; + focus(): void; + setPosition(x: number, y: number, animate?: boolean): void; + getBounds(): Rectangle; + inspectElement(x: number, y: number): void; + isDevToolsOpened(): boolean;isDevToolsOpened(): boolean; + openDevTools(options?: OpenDevToolsOptions): void; + closeDevTools(): void; + loadURL(url: string, options?: LoadURLOptions): void; + webContents: WebContents; + } + + // http://electron.atom.io/docs/api/menu-item + declare class MenuItem {} + + // http://electron.atom.io/docs/api/menu + + declare type MenuItemConstructorOptions = { + type?: 'normal' | 'separator' | 'submenu' | 'checkbox' | 'radio'; + label?: string; + role?: string; + click?: (menuItem: MenuItem, browserWindow: BrowserWindow, event: Event) => void; + } + + declare type PopupOptions = { + x?: number; + y?: number; + } + + declare class Menu { + static buildFromTemplate(template: Array<MenuItemConstructorOptions>): Menu; + static setApplicationMenu(menu: Menu): void; + popup(browserWindow?: BrowserWindow, options?: PopupOptions): void; + } + + // http://electron.atom.io/docs/api/app + + declare class App extends EventEmitter { + setPath(name: string, path: string): void; + getPath(name: string): string; + quit(): void; } + // MainInterface + + declare var nativeImage: { + createEmpty(): NativeImage, + createFromPath(path: string): NativeImage, + createFromBuffer(buffer: Buffer, scaleFactor?: number): NativeImage, + createFromDataURL(dataURL: string): NativeImage + }; + declare var webFrame: WebFrame; + declare var app: App; + declare var ipcRenderer: IpcRenderer; + declare var ipcMain: IpcMain; + declare var remote: Remote; + declare var shell: Shell; + declare var clipboard: Clipboard; }
\ No newline at end of file diff --git a/flow-libs/history.js.flow b/flow-libs/history.js.flow new file mode 100644 index 0000000000..69c25e2011 --- /dev/null +++ b/flow-libs/history.js.flow @@ -0,0 +1,24 @@ +// @flow + +declare module 'history' { + + declare class History { + length: number; + action: string; + location: string; + index: number; + entries: Array<string>; + createHref(location: string): string; + push(path: string): void; + replace(path: string): void; + go(index: number): void; + goBack(): void; + goForward(): void; + canGo(index: number): boolean; + block(prompt?: boolean): void; + listen(listener: (location: string, action: string) => void): void; + } + + declare function createMemoryHistory(): History; + +}
\ No newline at end of file diff --git a/flow-libs/nseventmonitor.js.flow b/flow-libs/nseventmonitor.js.flow new file mode 100644 index 0000000000..33b0c06966 --- /dev/null +++ b/flow-libs/nseventmonitor.js.flow @@ -0,0 +1,42 @@ +declare module 'nseventmonitor' { + + declare export class NSEventMonitor { + start(eventMask: number, handler: () => void): void; + stop(): void; + } + + declare export var NSEventMask: { + leftMouseDown: number; + leftMouseUp: number; + rightMouseDown: number; + rightMouseUp: number; + mouseMoved: number; + leftMouseDragged: number; + rightMouseDragged: number; + mouseEntered: number; + mouseExited: number; + keyDown: number; + keyUp: number; + flagsChanged: number; + appKitDefined: number; + applicationDefined: number; + periodic: number; + cursorUpdate: number; + scrollWheel: number; + tabletPoint: number; + tabletProximity: number; + otherMouseDown: number; + otherMouseUp: number; + otherMouseDragged: number; + gesture: number; + magnify: number; + swipe: number; + rotate: number; + beginGesture: number; + endGesture: number; + smartMagnify: number; + maskPressure: number; + directTouch: number; + any: number; + } +}
\ No newline at end of file diff --git a/flow-typed/npm/electron-log_vx.x.x.js b/flow-typed/npm/electron-log_vx.x.x.js index 021fae4cdf..ea47a182aa 100644 --- a/flow-typed/npm/electron-log_vx.x.x.js +++ b/flow-typed/npm/electron-log_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: 8f24be350d06d53ea4029a4a47f3c9ed -// flow-typed version: <<STUB>>/electron-log_v^2.2.0/flow_v0.46.0 +// flow-typed signature: a2b1f23f2fe8286d15c43e28d1f28578 +// flow-typed version: <<STUB>>/electron-log_v2.2.6/flow_v0.48.0 /** * This is an autogenerated libdef stub for: diff --git a/flow-typed/npm/sudo-prompt_vx.x.x.js b/flow-typed/npm/sudo-prompt_vx.x.x.js index 78a3eeda38..3c762d1dca 100644 --- a/flow-typed/npm/sudo-prompt_vx.x.x.js +++ b/flow-typed/npm/sudo-prompt_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: 9f8752969e81b3c77889d339ee49deb6 -// flow-typed version: <<STUB>>/sudo-prompt_v^7.0.0/flow_v0.46.0 +// flow-typed signature: ff03b3d5b4912b377eef1ed260d6968f +// flow-typed version: <<STUB>>/sudo-prompt_v7.0.0/flow_v0.48.0 /** * This is an autogenerated libdef stub for: |
