diff --git a/README.md b/README.md index d4cfbf476..33edbdd90 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,59 @@ yarn package yarn start ``` -> Troubleshooting: If it hangs on a white screen in Electron even though it has compiled and has been syncing for a long time, then simply choose 'View > Reload' (CMD + R on macOS) from the Electron menu +> Troubleshooting: If it hangs on a white screen in Electron even though it has compiled and has been syncing for a long time, then simply choose 'View > Reload' (CMD + R on macOS) from the Fether/Electron menu. If the Fether menu is not shown in the tray, then by clicking the Fether window and then holding down the ALT key to reveal it. + +> Developer Tools: Open developer tools automatically by running `DEBUG=true yarn start` when not in the production environment + +# Run without taskbar mode (no tray icon) + +```bash +TASKBAR=false yarn start +``` + +# Usage of taskbar mode + +### macOS + +Taskbar mode is `true` by default. + +* Enabled `true` + * Fether window may be toggled open/closed by clicking the Fether tray icon, but not the Fether dock icon + * Fether window does not have a frame (i.e. no close/minimise icons) +* Disabled `false` + * Fether window may be toggled open by clicking the Fether dock icon + * Fether window has a frame (with close/minimise icons) +* Always + * Fether menu shown in the tray by default + * Fether window position is saved upon move, minimising, and close so it is restored in the same position. + +### Linux + +Taskbar mode is `true` by default. + +* Enabled `true` + * Fether window may be toggled minimise/restore by clicking the Fether tray icon to reveal a tooltip that says "Click to toggle Fether window" and then clicking the tooltip. + * Fether window may not have a frame (i.e. no close/minimise icons) if `frame: false` in packages/fether-electron/src/main/app/options/config/index.js +* Disabled `false` + * Fether window may be toggled open by clicking the Fether dock icon + * Fether window has a frame (with close/minimise icons) +* Always + * Fether menu may not be not shown in the tray by default depending on whether `setMenuBarVisibility` has been set. Show the Fether menu in the tray by clicking the Fether window and then holding down the ALT key to reveal it. + * Fether menu may be configured to automatically hide by setting `setAutoHideMenuBar` + * Fether window position is saved upon move, minimising, and close so it is restored in the same position. + +### Windows + +Taskbar mode is always `false` since the Fether menu does not appear without a frame on the Fether window. + +* Disabled `false` + * Fether window may be toggled open/minimise by clicking the Fether dock icon + * Fether window has a frame (with close/minimise icons). +* Always + * Fether menu is shown in the Fether window by clicking the Fether window and then holding down the ALT key to reveal it. + * Fether menu may be configured to automatically hide by setting `setAutoHideMenuBar` + * Fether tray icon does nothing + * Fether window position is saved upon move, minimising, and close so it is restored in the same position. ## Join the chat! diff --git a/packages/fether-electron/babel.config.js b/packages/fether-electron/babel.config.js new file mode 100644 index 000000000..7238a1fe2 --- /dev/null +++ b/packages/fether-electron/babel.config.js @@ -0,0 +1,4 @@ +module.exports = { + plugins: [['@babel/plugin-proposal-class-properties', { loose: false }]], + presets: ['@babel/preset-env'] +}; diff --git a/packages/fether-electron/build/icons/icon.ico b/packages/fether-electron/build/icons/icon.ico new file mode 100644 index 000000000..4f8aa555b Binary files /dev/null and b/packages/fether-electron/build/icons/icon.ico differ diff --git a/packages/fether-electron/electron-builder.json b/packages/fether-electron/electron-builder.json index cfad34938..02dcdff79 100644 --- a/packages/fether-electron/electron-builder.json +++ b/packages/fether-electron/electron-builder.json @@ -1,5 +1,6 @@ { "appId": "io.parity.fether", + "copyright": "Copyright 2015-2018 Parity Technologies (UK) Ltd.", "linux": { "category": "Utility", "target": ["AppImage", "snap", "deb", "tar.xz"] @@ -10,6 +11,7 @@ "productName": "Parity Fether", "publish": "github", "win": { + "icon": "icons/icon.ico", "publisherName": "Parity Technologies (UK) Ltd.", "target": "nsis" } diff --git a/packages/fether-electron/package.json b/packages/fether-electron/package.json index 582a59853..ed8e6abc5 100644 --- a/packages/fether-electron/package.json +++ b/packages/fether-electron/package.json @@ -36,24 +36,26 @@ "prerelease": "./scripts/revertElectronBug.sh", "release": "electron-builder", "start": "cross-env ELECTRON_START_URL=http://localhost:3000 electron-webpack dev --ws-origins all", - "test": "echo Skipped." + "test": "jest --all --color --coverage" }, "dependencies": { "@parity/electron": "^4.0.0", "ansi-styles": "^3.2.1", "commander": "^2.15.1", "commander-remaining-args": "^1.2.0", + "electron-positioner": "^4.1.0", + "electron-settings": "^3.2.0", "fether-react": "^0.3.0", "pino": "^4.16.1", "pino-multi-stream": "^3.1.2", - "source-map-support": "^0.5.6" + "source-map-support": "^0.5.10" }, "devDependencies": { - "copyfiles": "^2.0.0", + "copyfiles": "^2.1.0", "cross-env": "^5.2.0", "electron": "^4.0.1", - "electron-builder": "^20.29.0", - "electron-webpack": "^2.1.2", - "webpack": "^4.7.0" + "electron-builder": "^20.38.5", + "electron-webpack": "^2.6.1", + "webpack": "^4.29.1" } } \ No newline at end of file diff --git a/packages/fether-electron/src/main/cli/index.js b/packages/fether-electron/src/main/app/cli/index.js similarity index 92% rename from packages/fether-electron/src/main/cli/index.js rename to packages/fether-electron/src/main/app/cli/index.js index 16d365973..3c17141d4 100644 --- a/packages/fether-electron/src/main/cli/index.js +++ b/packages/fether-electron/src/main/app/cli/index.js @@ -5,8 +5,8 @@ import cli from 'commander'; -const { productName } = require('../../../electron-builder.json'); -const { version } = require('../../../package.json'); +const { productName } = require('../../../../electron-builder.json'); +const { version } = require('../../../../package.json'); /** * Process.argv arguments length is different in electron mode and in packaged diff --git a/packages/fether-electron/src/main/app/index.js b/packages/fether-electron/src/main/app/index.js new file mode 100644 index 000000000..5019f8426 --- /dev/null +++ b/packages/fether-electron/src/main/app/index.js @@ -0,0 +1,114 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +import EventEmitter from 'events'; + +import { + calculateWinPosition, + createPositioner, + createTray, + createWindow, + fixWinPosition, + getScreenResolution, + hideWindow, + loadTray, + moveWindowUp, + onTrayClick, + onWindowClose, + processSaveWinPosition, + setupAppListeners, + setupDebug, + setupGlobals, + setupLogger, + setupMenu, + setupParityEthereum, + setupRequestListeners, + setupSecurity, + setupWinListeners, + setupWin32Listeners, + showTrayBalloon, + showWindow, + updateProgress, + windowClear +} from './methods'; + +let hasCalledInitFetherApp = false; + +class FetherApp extends EventEmitter { + constructor (electronApp, options) { + super(); + + if (hasCalledInitFetherApp) { + this.emit( + 'error', + new Error('Unable to initialise Fether app more than once') + ); + } + + /** + * After the Fether instance and fetherApp.win has been created. + * If the user then chooses from the Fether Menu "Window > Close" + * it runs windowClear, which deletes fetherApp.win and associated + * listeners since the 'close' event also occurs when the user exits. + * If the user then clicks the tray icon to re-open the Fether window, + * it will run the onTrayClick method, which calls fetherApp.showWindow + * and if fetherApp.win does not exist, it runs showWindow and createWindow + * to restore create fetherApp.win again and associated listeners. However we + * do not need to run all the fetherApp methods again like we did on the + * when fetherApp.win was first created (i.e. createTray, loadTray, + * setupDebug, setupSecurity, setupLogger, setupParityEthereum, setupGlobals) + */ + this.app = electronApp; + this.options = options; + + this.createWindow(); + this.updateProgress(0.4, undefined); + + // These methods are called only once when Fether instance is created + // (i.e. not called again when the Fether window closed and re-opened) + this.createTray(); + this.loadTray(); + this.setupDebug(); + this.setupSecurity(); + this.setupLogger(); + this.setupParityEthereum(); + this.setupGlobals(); + + this.updateProgress(0.8, undefined); + this.showWindow(undefined); + this.updateProgress(1.0, undefined); + this.updateProgress(-1, 'after-create-app'); + } + + calculateWinPosition = () => calculateWinPosition(this); + createPositioner = () => createPositioner(this); + createTray = () => createTray(this); + createWindow = () => createWindow(this); + fixWinPosition = positionStruct => fixWinPosition(this, positionStruct); + getScreenResolution = () => getScreenResolution(); + hideWindow = () => hideWindow(this); + loadTray = () => loadTray(this); + moveWindowUp = () => moveWindowUp(this); + onTrayClick = (e, bounds) => onTrayClick(this, e, bounds); + onWindowClose = () => onWindowClose(this); + processSaveWinPosition = () => processSaveWinPosition(this); + setupAppListeners = () => setupAppListeners(this); + setupDebug = () => setupDebug(this); + setupGlobals = () => setupGlobals(); + setupLogger = () => setupLogger(); + setupMenu = () => setupMenu(this); + setupParityEthereum = () => setupParityEthereum(this); + setupRequestListeners = () => setupRequestListeners(this); + setupSecurity = () => setupSecurity(this); + setupWinListeners = () => setupWinListeners(this); + setupWin32Listeners = () => setupWin32Listeners(this); + showTrayBalloon = () => showTrayBalloon(this); + showWindow = trayPos => showWindow(this, trayPos); + updateProgress = (percentage, eventListenerName) => + updateProgress(this, percentage, eventListenerName); + windowClear = () => windowClear(this); +} + +export default FetherApp; diff --git a/packages/fether-electron/src/main/app/menu/index.js b/packages/fether-electron/src/main/app/menu/index.js new file mode 100644 index 000000000..b1ed5239b --- /dev/null +++ b/packages/fether-electron/src/main/app/menu/index.js @@ -0,0 +1,21 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +import electron from 'electron'; +import { template } from './template'; + +const { Menu } = electron; + +const menu = Menu.buildFromTemplate(template); + +const getMenu = () => { + return Menu.getApplicationMenu(); +}; + +const addMenu = () => { + Menu.setApplicationMenu(menu); +}; + +export { addMenu, getMenu }; diff --git a/packages/fether-electron/src/main/app/menu/template/index.js b/packages/fether-electron/src/main/app/menu/template/index.js new file mode 100644 index 000000000..7018731c9 --- /dev/null +++ b/packages/fether-electron/src/main/app/menu/template/index.js @@ -0,0 +1,87 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +import electron from 'electron'; + +const { app, shell } = electron; + +// Create the Application's main menu +// https://github.com/electron/electron/blob/master/docs/api/menu.md#examples +export const template = [ + { + label: 'Edit', + submenu: [ + { role: 'cut' }, + { role: 'copy' }, + { role: 'paste' }, + { role: 'delete' }, + { role: 'selectall' } + ] + }, + { + label: 'View', + submenu: [ + { role: 'reload' }, + { role: 'forcereload' }, + { role: 'toggledevtools' }, + { type: 'separator' }, + { role: 'resetzoom' }, + { role: 'zoomin' }, + { role: 'zoomout' }, + { type: 'separator' }, + { role: 'togglefullscreen' } + ] + }, + { + role: 'window', + submenu: [{ role: 'minimize' }, { role: 'close' }] + }, + { + role: 'help', + submenu: [ + { + label: 'Learn More', + click () { + shell.openExternal('https://parity.io'); + } + } + ] + } +]; + +if (process.platform === 'darwin') { + template.unshift({ + label: app.getName(), + submenu: [ + { role: 'about' }, + { type: 'separator' }, + { role: 'services', submenu: [] }, + { type: 'separator' }, + { role: 'hide' }, + { role: 'hideothers' }, + { role: 'unhide' }, + { type: 'separator' }, + { role: 'quit' } + ] + }); + + // Edit menu + template[1].submenu.push( + { type: 'separator' }, + { + label: 'Speech', + submenu: [{ role: 'startspeaking' }, { role: 'stopspeaking' }] + } + ); + + // Window menu + template[3].submenu = [ + { role: 'close' }, + { role: 'minimize' }, + { role: 'zoom' }, + { type: 'separator' }, + { role: 'front' } + ]; +} diff --git a/packages/fether-electron/src/main/messages/index.js b/packages/fether-electron/src/main/app/messages/index.js similarity index 67% rename from packages/fether-electron/src/main/messages/index.js rename to packages/fether-electron/src/main/app/messages/index.js index a2a1125cf..b671ebf97 100644 --- a/packages/fether-electron/src/main/messages/index.js +++ b/packages/fether-electron/src/main/app/messages/index.js @@ -12,16 +12,23 @@ const pino = Pino(); /** * Handle all asynchronous messages from renderer to main. */ -export default async (mainWindow, event, action, ...args) => { +export default async (fetherAppWindow, event, action, ...args) => { try { if (!action) { return; } switch (action) { case 'app-resize': { - const [width] = mainWindow.getContentSize(); + const [width] = fetherAppWindow.getContentSize(); const newHeight = args[0]; - mainWindow.setContentSize(width, Math.round(newHeight) + 2); + const feedbackButtonHeight = 20; + const resizeHeight = newHeight + 2; + const height = + process.platform === 'win32' && fetherAppWindow.isMenuBarVisible() + ? resizeHeight + feedbackButtonHeight + : resizeHeight; + + fetherAppWindow.setContentSize(width, height); break; } case 'check-clock-sync': { diff --git a/packages/fether-electron/src/main/app/methods/calculateWinPosition.js b/packages/fether-electron/src/main/app/methods/calculateWinPosition.js new file mode 100644 index 000000000..88571a8dc --- /dev/null +++ b/packages/fether-electron/src/main/app/methods/calculateWinPosition.js @@ -0,0 +1,43 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +function calculateWinPosition (fetherApp, trayPos) { + const { cachedBounds, options, positioner, tray } = fetherApp; + + if (trayPos && trayPos.x !== 0) { + // Cache the bounds + fetherApp.cachedBounds = trayPos; + } else if (cachedBounds) { + // Cached value will be used if showWindow is called without bounds data + trayPos = cachedBounds; + } else if (tray && tray.getBounds) { + // Get the current tray bounds + trayPos = tray.getBounds(); + } + + // Default the window to the right if `trayPos` bounds are undefined or null. + let noBoundsPosition = null; + + if ( + (trayPos === undefined || (trayPos && trayPos.x === 0)) && + options.windowPosition && + options.windowPosition.substr(0, 4) === 'tray' + ) { + noBoundsPosition = + process.platform === 'win32' ? 'bottomRight' : 'topRight'; + } + + const position = positioner.calculate( + noBoundsPosition || options.windowPosition, + trayPos + ); + + return { + x: options.x ? options.x : position.x, + y: options.y ? options.y : position.y + }; +} + +export default calculateWinPosition; diff --git a/packages/fether-electron/src/main/app/methods/createPositioner.js b/packages/fether-electron/src/main/app/methods/createPositioner.js new file mode 100644 index 000000000..327152bdf --- /dev/null +++ b/packages/fether-electron/src/main/app/methods/createPositioner.js @@ -0,0 +1,12 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +import Positioner from 'electron-positioner'; + +function createPositioner (fetherApp) { + fetherApp.positioner = new Positioner(fetherApp.win); +} + +export default createPositioner; diff --git a/packages/fether-electron/src/main/app/methods/createTray.js b/packages/fether-electron/src/main/app/methods/createTray.js new file mode 100644 index 000000000..015f66fa5 --- /dev/null +++ b/packages/fether-electron/src/main/app/methods/createTray.js @@ -0,0 +1,23 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +import { Tray } from 'electron'; + +function createTray (fetherApp) { + const { + app: { dock }, + options + } = fetherApp; + + if (options.withTaskbar) { + fetherApp.tray = new Tray(options.icon); + + if (process.platform === 'darwin' && dock) { + dock.setIcon(options.iconDock); + } + } +} + +export default createTray; diff --git a/packages/fether-electron/src/main/app/methods/createWindow.js b/packages/fether-electron/src/main/app/methods/createWindow.js new file mode 100644 index 000000000..4b1a97445 --- /dev/null +++ b/packages/fether-electron/src/main/app/methods/createWindow.js @@ -0,0 +1,43 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +import electron from 'electron'; + +const { BrowserWindow } = electron; + +function createWindow (fetherApp) { + const { + createPositioner, + options, + setupAppListeners, + setupMenu, + setupRequestListeners + } = fetherApp; + + fetherApp.emit('create-app'); + + setupAppListeners(fetherApp); + + fetherApp.emit('create-window'); + + fetherApp.win = new BrowserWindow(options); + + if (options.showOnAllWorkspaces !== false) { + fetherApp.win.setVisibleOnAllWorkspaces(true); + } + + setupMenu(fetherApp); + + // Opens file:///path/to/build/index.html in prod mode, or whatever is + // passed to ELECTRON_START_URL + fetherApp.win.loadURL(options.index); + + createPositioner(fetherApp); + setupRequestListeners(fetherApp); + + fetherApp.emit('after-create-window'); +} + +export default createWindow; diff --git a/packages/fether-electron/src/main/app/methods/fixWinPosition.js b/packages/fether-electron/src/main/app/methods/fixWinPosition.js new file mode 100644 index 000000000..37a6086fd --- /dev/null +++ b/packages/fether-electron/src/main/app/methods/fixWinPosition.js @@ -0,0 +1,45 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +/** + * Proposes a fixed window position that may be used if the window is moved + * out of the screen bounds threshold. If the window is moved across to a + * second monitor (without screen mirroring) then if the screen is hidden + * and then shown again (by pressing the Fether tray icon), since the + * coordinates of the window are outside the screen bounds the window + * will be restored into the users primary screen. + */ +function fixWinPosition (fetherApp, proposedWindowPosition) { + const { trayDepth, win } = fetherApp; + + if (!proposedWindowPosition) { + return; + } + + const newPosition = { x: undefined, y: undefined }; + const resolution = fetherApp.getScreenResolution(); + const winWidth = win.getSize()[0]; + const winHeight = win.getSize()[1]; + + if (proposedWindowPosition.x < trayDepth) { + newPosition.x = trayDepth; + } + + if (proposedWindowPosition.y < trayDepth) { + newPosition.y = trayDepth; + } + + if (proposedWindowPosition.x >= resolution.x - winWidth - trayDepth) { + newPosition.x = resolution.x - winWidth - trayDepth; + } + + if (proposedWindowPosition.y >= resolution.y - winHeight - trayDepth) { + newPosition.y = resolution.y - winHeight - trayDepth; + } + + return newPosition; +} + +export default fixWinPosition; diff --git a/packages/fether-electron/src/main/app/methods/getScreenResolution.js b/packages/fether-electron/src/main/app/methods/getScreenResolution.js new file mode 100644 index 000000000..06f74f6ed --- /dev/null +++ b/packages/fether-electron/src/main/app/methods/getScreenResolution.js @@ -0,0 +1,16 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +import { screen } from 'electron'; + +// https://ourcodeworld.com/articles/read/285/how-to-get-the-screen-width-and-height-in-electron-framework +function getScreenResolution () { + const mainScreen = screen.getPrimaryDisplay(); + const mainScreenDims = mainScreen.size; + + return { x: mainScreenDims.width, y: mainScreenDims.height }; +} + +export default getScreenResolution; diff --git a/packages/fether-electron/src/main/app/methods/hideWindow.js b/packages/fether-electron/src/main/app/methods/hideWindow.js new file mode 100644 index 000000000..9c8514026 --- /dev/null +++ b/packages/fether-electron/src/main/app/methods/hideWindow.js @@ -0,0 +1,19 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +function hideWindow (fetherApp) { + const { processSaveWinPosition, win } = fetherApp; + + if (!win) { + return; + } + + processSaveWinPosition(fetherApp); // Save window position when hide, particularly necessary on Linux + fetherApp.emit('hide-window'); + win.hide(); + fetherApp.emit('after-hide-window'); +} + +export default hideWindow; diff --git a/packages/fether-electron/src/main/app/methods/index.js b/packages/fether-electron/src/main/app/methods/index.js new file mode 100644 index 000000000..7f81604a6 --- /dev/null +++ b/packages/fether-electron/src/main/app/methods/index.js @@ -0,0 +1,60 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +import calculateWinPosition from './calculateWinPosition'; +import createPositioner from './createPositioner'; +import createTray from './createTray'; +import createWindow from './createWindow'; +import fixWinPosition from './fixWinPosition'; +import getScreenResolution from './getScreenResolution'; +import hideWindow from './hideWindow'; +import loadTray from './loadTray'; +import moveWindowUp from './moveWindowUp'; +import onTrayClick from './onTrayClick'; +import onWindowClose from './onWindowClose'; +import processSaveWinPosition from './processSaveWinPosition'; +import setupAppListeners from './setupAppListeners'; +import setupDebug from './setupDebug'; +import setupGlobals from './setupGlobals'; +import setupLogger from './setupLogger'; +import setupMenu from './setupMenu'; +import setupParityEthereum from './setupParityEthereum'; +import setupRequestListeners from './setupRequestListeners'; +import setupSecurity from './setupSecurity'; +import setupWinListeners from './setupWinListeners'; +import setupWin32Listeners from './setupWin32Listeners'; +import showTrayBalloon from './showTrayBalloon'; +import showWindow from './showWindow'; +import updateProgress from './updateProgress'; +import windowClear from './windowClear'; + +export { + calculateWinPosition, + createPositioner, + createTray, + createWindow, + fixWinPosition, + getScreenResolution, + hideWindow, + loadTray, + moveWindowUp, + onTrayClick, + onWindowClose, + processSaveWinPosition, + setupAppListeners, + setupDebug, + setupGlobals, + setupLogger, + setupMenu, + setupRequestListeners, + setupParityEthereum, + setupSecurity, + setupWinListeners, + setupWin32Listeners, + showTrayBalloon, + showWindow, + updateProgress, + windowClear +}; diff --git a/packages/fether-electron/src/main/app/methods/loadTray.js b/packages/fether-electron/src/main/app/methods/loadTray.js new file mode 100644 index 000000000..68bde2ec6 --- /dev/null +++ b/packages/fether-electron/src/main/app/methods/loadTray.js @@ -0,0 +1,43 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +import Pino from '../utils/pino'; + +const pino = Pino(); + +function loadTray (fetherApp) { + const { app, onTrayClick, options, showTrayBalloon, tray } = fetherApp; + + if (options.withTaskbar) { + fetherApp.emit('load-tray'); + + if (process.platform === 'darwin' && app.dock && !options.showDockIcon) { + app.dock.hide(); + } + + const defaultClickEvent = options.showOnRightClick + ? 'right-click' + : 'click'; + + // Note: See https://github.com/RocketChat/Rocket.Chat.Electron/issues/44 + if (process.platform === 'win32') { + showTrayBalloon(fetherApp); + } + + tray.on(defaultClickEvent, () => onTrayClick(fetherApp)); + tray.on('double-click', () => onTrayClick(fetherApp)); + // Right click event handler does not work on Windows as intended + tray.on('right-click', () => { + if (process.platform === 'win32') { + pino.info('Detected right click on Windows'); + showTrayBalloon(fetherApp); + } + }); + tray.setToolTip(options.tooltip); + tray.setHighlightMode('never'); + } +} + +export default loadTray; diff --git a/packages/fether-electron/src/main/app/methods/moveWindowUp.js b/packages/fether-electron/src/main/app/methods/moveWindowUp.js new file mode 100644 index 000000000..89a7ab5e5 --- /dev/null +++ b/packages/fether-electron/src/main/app/methods/moveWindowUp.js @@ -0,0 +1,40 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +import Pino from '../utils/pino'; + +const pino = Pino(); + +/** + * If the Fether window is restored on a page with a small window height + * and the window is positioned close to the bottom of the screen, then + * we do not want it to crop the bottom of the window when the user navigates + * to a page with a larger window height. So if the user navigates to a page + * with a larger window height that causes it to be cropped, then we will + * automatically move the window upward so it is viewable to the user + */ +function moveWindowUp (fetherApp) { + const { getScreenResolution, win } = fetherApp; + + if (!win) { + return; + } + + pino.info('Fether window resized. Moving it back up into view if required'); + const position = win.getPosition(); + const positionStruct = { x: position[0], y: position[1] }; + const trayDepth = fetherApp.trayDepth || 40; // Default incase resizes on load + const resolution = getScreenResolution(); + const winHeight = win.getSize()[1]; + const maxWinY = resolution.y - winHeight - trayDepth; + const adjustY = positionStruct.y - maxWinY; + + if (adjustY > 0) { + fetherApp.emit('moved-window-up-into-view'); + win.setPosition(positionStruct.x, maxWinY); + } +} + +export default moveWindowUp; diff --git a/packages/fether-electron/src/main/app/methods/onTrayClick.js b/packages/fether-electron/src/main/app/methods/onTrayClick.js new file mode 100644 index 000000000..0ede1a6ef --- /dev/null +++ b/packages/fether-electron/src/main/app/methods/onTrayClick.js @@ -0,0 +1,20 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +function onTrayClick (fetherApp, e, bounds) { + const { cachedBounds, hideWindow, win } = fetherApp; + const { altKey, ctrlKey, metaKey, shiftKey } = e; + + if ((win && win.isVisible()) || altKey || shiftKey || ctrlKey || metaKey) { + return hideWindow(fetherApp); + } + + // cachedBounds are needed for double-clicked event + const newCacheBounds = bounds || cachedBounds; + fetherApp.cachedBounds = newCacheBounds; + fetherApp.showWindow(fetherApp, newCacheBounds); +} + +export default onTrayClick; diff --git a/packages/fether-electron/src/main/app/methods/onWindowClose.js b/packages/fether-electron/src/main/app/methods/onWindowClose.js new file mode 100644 index 000000000..02b59ab8d --- /dev/null +++ b/packages/fether-electron/src/main/app/methods/onWindowClose.js @@ -0,0 +1,13 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +function onWindowClose (fetherApp) { + const { processSaveWinPosition, windowClear } = fetherApp; + + processSaveWinPosition(fetherApp); + windowClear(fetherApp); +} + +export default onWindowClose; diff --git a/packages/fether-electron/src/main/app/methods/processSaveWinPosition.js b/packages/fether-electron/src/main/app/methods/processSaveWinPosition.js new file mode 100644 index 000000000..3af5662fe --- /dev/null +++ b/packages/fether-electron/src/main/app/methods/processSaveWinPosition.js @@ -0,0 +1,67 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +import { + getChangedScreenResolution, + shouldFixWindowPosition +} from '../utils/window'; +import { saveWindowPosition } from '../settings'; + +function processSaveWinPosition (fetherApp) { + const { + fixWinPosition, + getScreenResolution, + previousScreenResolution, + win + } = fetherApp; + + if (!win) { + return; + } + + const resolution = getScreenResolution(); + + fetherApp.previousScreenResolution = getChangedScreenResolution( + previousScreenResolution, + resolution + ); + + // Get the latest position. The window may have been moved to a different + // screen with smaller resolution. We must move it to prevent cropping. + const position = win.getPosition(); + + const positionStruct = { x: position[0], y: position[1] }; + + const fixedWinPosition = fixWinPosition(fetherApp, positionStruct); + + const newFixedPosition = { + x: fixedWinPosition.x || positionStruct.x, + y: fixedWinPosition.y || positionStruct.y + }; + + /** + * Only move it immediately back into the threshold of screen tray bounds + * to prevent it from being cropped if the screen resolution is reduced. + * Do not call this all the time otherwise it will crash with + * a call stack exceeded if the user keeps trying to move it outside the screen bounds + * and it would also prevent the user from moving it to a different screen at all. + */ + if (shouldFixWindowPosition(previousScreenResolution, resolution)) { + // Move window to the fixed x-coordinate position if that required fixing + if (fixedWinPosition.x) { + win.setPosition(fixedWinPosition.x, positionStruct.y, true); + } + + // Move window to the fixed y-coordinate position if that required fixing + if (fixedWinPosition.y) { + win.setPosition(positionStruct.x, fixedWinPosition.y, true); + } + } + + saveWindowPosition(newFixedPosition || positionStruct); + fetherApp.emit('after-moved-window-position-saved'); +} + +export default processSaveWinPosition; diff --git a/packages/fether-electron/src/main/app/methods/setupAppListeners.js b/packages/fether-electron/src/main/app/methods/setupAppListeners.js new file mode 100644 index 000000000..cc67bfed7 --- /dev/null +++ b/packages/fether-electron/src/main/app/methods/setupAppListeners.js @@ -0,0 +1,76 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +import { productName } from '../../../../electron-builder.json'; +import { getSavedWindowPosition } from '../settings'; +import Pino from '../utils/pino'; + +const pino = Pino(); + +function setupAppListeners (fetherApp) { + fetherApp.on('create-app', () => { + pino.info( + `Starting ${productName} (${ + fetherApp.options.withTaskbar ? 'with' : 'without' + } tray icon)...` + ); + }); + + fetherApp.on('create-window', () => { + pino.info('Creating window'); + }); + + fetherApp.on('after-create-window', () => { + pino.info('Finished creating window'); + }); + + fetherApp.on('load-tray', () => { + pino.info('Configuring taskbar mode for the window'); + }); + + fetherApp.on('show-window', () => { + pino.info('Showing window'); + }); + + fetherApp.on('after-show-window', () => { + pino.info('Finished showing window'); + }); + + fetherApp.on('after-create-app', () => { + pino.info(`Ready to use ${productName}`); + }); + + fetherApp.on('hide-window', () => { + pino.info('Hiding window on blur since not on top'); + }); + + fetherApp.on('after-hide-window', () => { + pino.info('Finished hiding window'); + }); + + fetherApp.on('blur-window', () => { + pino.info('Blur window since lost focus when on top'); + }); + + fetherApp.on('after-moved-window-position-saved', () => { + const pos = getSavedWindowPosition(); + + pino.info(`Saved window position after move (x: ${pos.x}, y: ${pos.y})`); + }); + + fetherApp.on('moved-window-up-into-view', () => { + pino.info('Moved window up into view'); + }); + + fetherApp.on('after-close-window', () => { + pino.info('Deleted window upon close'); + }); + + fetherApp.on('error', error => { + console.error(error); + }); +} + +export default setupAppListeners; diff --git a/packages/fether-electron/src/main/app/methods/setupDebug.js b/packages/fether-electron/src/main/app/methods/setupDebug.js new file mode 100644 index 000000000..a7577278e --- /dev/null +++ b/packages/fether-electron/src/main/app/methods/setupDebug.js @@ -0,0 +1,16 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +const withDebug = process.env.DEBUG === 'true'; + +function setupDebug (fetherApp) { + const { options, win } = fetherApp; + // Enable with `DEBUG=true yarn start` and access Developer Tools + if (withDebug && options.webPreferences.devTools) { + win.webContents.openDevTools(); + } +} + +export default setupDebug; diff --git a/packages/fether-electron/src/main/app/methods/setupGlobals.js b/packages/fether-electron/src/main/app/methods/setupGlobals.js new file mode 100644 index 000000000..90f57d213 --- /dev/null +++ b/packages/fether-electron/src/main/app/methods/setupGlobals.js @@ -0,0 +1,14 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +import cli from '../cli'; + +function setupGlobals () { + // Globals for fether-react parityStore + global.wsInterface = cli.wsInterface; + global.wsPort = cli.wsPort; +} + +export default setupGlobals; diff --git a/packages/fether-electron/src/main/app/methods/setupLogger.js b/packages/fether-electron/src/main/app/methods/setupLogger.js new file mode 100644 index 000000000..cebc95fa4 --- /dev/null +++ b/packages/fether-electron/src/main/app/methods/setupLogger.js @@ -0,0 +1,17 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +import parityElectron from '@parity/electron'; + +import Pino from '../utils/pino'; + +function setupLogger () { + // Set options for @parity/electron + parityElectron({ + logger: namespace => log => Pino({ name: namespace }).info(log) + }); +} + +export default setupLogger; diff --git a/packages/fether-electron/src/main/app/methods/setupMenu.js b/packages/fether-electron/src/main/app/methods/setupMenu.js new file mode 100644 index 000000000..f465e8131 --- /dev/null +++ b/packages/fether-electron/src/main/app/methods/setupMenu.js @@ -0,0 +1,26 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +import { addMenu } from '../menu'; +import Pino from '../utils/pino'; + +const pino = Pino(); + +function setupMenu (fetherApp) { + // Add Fether menu + addMenu(); + + /** + * Toggle the Fether menu bar in the frame. + * If not shown by default then when shown it may crop the bottom + * of the window when menu open/close toggled on Windows. + */ + fetherApp.win.setAutoHideMenuBar(false); // Pressing ALT shows menu bar + fetherApp.win.setMenuBarVisibility(true); + + pino.info('Finished configuring Electron menu'); +} + +export default setupMenu; diff --git a/packages/fether-electron/src/main/app/methods/setupParityEthereum.js b/packages/fether-electron/src/main/app/methods/setupParityEthereum.js new file mode 100644 index 000000000..8a6f9ca25 --- /dev/null +++ b/packages/fether-electron/src/main/app/methods/setupParityEthereum.js @@ -0,0 +1,13 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +import ParityEthereum from '../parityEthereum'; + +function setupParityEthereum (fetherApp) { + // Download, install, and run Parity Ethereum if not running and requested + return new ParityEthereum(fetherApp.win); +} + +export default setupParityEthereum; diff --git a/packages/fether-electron/src/main/app/methods/setupRequestListeners.js b/packages/fether-electron/src/main/app/methods/setupRequestListeners.js new file mode 100644 index 000000000..6fdb84f68 --- /dev/null +++ b/packages/fether-electron/src/main/app/methods/setupRequestListeners.js @@ -0,0 +1,35 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +import electron from 'electron'; + +import messages from '../messages'; +const { ipcMain, session } = electron; + +function setupRequestListeners (fetherApp) { + // Listen to messages from renderer process + ipcMain.on('asynchronous-message', (...args) => { + return messages(fetherApp.win, ...args); + }); + + // WS calls have Origin `file://` by default, which is not trusted. + // We override Origin header on all WS connections with an authorized one. + session.defaultSession.webRequest.onBeforeSendHeaders( + { + urls: ['ws://*/*', 'wss://*/*'] + }, + (details, callback) => { + if (!fetherApp.win || !fetherApp.win.id) { + // There might be a split second where the user closes the app, so + // this.fether.window is null, but there is still a network request done. + return; + } + details.requestHeaders.Origin = `parity://${fetherApp.win.id}.ui.parity`; + callback({ requestHeaders: details.requestHeaders }); // eslint-disable-line + } + ); +} + +export default setupRequestListeners; diff --git a/packages/fether-electron/src/main/app/methods/setupSecurity.js b/packages/fether-electron/src/main/app/methods/setupSecurity.js new file mode 100644 index 000000000..9a170b0d6 --- /dev/null +++ b/packages/fether-electron/src/main/app/methods/setupSecurity.js @@ -0,0 +1,11 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +function setupSecurity (fetherApp) { + // Security to prevent window contents from being captured by other apps + fetherApp.win.setContentProtection(true); +} + +export default setupSecurity; diff --git a/packages/fether-electron/src/main/app/methods/setupWin32Listeners.js b/packages/fether-electron/src/main/app/methods/setupWin32Listeners.js new file mode 100644 index 000000000..1f30a8d73 --- /dev/null +++ b/packages/fether-electron/src/main/app/methods/setupWin32Listeners.js @@ -0,0 +1,95 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +import Pino from '../utils/pino'; + +const pino = Pino(); + +function setupWin32Listeners (fetherApp) { + const { + moveWindowUp, + onWindowClose, + processSaveWinPosition, + showTrayBalloon, + win + } = fetherApp; + + if (process.platform === 'win32') { + /** + * Hook WM_SYSKEYUP + * + * Open the Fether Electron menu when the Fether window is active + * and the user enters a keyboard ALT key or both ALT and another key together. + * Reference: https://docs.microsoft.com/en-gb/windows/desktop/inputdev/wm-syskeyup + */ + win.hookWindowMessage(Number.parseInt('0x0105'), (wParam, lParam) => { + /** + * Detect when user presses ALT+keyCode. + * i.e. Use `wParam && wParam.readUInt32LE(0) === 77` to detect ALT+m. + * Reference: https://nodejs.org/api/buffer.html + */ + if (wParam) { + pino.info('Detected ALT key pressed to toggle the Fether menu'); + // showTrayBalloon(fetherApp); + } + }); + + /** + * Hook WM_SYSCOMMAND + * + * Detect events on Windows + * Credit: http://robmayhew.com/listening-for-events-from-windows-in-electron-tutorial/ + */ + win.hookWindowMessage(Number.parseInt('0x0112'), (wParam, lParam) => { + let eventName = null; + + if (wParam.readUInt32LE(0) === 0xf060) { + // SC_CLOSE + eventName = 'close'; + onWindowClose(fetherApp); + } else if (wParam.readUInt32LE(0) === 0xf030) { + // SC_MAXIMIZE + eventName = 'maximize'; + showTrayBalloon(fetherApp); + } else if (wParam.readUInt32LE(0) === 0xf020) { + // SC_MINIMIZE + eventName = 'minimize'; + processSaveWinPosition(fetherApp); + } else if (wParam.readUInt32LE(0) === 0xf120) { + // SC_RESTORE + eventName = 'restored'; + showTrayBalloon(fetherApp); + } + + if (eventName !== null) { + pino.info('Detected event:', eventName); + } + }); + + /** + * Hook WM_EXITSIZEMOVE + * + * Detect event on Windows when Fether window was moved or resized + */ + win.hookWindowMessage(Number.parseInt('0x0232'), (wParam, lParam) => { + pino.info('Detected completion of move or resize event'); + + // Move Fether window back up into view if it was a resize event + // that causes the bottom to be cropped + moveWindowUp(fetherApp); + + // Try again after a delay incase Fether window resize occurs + // x seconds after navigating to a new page. + setTimeout(() => { + moveWindowUp(fetherApp); + }, 5000); + + // Save Fether window position to Electron settings + processSaveWinPosition(fetherApp); + }); + } +} + +export default setupWin32Listeners; diff --git a/packages/fether-electron/src/main/app/methods/setupWinListeners.js b/packages/fether-electron/src/main/app/methods/setupWinListeners.js new file mode 100644 index 000000000..0560bfa07 --- /dev/null +++ b/packages/fether-electron/src/main/app/methods/setupWinListeners.js @@ -0,0 +1,82 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +import electron from 'electron'; +import debounce from 'lodash/debounce'; + +import Pino from '../utils/pino'; + +const pino = Pino(); + +function setupWinListeners (fetherApp) { + const { + hideWindow, + moveWindowUp, + onWindowClose, + options, + processSaveWinPosition, + win + } = fetherApp; + + // Open external links in browser + win.webContents.on('new-window', (event, url) => { + event.preventDefault(); + electron.shell.openExternal(url); + }); + + // Linux (unchecked on others) + win.on('move', () => { + /** + * On Linux using this with debouncing is the closest equivalent + * to using 'moved' (not supported on Linux) with debouncing + */ + debounce(() => { + processSaveWinPosition(fetherApp); + }, 1000); + }); + + // macOS (not Windows or Linux) + win.on('moved', () => { + /** + * On macOS save the position in the 'moved' event since if + * we run it just in 'close' instead, then if the Fether app + * crashes after they've moved the Fether window then it won't run + * 'close' and it won't save the window position. + * + * On Windows we use the equivalent WM_EXITSIZEMOVE that detects + * the equivalent of 'moved' + * + * On Linux the closest equivalent to achieving 'moved' is debouncing + * on the 'move' event. It also works in 'close' even when app crashes + */ + processSaveWinPosition(fetherApp); + }); + + // macOS and Linux (not Windows) + win.on('resize', () => { + pino.info('Detected resize event'); + + moveWindowUp(fetherApp); + setTimeout(() => { + moveWindowUp(fetherApp); + }, 5000); + }); + + win.on('blur', () => { + options.alwaysOnTop ? fetherApp.emit('blur-window') : hideWindow(fetherApp); + }); + + win.on('close', () => { + onWindowClose(fetherApp); + }); + + win.on('closed', () => { + fetherApp.win = null; + + fetherApp.emit('after-closed-window'); + }); +} + +export default setupWinListeners; diff --git a/packages/fether-electron/src/main/app/methods/showTrayBalloon.js b/packages/fether-electron/src/main/app/methods/showTrayBalloon.js new file mode 100644 index 000000000..15bdd8035 --- /dev/null +++ b/packages/fether-electron/src/main/app/methods/showTrayBalloon.js @@ -0,0 +1,33 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +import path from 'path'; + +import staticPath from '../utils/staticPath'; +import Pino from '../utils/pino'; + +const iconBalloonPath = path.join( + staticPath, + 'assets', + 'icons', + 'win', + 'iconBalloon.png' +); + +const pino = Pino(); + +function showTrayBalloon (fetherApp) { + const { tray } = fetherApp; + + pino.info('Showing Tray Balloon'); + + tray.displayBalloon({ + content: `Press ALT in the Fether window to toggle the menu`, + icon: iconBalloonPath, + title: 'Fether Menu' + }); +} + +export default showTrayBalloon; diff --git a/packages/fether-electron/src/main/app/methods/showWindow.js b/packages/fether-electron/src/main/app/methods/showWindow.js new file mode 100644 index 000000000..6849b1e25 --- /dev/null +++ b/packages/fether-electron/src/main/app/methods/showWindow.js @@ -0,0 +1,84 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +import { screen } from 'electron'; + +import { getSavedWindowPosition, hasSavedWindowPosition } from '../settings'; + +import Pino from '../utils/pino'; + +const pino = Pino(); + +function showWindow (fetherApp, trayPos) { + const { + calculateWinPosition, + createWindow, + fixWinPosition, + setupWinListeners, + setupWin32Listeners, + win + } = fetherApp; + + if (!win) { + createWindow(fetherApp); + } + + fetherApp.emit('show-window'); + + const calculatedWinPosition = calculateWinPosition(fetherApp, trayPos); + + pino.info('Calculated window position: ', calculatedWinPosition); + + const mainScreen = screen.getPrimaryDisplay(); + // const allScreens = screen.getAllDisplays(); + const mainScreenDims = mainScreen.size; + const mainScreenWorkAreaSize = mainScreen.workAreaSize; + + // workAreaSize does not include the tray depth + fetherApp.trayDepth = Math.max( + mainScreenDims.width - mainScreenWorkAreaSize.width, + mainScreenDims.height - mainScreenWorkAreaSize.height + ); + + pino.info( + 'Previously saved window position exists: ', + hasSavedWindowPosition() + ); + + const loadedWindowPosition = hasSavedWindowPosition() + ? getSavedWindowPosition() + : undefined; + + pino.info('Loaded window position: ', loadedWindowPosition); + + const fixedWinPosition = fixWinPosition(fetherApp, loadedWindowPosition); + + pino.info('Fixed window position: ', fixedWinPosition); + + /** + * Since the user may change the tray to be on any side of the screen. + * If the user moved the window out of where the tray would be in the screen resolution bounds. + * Restore the window so it is fully visible adjacent to where the tray would be. + */ + const x = + (fixedWinPosition && fixedWinPosition.x) || + (loadedWindowPosition && loadedWindowPosition.x) || + calculatedWinPosition.x; + + const y = + (fixedWinPosition && fixedWinPosition.y) || + (loadedWindowPosition && loadedWindowPosition.y) || + calculatedWinPosition.y; + + fetherApp.win.setPosition(x, y); + fetherApp.win.show(); + + setupWinListeners(fetherApp); + setupWin32Listeners(fetherApp); + + fetherApp.emit('after-show-window'); +} + +export default showWindow; diff --git a/packages/fether-electron/src/main/app/methods/updateProgress.js b/packages/fether-electron/src/main/app/methods/updateProgress.js new file mode 100644 index 000000000..eeb11fa22 --- /dev/null +++ b/packages/fether-electron/src/main/app/methods/updateProgress.js @@ -0,0 +1,21 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +// Optionally update the progress bar shown on the Dock icon +// (i.e. 0.1 is 10%, 1.0 is 100%, -1 hides progress bar). +// Optionally emit event +function updateProgress (fetherApp, percentage, eventListenerName) { + const { win } = fetherApp; + + if (percentage) { + win.setProgressBar(percentage); + } + + if (eventListenerName) { + fetherApp.emit(eventListenerName); + } +} + +export default updateProgress; diff --git a/packages/fether-electron/src/main/app/methods/windowClear.js b/packages/fether-electron/src/main/app/methods/windowClear.js new file mode 100644 index 000000000..9d65ed25c --- /dev/null +++ b/packages/fether-electron/src/main/app/methods/windowClear.js @@ -0,0 +1,21 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +function windowClear (fetherApp) { + const { win } = fetherApp; + + if (win) { + // Remove relevant events when window object deleted + const events = ['close', 'move', 'moved', 'resize']; + for (let event in events) { + win.removeAllListeners(event); + } + delete fetherApp.win; + } + + fetherApp.emit('after-close-window'); +} + +export default windowClear; diff --git a/packages/fether-electron/src/main/app/options/config/index.js b/packages/fether-electron/src/main/app/options/config/index.js new file mode 100644 index 000000000..b4313bc20 --- /dev/null +++ b/packages/fether-electron/src/main/app/options/config/index.js @@ -0,0 +1,81 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +import path from 'path'; +import url from 'url'; + +import staticPath from '../../utils/staticPath'; + +const INDEX_HTML_PATH = + process.env.ELECTRON_START_URL || + url.format({ + pathname: path.join(staticPath, 'build', 'index.html'), + protocol: 'file:', + slashes: true + }); + +// Icon path differs when started with `yarn electron` or `yarn start` +let iconPath = path.join(staticPath, 'assets', 'icons', 'icon.png'); +let iconDockPath = ''; + +if (process.platform === 'win32') { + iconPath = path.join(staticPath, 'assets', 'icons', 'win', 'icon.ico'); +} else if (process.platform === 'darwin') { + // https://github.com/electron/electron/blob/master/docs/api/native-image.md#template-image + iconPath = path.join( + staticPath, + 'assets', + 'icons', + 'mac', + 'iconTemplate.png' + ); + iconDockPath = path.join( + staticPath, + 'assets', + 'icons', + 'mac', + 'iconDock.png' + ); +} + +const shouldUseFrame = process.platform === 'win32'; + +const windowPosition = + process.platform === 'win32' ? 'trayBottomCenter' : 'trayCenter'; + +// API docs: https://electronjs.org/docs/api/browser-window +const DEFAULT_OPTIONS = { + alwaysOnTop: true, + dir: staticPath, + frame: true, + height: 640, + hasShadow: true, + icon: iconPath, + iconDock: iconDockPath, + index: INDEX_HTML_PATH, + resizable: false, + show: false, // Run showWindow later + showDockIcon: true, // macOS usage only + tabbingIdentifier: 'parity', + webPreferences: { + devTools: true, + enableRemoteModule: true // Remote is required in fether-react parityStore.js + }, + width: 360, + windowPosition: windowPosition, // Required + withTaskbar: false +}; + +const TASKBAR_OPTIONS = { + frame: shouldUseFrame, + height: 464, + // On Linux the user must click the tray icon and then click the tooltip + // to toggle the Fether window open/close + tooltip: 'Click to toggle Fether window', + width: 352, + withTaskbar: true +}; + +export { DEFAULT_OPTIONS, TASKBAR_OPTIONS }; diff --git a/packages/fether-electron/src/main/app/options/index.js b/packages/fether-electron/src/main/app/options/index.js new file mode 100644 index 000000000..dc0bee4e8 --- /dev/null +++ b/packages/fether-electron/src/main/app/options/index.js @@ -0,0 +1,11 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +import { DEFAULT_OPTIONS, TASKBAR_OPTIONS } from './config'; + +export default (withTaskbar, customOptions) => + withTaskbar + ? Object.assign({}, DEFAULT_OPTIONS, TASKBAR_OPTIONS, customOptions || {}) + : Object.assign({}, DEFAULT_OPTIONS, customOptions || {}); diff --git a/packages/fether-electron/src/main/app/parityEthereum/index.js b/packages/fether-electron/src/main/app/parityEthereum/index.js new file mode 100644 index 000000000..9709d3ae9 --- /dev/null +++ b/packages/fether-electron/src/main/app/parityEthereum/index.js @@ -0,0 +1,98 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +import { + getParityPath, + fetchParity, + isParityRunning, + runParity +} from '@parity/electron'; +import getRemainingArgs from 'commander-remaining-args'; + +import { parity } from '../../../../package.json'; +import handleError from '../utils/handleError'; +import cli from '../cli'; +import Pino from '../utils/pino'; + +const pino = Pino(); + +let hasCalledInitParityEthereum = false; + +class ParityEthereum { + constructor (fetherAppWindow) { + if (hasCalledInitParityEthereum) { + throw new Error('Unable to initialise Parity Ethereum more than once'); + } + + // Check if Parity Ethereum is installed + getParityPath() + // Download and install Parity Ethereum if not present + .catch(() => { + pino.info('Downloading and Installing Parity Ethereum'); + return this.install(fetherAppWindow); + }) + .then(async () => { + // Do not run Parity Ethereum if the user ran Fether with --no-run-parity + if (!cli.runParity) { + return false; + } + + // Do not run Parity Ethereum if it is already running + if (await this.isRunning()) { + return true; + } + + // Run Parity Ethereum when installed + await this.run(); + pino.info('Running Parity Ethereum'); + return true; + }) + .then(isRunning => { + // Notify the renderers + fetherAppWindow.webContents.send('parity-running', isRunning); + global.isParityRunning = isRunning; // Send this variable to renderers via IPC + }) + .catch(handleError); + } + + install = fetherAppWindow => { + return fetchParity(fetherAppWindow, { + onProgress: progress => { + // Notify the renderers on download progress + return fetherAppWindow.webContents.send( + 'parity-download-progress', + progress + ); + }, + parityChannel: parity.channel + }); + }; + + isRunning = async () => { + return isParityRunning({ + wsInterface: cli.wsInterface, + wsPort: cli.wsPort + }); + }; + + run = async () => { + return runParity({ + flags: [ + ...getRemainingArgs(cli), + '--light', + '--chain', + cli.chain, + '--ws-interface', + cli.wsInterface, + '--ws-port', + cli.wsPort + ], + onParityError: err => + handleError(err, 'An error occured with Parity Ethereum.') + }); + }; +} + +export default ParityEthereum; diff --git a/packages/fether-electron/src/main/app/settings/index.js b/packages/fether-electron/src/main/app/settings/index.js new file mode 100644 index 000000000..5ca03557f --- /dev/null +++ b/packages/fether-electron/src/main/app/settings/index.js @@ -0,0 +1,30 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +import settings from 'electron-settings'; + +const hasSavedWindowPosition = () => { + return settings.has('position.x') && settings.has('position.y'); +}; + +const getSavedWindowPosition = () => { + return { + x: settings.get('position.x'), + y: settings.get('position.y') + }; +}; + +/** + * Returns the latest window resolution if it differs from the previous resolution. + * Note that the previous window resolution may be undefined if being changed in settings. + */ +const saveWindowPosition = position => { + settings.set('position', { + x: position.x, + y: position.y + }); +}; + +export { getSavedWindowPosition, hasSavedWindowPosition, saveWindowPosition }; diff --git a/packages/fether-electron/src/main/utils/handleError.js b/packages/fether-electron/src/main/app/utils/handleError.js similarity index 94% rename from packages/fether-electron/src/main/utils/handleError.js rename to packages/fether-electron/src/main/app/utils/handleError.js index 8159dfede..16bfc24f0 100644 --- a/packages/fether-electron/src/main/utils/handleError.js +++ b/packages/fether-electron/src/main/app/utils/handleError.js @@ -5,7 +5,7 @@ import { app, dialog, shell } from 'electron'; -import { bugs, name, parity } from '../../../package.json'; +import { bugs, name, parity } from '../../../../package.json'; import Pino from './pino'; const logFile = `${app.getPath('userData')}/${name}.log`; diff --git a/packages/fether-electron/src/main/utils/pino.js b/packages/fether-electron/src/main/app/utils/pino.js similarity index 94% rename from packages/fether-electron/src/main/utils/pino.js rename to packages/fether-electron/src/main/app/utils/pino.js index 620844917..cb4ea31d3 100644 --- a/packages/fether-electron/src/main/utils/pino.js +++ b/packages/fether-electron/src/main/app/utils/pino.js @@ -8,7 +8,7 @@ import fs from 'fs'; import { multistream } from 'pino-multi-stream'; import Pino from 'pino'; -import { name } from '../../../package.json'; +import { name } from '../../../../package.json'; // Pino by default outputs JSON. We prettify that. const pretty = Pino.pretty(); diff --git a/packages/fether-electron/src/main/utils/staticPath.js b/packages/fether-electron/src/main/app/utils/staticPath.js similarity index 100% rename from packages/fether-electron/src/main/utils/staticPath.js rename to packages/fether-electron/src/main/app/utils/staticPath.js diff --git a/packages/fether-electron/src/main/app/utils/window.js b/packages/fether-electron/src/main/app/utils/window.js new file mode 100644 index 000000000..c940e3080 --- /dev/null +++ b/packages/fether-electron/src/main/app/utils/window.js @@ -0,0 +1,56 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +import Pino from './pino'; +const pino = Pino(); + +/** + * Returns the latest window resolution if it differs from the previous resolution. + * Note that the previous window resolution may be undefined if being changed in settings. + */ +const getChangedScreenResolution = ( + previousScreenResolution, + currentScreenResolution +) => { + if ( + !previousScreenResolution || + (previousScreenResolution && + previousScreenResolution.x !== currentScreenResolution.x) || + (previousScreenResolution && + previousScreenResolution.y !== currentScreenResolution.y) + ) { + return currentScreenResolution; + } + return previousScreenResolution; +}; + +/** + * Determine if we need to fix the window position. We will fix the window position if + * the user is changing display resolution since the previousScreenResolution may be undefined, + * or if the new previousScreenResolution was larger than the currentScreenResolution + * to prevent the window moved to a position where it is cropped or not visible at all. + */ +const shouldFixWindowPosition = ( + previousScreenResolution, + currentScreenResolution +) => { + pino.info( + 'Window resolution (previous, current): ', + previousScreenResolution, + currentScreenResolution + ); + if ( + !previousScreenResolution || + (previousScreenResolution && + previousScreenResolution.x > currentScreenResolution.x) || + (previousScreenResolution && + previousScreenResolution.y > currentScreenResolution.y) + ) { + return true; + } + return false; +}; + +export { getChangedScreenResolution, shouldFixWindowPosition }; diff --git a/packages/fether-electron/src/main/app/utils/window.spec.js b/packages/fether-electron/src/main/app/utils/window.spec.js new file mode 100644 index 000000000..e8a3bacc2 --- /dev/null +++ b/packages/fether-electron/src/main/app/utils/window.spec.js @@ -0,0 +1,88 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +/* eslint-env jest */ + +import { getChangedScreenResolution, shouldFixWindowPosition } from './window'; + +jest.mock('./pino', () => () => ({ + info: () => {} +})); + +let smallScreenResolution, largeScreenResolution; + +describe('window resolution', () => { + beforeEach(() => { + smallScreenResolution = { + x: 1024, + y: 768 + }; + + largeScreenResolution = { + x: 1366, + y: 768 + }; + }); + + test('should return previous resolution if it was defined and current resolution is the same', () => { + const previousScreenResolution = largeScreenResolution; + const currentScreenResolution = largeScreenResolution; + const screenResolution = getChangedScreenResolution( + previousScreenResolution, + currentScreenResolution + ); + expect(screenResolution).toEqual(previousScreenResolution); + }); + + test('should return current resolution if either its x or y coordinate differs in the previous resolution', () => { + const previousScreenResolution = largeScreenResolution; + const currentScreenResolution = smallScreenResolution; + const screenResolution = getChangedScreenResolution( + previousScreenResolution, + currentScreenResolution + ); + expect(screenResolution).toEqual(currentScreenResolution); + }); + + test('should return true to change resolution if previousScreenResolution is undefined', () => { + const previousScreenResolution = undefined; + const currentScreenResolution = smallScreenResolution; + const recommendation = shouldFixWindowPosition( + previousScreenResolution, + currentScreenResolution + ); + expect(recommendation).toEqual(true); + }); + + test('should return true to change resolution if either the x or y coordinate of the previousScreenResolution is greater than in the current resolution', () => { + const previousScreenResolution = largeScreenResolution; + const currentScreenResolution = smallScreenResolution; + const recommendation = shouldFixWindowPosition( + previousScreenResolution, + currentScreenResolution + ); + expect(recommendation).toEqual(true); + }); + + test('should return false to not change resolution if current resolution is larger than previous resolution', () => { + const previousScreenResolution = smallScreenResolution; + const currentScreenResolution = largeScreenResolution; + const recommendation = shouldFixWindowPosition( + previousScreenResolution, + currentScreenResolution + ); + expect(recommendation).toEqual(false); + }); + + test('should return false to not change resolution if current resolution is equal to the previous resolution', () => { + const previousScreenResolution = smallScreenResolution; + const currentScreenResolution = smallScreenResolution; + const recommendation = shouldFixWindowPosition( + previousScreenResolution, + currentScreenResolution + ); + expect(recommendation).toEqual(false); + }); +}); diff --git a/packages/fether-electron/src/main/index.js b/packages/fether-electron/src/main/index.js index dd9768115..e6dcad74b 100644 --- a/packages/fether-electron/src/main/index.js +++ b/packages/fether-electron/src/main/index.js @@ -3,151 +3,49 @@ // // SPDX-License-Identifier: BSD-3-Clause -import parityElectron, { - getParityPath, - fetchParity, - isParityRunning, - runParity, - killParity -} from '@parity/electron'; import electron from 'electron'; -import getRemainingArgs from 'commander-remaining-args'; -import path from 'path'; -import url from 'url'; +import { killParity } from '@parity/electron'; -import addMenu from './menu'; -import cli from './cli'; -import handleError from './utils/handleError'; -import messages from './messages'; -import { parity } from '../../package.json'; -import Pino from './utils/pino'; -import { productName } from '../../electron-builder.json'; -import staticPath from './utils/staticPath'; +import Pino from './app/utils/pino'; +import FetherApp from './app'; +import fetherAppOptions from './app/options'; -const { app, BrowserWindow, ipcMain, session } = electron; -let mainWindow; +const { app } = electron; const pino = Pino(); +let withTaskbar = process.env.TASKBAR !== 'false'; + +pino.info('Platform detected: ', process.platform); + // Disable gpu acceleration on linux // https://github.com/parity-js/fether/issues/85 if (!['darwin', 'win32'].includes(process.platform)) { app.disableHardwareAcceleration(); } -function createWindow () { - pino.info(`Starting ${productName}...`); - mainWindow = new BrowserWindow({ - height: 640, - resizable: false, - width: 360 - }); - - // Set options for @parity/electron - parityElectron({ - logger: namespace => log => Pino({ name: namespace }).info(log) - }); - - // Look if Parity is installed - getParityPath() - .catch(() => - // Install parity if not present - fetchParity(mainWindow, { - onProgress: progress => - // Notify the renderers on download progress - mainWindow.webContents.send('parity-download-progress', progress), - parityChannel: parity.channel - }) - ) - .then(async () => { - // Run parity when installed - - // Don't run parity if the user ran fether with --no-run-parity - if (!cli.runParity) { - return; - } - - if ( - await isParityRunning({ - wsInterface: cli.wsInterface, - wsPort: cli.wsPort - }) - ) { - return; - } - - return runParity({ - flags: [ - ...getRemainingArgs(cli), - '--light', - '--chain', - cli.chain, - '--ws-interface', - cli.wsInterface, - '--ws-port', - cli.wsPort - ], - onParityError: err => handleError(err, 'An error occured with Parity.') - }); - }) - .then(() => { - // Notify the renderers - mainWindow.webContents.send('parity-running', true); - global.isParityRunning = true; // Send this variable to renderes via IPC - }) - .catch(handleError); - - // Globals for fether-react parityStore - global.wsInterface = cli.wsInterface; - global.wsPort = cli.wsPort; - - // Opens file:///path/to/build/index.html in prod mode, or whatever is - // passed to ELECTRON_START_URL - mainWindow.loadURL( - process.env.ELECTRON_START_URL || - url.format({ - pathname: path.join(staticPath, 'build', 'index.html'), - protocol: 'file:', - slashes: true - }) - ); +const options = fetherAppOptions(withTaskbar, {}); - // Listen to messages from renderer process - ipcMain.on('asynchronous-message', (...args) => - messages(mainWindow, ...args) - ); - - // Add application menu - addMenu(mainWindow); - - // WS calls have Origin `file://` by default, which is not trusted. - // We override Origin header on all WS connections with an authorized one. - session.defaultSession.webRequest.onBeforeSendHeaders( - { - urls: ['ws://*/*', 'wss://*/*'] - }, - (details, callback) => { - if (!mainWindow) { - // There might be a split second where the user closes the app, so - // mainWindow is null, but there is still a network request done. - return; - } - details.requestHeaders.Origin = `parity://${mainWindow.id}.ui.parity`; - callback({ requestHeaders: details.requestHeaders }); // eslint-disable-line - } - ); +app.on('ready', () => { + return new FetherApp(app, options); +}); - // Open external links in browser - mainWindow.webContents.on('new-window', function (event, url) { - event.preventDefault(); - electron.shell.openExternal(url); - }); +// Event triggered by clicking the Electron icon in the menu Dock +// Reference: https://electronjs.org/docs/api/app#event-activate-macos +app.on('activate', (event, hasVisibleWindows) => { + if (withTaskbar) { + pino.info( + 'Detected Fether taskbar mode. Launching from application dock is not permitted.' + ); + return; + } - mainWindow.on('closed', () => { - mainWindow = null; - }); -} + if (hasVisibleWindows) { + pino.info('Existing Fether window detected.'); + return; + } -app.on('ready', createWindow); + return new FetherApp(app, options); +}); app.on('window-all-closed', () => { if (process.platform !== 'darwin') { @@ -156,13 +54,12 @@ app.on('window-all-closed', () => { } }); -// Make sure parity stops when UI stops +// Make sure Parity Ethereum stops when UI stops app.on('before-quit', killParity); + app.on('will-quit', killParity); -app.on('quit', killParity); -app.on('activate', () => { - if (mainWindow === null) { - createWindow(); - } +app.on('quit', () => { + pino.info('Leaving Fether'); + killParity(); }); diff --git a/packages/fether-electron/src/main/menu/index.js b/packages/fether-electron/src/main/menu/index.js deleted file mode 100644 index b41c6006f..000000000 --- a/packages/fether-electron/src/main/menu/index.js +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright 2015-2018 Parity Technologies (UK) Ltd. -// This file is part of Parity. -// -// SPDX-License-Identifier: BSD-3-Clause - -import electron from 'electron'; - -const { app, Menu, shell } = electron; - -export default mainWindow => { - // Create the Application's main menu - // https://github.com/electron/electron/blob/master/docs/api/menu.md#examples - const template = [ - { - label: 'Edit', - submenu: [ - { role: 'cut' }, - { role: 'copy' }, - { role: 'paste' }, - { role: 'delete' }, - { role: 'selectall' } - ] - }, - { - label: 'View', - submenu: [ - { role: 'reload' }, - { role: 'forcereload' }, - { role: 'toggledevtools' }, - { type: 'separator' }, - { role: 'resetzoom' }, - { role: 'zoomin' }, - { role: 'zoomout' }, - { type: 'separator' }, - { role: 'togglefullscreen' } - ] - }, - { - role: 'window', - submenu: [{ role: 'minimize' }, { role: 'close' }] - }, - { - role: 'help', - submenu: [ - { - label: 'Learn More', - click () { - shell.openExternal('https://parity.io'); - } - } - ] - } - ]; - - if (process.platform === 'darwin') { - template.unshift({ - label: app.getName(), - submenu: [ - { role: 'about' }, - { type: 'separator' }, - { role: 'services', submenu: [] }, - { type: 'separator' }, - { role: 'hide' }, - { role: 'hideothers' }, - { role: 'unhide' }, - { type: 'separator' }, - { role: 'quit' } - ] - }); - - // Edit menu - template[1].submenu.push( - { type: 'separator' }, - { - label: 'Speech', - submenu: [{ role: 'startspeaking' }, { role: 'stopspeaking' }] - } - ); - - // Window menu - template[3].submenu = [ - { role: 'close' }, - { role: 'minimize' }, - { role: 'zoom' }, - { type: 'separator' }, - { role: 'front' } - ]; - } - - const menu = Menu.buildFromTemplate(template); - - Menu.setApplicationMenu(menu); - mainWindow.setAutoHideMenuBar(true); -}; diff --git a/packages/fether-electron/static/assets/icons/icon.png b/packages/fether-electron/static/assets/icons/icon.png new file mode 100644 index 000000000..4b4e00460 Binary files /dev/null and b/packages/fether-electron/static/assets/icons/icon.png differ diff --git a/packages/fether-electron/static/assets/icons/icon@2x.png b/packages/fether-electron/static/assets/icons/icon@2x.png new file mode 100644 index 000000000..f33a32c8f Binary files /dev/null and b/packages/fether-electron/static/assets/icons/icon@2x.png differ diff --git a/packages/fether-electron/static/assets/icons/icon@3x.png b/packages/fether-electron/static/assets/icons/icon@3x.png new file mode 100644 index 000000000..c930c704d Binary files /dev/null and b/packages/fether-electron/static/assets/icons/icon@3x.png differ diff --git a/packages/fether-electron/static/assets/icons/icon@4x.png b/packages/fether-electron/static/assets/icons/icon@4x.png new file mode 100644 index 000000000..5831c576b Binary files /dev/null and b/packages/fether-electron/static/assets/icons/icon@4x.png differ diff --git a/packages/fether-electron/static/assets/icons/icon@5x.png b/packages/fether-electron/static/assets/icons/icon@5x.png new file mode 100644 index 000000000..7d90fe621 Binary files /dev/null and b/packages/fether-electron/static/assets/icons/icon@5x.png differ diff --git a/packages/fether-electron/static/assets/icons/mac/icon.png b/packages/fether-electron/static/assets/icons/mac/icon.png new file mode 100644 index 000000000..4b4e00460 Binary files /dev/null and b/packages/fether-electron/static/assets/icons/mac/icon.png differ diff --git a/packages/fether-electron/static/assets/icons/mac/iconDock.png b/packages/fether-electron/static/assets/icons/mac/iconDock.png new file mode 100644 index 000000000..6f55faf43 Binary files /dev/null and b/packages/fether-electron/static/assets/icons/mac/iconDock.png differ diff --git a/packages/fether-electron/static/assets/icons/mac/iconTemplate.png b/packages/fether-electron/static/assets/icons/mac/iconTemplate.png new file mode 100644 index 000000000..4b4e00460 Binary files /dev/null and b/packages/fether-electron/static/assets/icons/mac/iconTemplate.png differ diff --git a/packages/fether-electron/static/assets/icons/mac/iconTemplate@2x.png b/packages/fether-electron/static/assets/icons/mac/iconTemplate@2x.png new file mode 100644 index 000000000..088f6bd01 Binary files /dev/null and b/packages/fether-electron/static/assets/icons/mac/iconTemplate@2x.png differ diff --git a/packages/fether-electron/static/assets/icons/mac/iconTemplate@3x.png b/packages/fether-electron/static/assets/icons/mac/iconTemplate@3x.png new file mode 100644 index 000000000..325c7f6b6 Binary files /dev/null and b/packages/fether-electron/static/assets/icons/mac/iconTemplate@3x.png differ diff --git a/packages/fether-electron/static/assets/icons/mac/iconTemplate@4x.png b/packages/fether-electron/static/assets/icons/mac/iconTemplate@4x.png new file mode 100644 index 000000000..ca5f06685 Binary files /dev/null and b/packages/fether-electron/static/assets/icons/mac/iconTemplate@4x.png differ diff --git a/packages/fether-electron/static/assets/icons/mac/iconTemplate@5x.png b/packages/fether-electron/static/assets/icons/mac/iconTemplate@5x.png new file mode 100644 index 000000000..91ff3f721 Binary files /dev/null and b/packages/fether-electron/static/assets/icons/mac/iconTemplate@5x.png differ diff --git a/packages/fether-electron/static/assets/icons/old-white/icon.png b/packages/fether-electron/static/assets/icons/old-white/icon.png new file mode 100644 index 000000000..92d3d6a2a Binary files /dev/null and b/packages/fether-electron/static/assets/icons/old-white/icon.png differ diff --git a/packages/fether-electron/static/assets/icons/old-white/icon@2x.png b/packages/fether-electron/static/assets/icons/old-white/icon@2x.png new file mode 100644 index 000000000..e12cf0d07 Binary files /dev/null and b/packages/fether-electron/static/assets/icons/old-white/icon@2x.png differ diff --git a/packages/fether-electron/static/assets/icons/old-white/icon@3x.png b/packages/fether-electron/static/assets/icons/old-white/icon@3x.png new file mode 100644 index 000000000..e477eaf55 Binary files /dev/null and b/packages/fether-electron/static/assets/icons/old-white/icon@3x.png differ diff --git a/packages/fether-electron/static/assets/icons/old-white/icon@4x.png b/packages/fether-electron/static/assets/icons/old-white/icon@4x.png new file mode 100644 index 000000000..0d75cd760 Binary files /dev/null and b/packages/fether-electron/static/assets/icons/old-white/icon@4x.png differ diff --git a/packages/fether-electron/static/assets/icons/old-white/icon@5x.png b/packages/fether-electron/static/assets/icons/old-white/icon@5x.png new file mode 100644 index 000000000..8d657d065 Binary files /dev/null and b/packages/fether-electron/static/assets/icons/old-white/icon@5x.png differ diff --git a/packages/fether-electron/static/assets/icons/win/icon.ico b/packages/fether-electron/static/assets/icons/win/icon.ico new file mode 100644 index 000000000..4f8aa555b Binary files /dev/null and b/packages/fether-electron/static/assets/icons/win/icon.ico differ diff --git a/packages/fether-electron/static/assets/icons/win/iconBalloon.png b/packages/fether-electron/static/assets/icons/win/iconBalloon.png new file mode 100644 index 000000000..e477eaf55 Binary files /dev/null and b/packages/fether-electron/static/assets/icons/win/iconBalloon.png differ diff --git a/packages/fether-react/package.json b/packages/fether-react/package.json index 50368528d..548b99293 100644 --- a/packages/fether-react/package.json +++ b/packages/fether-react/package.json @@ -31,7 +31,7 @@ "start": "npm-run-all -p start-*", "start-css": "npm run build-css -- --watch --recursive", "start-js": "cross-env SKIP_PREFLIGHT_CHECK=true BROWSER=none craco start --react-scripts ../../node_modules/react-scripts", - "test": "ln -s ../../../node_modules/react-scripts node_modules/ && cross-env SKIP_PREFLIGHT_CHECK=true craco test; rm node_modules/react-scripts" + "test": "ln -s ../../../node_modules/react-scripts node_modules/ && cross-env SKIP_PREFLIGHT_CHECK=true craco test; EXIT_CODE=$?; rm node_modules/react-scripts; exit $EXIT_CODE" }, "dependencies": { "@craco/craco": "^3.3.1", diff --git a/packages/fether-react/src/Onboarding/Onboarding.js b/packages/fether-react/src/Onboarding/Onboarding.js index cd74b6059..bc19c43d7 100644 --- a/packages/fether-react/src/Onboarding/Onboarding.js +++ b/packages/fether-react/src/Onboarding/Onboarding.js @@ -53,14 +53,16 @@ class Onboarding extends Component {
Terms of Use} />
-
- +
+
+ +
diff --git a/packages/fether-react/src/RequireHealthOverlay/RequireHealthOverlay.js b/packages/fether-react/src/RequireHealthOverlay/RequireHealthOverlay.js index f69a79053..c699b2d34 100644 --- a/packages/fether-react/src/RequireHealthOverlay/RequireHealthOverlay.js +++ b/packages/fether-react/src/RequireHealthOverlay/RequireHealthOverlay.js @@ -87,7 +87,7 @@ class RequireHealthOverlay extends Component { case STATUS.CLOCKNOTSYNC: return 'Your clock is not sync'; case STATUS.DOWNLOADING: - return 'Downloading Parity...'; + return 'Downloading Parity Ethereum...'; case STATUS.NOINTERNET: return 'No Internet connection'; case STATUS.NOPEERS: diff --git a/packages/fether-react/src/assets/sass/components/_search-form.scss b/packages/fether-react/src/assets/sass/components/_search-form.scss index e82467546..e40be708a 100644 --- a/packages/fether-react/src/assets/sass/components/_search-form.scss +++ b/packages/fether-react/src/assets/sass/components/_search-form.scss @@ -15,7 +15,7 @@ font-weight: 400; font-family: $mono; line-height: ms(0) * 1.3; - width: calc(100% - 2rem;); + width: calc(100% - 2rem); opacity: 0.75; padding: 0.6rem 0.5rem 0.5rem; &:focus { diff --git a/packages/fether-react/src/assets/sass/components/_terms-and-conditions.scss b/packages/fether-react/src/assets/sass/components/_terms-and-conditions.scss index 058ef9d8d..ef3661ca1 100644 --- a/packages/fether-react/src/assets/sass/components/_terms-and-conditions.scss +++ b/packages/fether-react/src/assets/sass/components/_terms-and-conditions.scss @@ -1,7 +1,14 @@ +.terms-and-conditions-wrapper { + border-radius: 0.25rem; + box-shadow: 0 0.125rem 0.5rem rgba($black, 0.125); +} + .terms-and-conditions { - height: 20rem; + height: 16rem; /* Different impact on taskbar app */ + overflow-x: hidden; overflow-y: auto; - padding: 0.5rem; + margin-top: 0.75rem; + padding-left: 0.5rem; * { user-select: text; @@ -11,6 +18,7 @@ li { margin-bottom: 0.5rem; margin-top: 0.5rem; + padding-right: 0.5rem; } ul, diff --git a/packages/fether-react/src/assets/sass/shared/_basics.scss b/packages/fether-react/src/assets/sass/shared/_basics.scss index 93a4f587d..46711d596 100644 --- a/packages/fether-react/src/assets/sass/shared/_basics.scss +++ b/packages/fether-react/src/assets/sass/shared/_basics.scss @@ -3,6 +3,10 @@ html { } body { + /* Running taskbar app requires $eggshell background colour otherwise + * Onboarding component screen is shown with grey background + */ + background-color: $eggshell; user-select: none; color: $black; font-family: $sans; @@ -10,6 +14,8 @@ body { font-size: ms(0); line-height: ms(0) * 1.3; overflow-y: hidden; + /* https://electronjs.org/docs/api/frameless-window#draggable-region */ + -webkit-app-region: drag; } .hidden { @@ -25,3 +31,31 @@ a:visited { cursor: pointer; } } + +button, +input[type='text'], +input[type='number'], +input[type='tel'], +input[type='email'], +input[type='password'], +input[type='range'], +p, +textarea, +.account.-header, /* i.e. account name/address in header that is clickable */ +.account_avatar, /* i.e. on create a new account page */ +.box.-card, /* i.e. account button */ +.form-details-buttons, /* i.e. show/hide transaction details button */ +.feedback, +.footer-feedback, /* i.e. feedback button in header */ +.header-nav_left, /* i.e. back button in header */ +.header-nav_right, /* i.e. menu */ +.text { + -webkit-app-region: no-drag; +} + +h1, /* i.e. Send Ether title */ +.account.-header, +.status, +.text { + -webkit-user-select: none; +} \ No newline at end of file diff --git a/packages/fether-react/src/assets/sass/shared/_box.scss b/packages/fether-react/src/assets/sass/shared/_box.scss index 1e5b02d5b..f8ca0c489 100644 --- a/packages/fether-react/src/assets/sass/shared/_box.scss +++ b/packages/fether-react/src/assets/sass/shared/_box.scss @@ -43,6 +43,10 @@ padding: 0.5rem; } + &.-padded-extra { + padding: 0.75rem; + } + &.-centered { display: flex; flex-direction: column; diff --git a/packages/fether-react/src/stores/parityStore.js b/packages/fether-react/src/stores/parityStore.js index 15925fa32..c5225f76e 100644 --- a/packages/fether-react/src/stores/parityStore.js +++ b/packages/fether-react/src/stores/parityStore.js @@ -43,6 +43,9 @@ export class ParityStore { this.setToken(token); } + // FIXME - consider moving to start of this constructor block since + // if `setToken` method is called then `connectToApi` is called, which + // requires `electron` to be defined if (!electron) { debug( 'Not in Electron, ParityStore will only have limited capabilities.' diff --git a/packages/fether-react/src/stores/sendStore.spec.js b/packages/fether-react/src/stores/sendStore.spec.js index 67071257c..8fe68f89e 100644 --- a/packages/fether-react/src/stores/sendStore.spec.js +++ b/packages/fether-react/src/stores/sendStore.spec.js @@ -50,8 +50,14 @@ describe('method acceptRequest', () => { }); describe('method clear', () => { - test('should clear tx', () => { - sendStore.setTx(mock.tx); + test('should clear txEth', () => { + sendStore.setTx(mock.txEth); + sendStore.clear(); + expect(sendStore.tx).toEqual({}); + }); + + test('should clear txErc20', () => { + sendStore.setTx(mock.txErc20); sendStore.clear(); expect(sendStore.tx).toEqual({}); }); @@ -82,12 +88,10 @@ describe('@computed confirmations', () => { }); describe('method send', () => { - beforeEach(() => { - sendStore.setTx(mock.tx); - }); - test('should call transfer$ if the token is Erc20', () => { - sendStore.send(mock.erc20); + sendStore.setTx(mock.txErc20); + sendStore.send('password'); + expect(mock.txErc20.token.address).toEqual('THIBCoin'); expect(mock.makeContract.transfer$).toHaveBeenCalledWith( '0x123', new BigNumber('10000000000000000'), @@ -96,7 +100,9 @@ describe('method send', () => { }); test('should call post$ if the token is ETH', () => { - sendStore.send(mock.eth); + sendStore.setTx(mock.txEth); + sendStore.send('password'); + expect(mock.txEth.token.address).toEqual('ETH'); expect(lightJs.post$).toHaveBeenCalledWith({ from: '0x456', gasPrice: new BigNumber('4000000000'), @@ -107,14 +113,18 @@ describe('method send', () => { test('should update txStatus', () => { sendStore.setTxStatus = jest.fn(); - sendStore.send(mock.eth); + sendStore.setTx(mock.txEth); + sendStore.send('password'); + expect(mock.txEth.token.address).toEqual('ETH'); expect(sendStore.setTxStatus).toHaveBeenCalledWith({ estimating: true }); }); test('should call acceptRequest when txStatus is requested', () => { sendStore.acceptRequest = jest.fn(() => Promise.resolve(true)); - sendStore.send(mock.eth, 'foo'); - expect(sendStore.acceptRequest).toHaveBeenCalledWith(1, 'foo'); + sendStore.setTx(mock.txEth); + sendStore.send('password'); + expect(mock.txEth.token.address).toEqual('ETH'); + expect(sendStore.acceptRequest).toHaveBeenCalledWith(1, 'password'); }); }); diff --git a/packages/fether-react/src/utils/testHelpers/mock.js b/packages/fether-react/src/utils/testHelpers/mock.js index d1dc0f952..09c97e169 100644 --- a/packages/fether-react/src/utils/testHelpers/mock.js +++ b/packages/fether-react/src/utils/testHelpers/mock.js @@ -6,15 +6,24 @@ /* eslint-env jest */ import BigNumber from 'bignumber.js'; +import { toWei } from '@parity/api/lib/util/wei'; + +const SECRET_PHRASE = 'foo'; +const ADDRESS_FROM = '0x456'; +const ADDRESS_TO = '0x123'; +const GAS_PRICE = 4; // in Gwei +const GAS_ESTIMATE = 456; +const GAS_ESTIMATE_CONTRACT_TX = 123; +const AMOUNT = 0.01; export const api = { eth: { - estimateGas: jest.fn(() => Promise.resolve(new BigNumber(456))) + estimateGas: jest.fn(() => Promise.resolve(new BigNumber(GAS_ESTIMATE))) }, parity: { - generateSecretPhrase: jest.fn(() => Promise.resolve('foo')), + generateSecretPhrase: jest.fn(() => Promise.resolve(SECRET_PHRASE)), newAccountFromPhrase: jest.fn(() => Promise.resolve()), - phraseToAddress: jest.fn(() => Promise.resolve('0x123')), + phraseToAddress: jest.fn(() => Promise.resolve(ADDRESS_TO)), setAccountName: jest.fn(() => Promise.resolve()), setAccountMeta: jest.fn(() => Promise.resolve()) }, @@ -24,7 +33,7 @@ export const api = { }; export const erc20 = { - address: 'foo', + address: 'THIBCoin', decimals: 18 }; @@ -36,7 +45,8 @@ export const makeContract = { contractObject: { instance: { transfer: { - estimateGas: () => Promise.resolve(new BigNumber(123)) + estimateGas: () => + Promise.resolve(new BigNumber(GAS_ESTIMATE_CONTRACT_TX)) } } }, @@ -50,9 +60,32 @@ export const post$ = { }) }; -export const tx = { - amount: 0.01, // In Ether or in token - from: '0x456', - gasPrice: 4, // in Gwei - to: '0x123' +export const txEth = { + amount: AMOUNT, // In Ether + from: ADDRESS_FROM, + gasPrice: GAS_PRICE, + to: ADDRESS_TO, + token: eth +}; + +const txErc20Base = { + amount: AMOUNT, // In token + from: ADDRESS_FROM, + gasPrice: GAS_PRICE, + to: ADDRESS_TO, + token: erc20 +}; + +export const txErc20 = { + ...txErc20Base, + args: [ + txErc20Base.to, + new BigNumber(txErc20Base.amount).multipliedBy( + new BigNumber(10).pow(txErc20Base.token.decimals) + ) + ], + options: { + from: txErc20Base.from, + gasPrice: toWei(txErc20Base.gasPrice, 'shannon') // shannon == gwei + } }; diff --git a/packages/fether-react/src/utils/transaction.js b/packages/fether-react/src/utils/transaction.js index 1bf6f3d74..b28e488f8 100644 --- a/packages/fether-react/src/utils/transaction.js +++ b/packages/fether-react/src/utils/transaction.js @@ -142,17 +142,19 @@ const getEthereumTx = tx => { txParams.to = token.address; txParams.data = '0x' + - new Abi(eip20).functions.find(f => f._name === 'transfer').encodeCall([ - new Token('address', to), - new Token( - 'uint', - '0x' + - new BigNumber(amount) - .multipliedBy(new BigNumber(10).pow(token.decimals)) - .toNumber() - .toString(16) - ) - ]); + new Abi(eip20).functions + .find(f => f._name === 'transfer') + .encodeCall([ + new Token('address', to), + new Token( + 'uint', + '0x' + + new BigNumber(amount) + .multipliedBy(new BigNumber(10).pow(token.decimals)) + .toNumber() + .toString(16) + ) + ]); } return new EthereumTx(txParams); diff --git a/packages/fether-react/src/utils/transaction.spec.js b/packages/fether-react/src/utils/transaction.spec.js index faf5f6816..4b7d6e8c9 100644 --- a/packages/fether-react/src/utils/transaction.spec.js +++ b/packages/fether-react/src/utils/transaction.spec.js @@ -39,13 +39,13 @@ describe('estimateGas', () => { }); test('should call estimateGasForErc20 with token', () => { - expect(estimateGas(mock.tx, mock.erc20, mock.api)).resolves.toEqual( - new BigNumber(153.75) + expect(estimateGas(mock.txErc20, mock.erc20, mock.api)).resolves.toEqual( + new BigNumber(154) ); }); test('should call estimateGasForEth with token', () => { - expect(estimateGas(mock.tx, mock.eth, mock.api)).resolves.toEqual( + expect(estimateGas(mock.txEth, mock.eth, mock.api)).resolves.toEqual( new BigNumber(570) ); }); diff --git a/yarn.lock b/yarn.lock index 6b00f6e98..7cd28facc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3696,6 +3696,11 @@ clone@^1.0.2: resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= +clone@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" + integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= + cmd-shim@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-2.0.2.tgz#6fcbda99483a8fd15d7d30a196ca69d688a2efdb" @@ -4106,7 +4111,7 @@ copy-descriptor@^0.1.0: resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= -copyfiles@^2.0.0: +copyfiles@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/copyfiles/-/copyfiles-2.1.0.tgz#0e2a4188162d6b2f3c5adfe34e9c0bd564d23164" integrity sha512-cAeDE0vL/koE9WSEGxqPpSyvU638Kgfu6wfrnj7kqp9FWa1CWsU54Coo6sdYZP4GstWa39tL/wIVJWfXcujgNA== @@ -5066,7 +5071,7 @@ ejs@^2.6.1: resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.6.1.tgz#498ec0d495655abc6f23cd61868d926464071aa0" integrity sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ== -electron-builder@^20.29.0: +electron-builder@^20.38.5: version "20.38.5" resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-20.38.5.tgz#31b3913a68b4911afd4cfc7bcd2522c5808040cd" integrity sha512-p88IDHhH2J4hA6KwRBJY+OfVZuFtFIShY3Uh/TwYAfbX0v1RhKZytuGdO8sty2zcWxDYX74xDBv+X9oN6qEIRQ== @@ -5131,6 +5136,11 @@ electron-osx-sign@0.4.11: minimist "^1.2.0" plist "^3.0.1" +electron-positioner@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/electron-positioner/-/electron-positioner-4.1.0.tgz#e158f8f6aabd6725a8a9b4f2279b9504bcbea1b0" + integrity sha512-726DfbI9ZNoCg+Fcu6XLuTKTnzf+6nFqv7h+K/V6Ug7IbaPMI7s9S8URnGtWFCy5N5PL4HSzRFF2mXuinftDdg== + electron-publish@20.38.5: version "20.38.5" resolved "https://registry.yarnpkg.com/electron-publish/-/electron-publish-20.38.5.tgz#c6ed7ea12bc80796b1f36489995f4651f730b1df" @@ -5144,6 +5154,14 @@ electron-publish@20.38.5: lazy-val "^1.0.3" mime "^2.4.0" +electron-settings@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/electron-settings/-/electron-settings-3.2.0.tgz#01461e153f95b6f18adbe0c360c70898eb0f43c3" + integrity sha512-7U+vDKd5Gch4Z9K6FjGq80eB3Anwz2GuPc2h/6hOiuvZrS1w+UNPcAA0oAU8G1s9sWAVEadCsr4ZJR6J4iTdzA== + dependencies: + clone "^2.1.1" + jsonfile "^4.0.0" + electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.103, electron-to-chromium@^1.3.62: version "1.3.113" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.113.tgz#b1ccf619df7295aea17bc6951dc689632629e4a9" @@ -5160,7 +5178,7 @@ electron-webpack-js@~2.3.0: babel-loader "^8.0.5" babel-plugin-component "^1.1.1" -electron-webpack@^2.1.2: +electron-webpack@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/electron-webpack/-/electron-webpack-2.6.1.tgz#69425faa780215586f8290b55279bad1d2f6c974" integrity sha512-PHr5/5syGsHzuFxQCzLuvBmPfE+MCDgllcR6s6nDrQC69pZ2ICACiKVVFO+Q2wVb2XBfH31jozaFZ6hscreuwg== @@ -14880,7 +14898,7 @@ webpack@4.19.1: watchpack "^1.5.0" webpack-sources "^1.2.0" -webpack@^4.7.0: +webpack@^4.29.1: version "4.29.1" resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.29.1.tgz#a6533d7bc6a6b1ed188cb029d53d231be777e175" integrity sha512-dY3KyQIVeg6cDPj9G5Bnjy9Pt9SoCpbNWl0RDKHstbd3MWe0dG9ri4RQRpCm43iToy3zoA1IMOpFkJ8Clnc7FQ==