From f66bd70a859e6bb185418985c311d547d7bd34c2 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Sat, 18 Apr 2020 17:09:59 +0100 Subject: [PATCH] feat(ux): move preferences to menubar License: MIT Signed-off-by: Henrique Dias --- assets/locales/en.json | 54 +++++++++++++- package.json | 2 +- src/auto-launch.js | 43 ++++++++++-- src/create-toggler.js | 23 ++---- src/dialogs/errors.js | 14 ++-- src/download-cid.js | 7 +- src/exec-or-sudo.js | 10 +-- src/ipfs-on-path/index.js | 55 ++++++++++++--- src/npm-on-ipfs/index.js | 73 +++++++++++-------- src/npm-on-ipfs/package.js | 18 +++-- src/setup-global-shortcut.js | 27 +++++-- src/take-screenshot.js | 7 +- src/tray.js | 65 +++++++++++++++-- src/webui/preload.js | 16 ----- test/unit/create-toggler.spec.js | 116 +++++++++---------------------- test/unit/mocks/webui.js | 9 --- 16 files changed, 340 insertions(+), 199 deletions(-) delete mode 100644 test/unit/mocks/webui.js diff --git a/assets/locales/en.json b/assets/locales/en.json index dfeb2754e..ea01c7a4f 100644 --- a/assets/locales/en.json +++ b/assets/locales/en.json @@ -17,7 +17,7 @@ "readReleaseNotes": "Read Release Notes", "status": "Status", "files": "Files", - "settings": "Settings", + "peers": "Peers", "quit": "Quit", "versions": "Versions", "screenshotTaken": "Screenshot taken", @@ -32,6 +32,7 @@ "close": "Close", "ok": "OK", "cancel": "Cancel", + "enable": "Enable", "reportTheError": "Report the error", "restartIpfsDesktop": "Restart IPFS Desktop", "openLogs": "Open logs", @@ -171,5 +172,56 @@ "couldNotSaveDialog": { "title": "Could not write to disk", "message": "There was an error writing to the disk. Please try again." + }, + "launchAtLoginNotSupported": { + "title": "Error", + "message": "Launch at login is not supported on your platform." + }, + "launchAtLoginFailed": { + "title": "Error", + "message": "Launch at login could not be enabled on your machine." + }, + "enableIpfsOnPath": { + "title": "Enable IPFS on PATH", + "message": "By enabling this option, IPFS will be available on your command line as \"ipfs\". This action is reversible.", + "action": "Enable" + }, + "disableIpfsOnPath": { + "title": "Disable IPFS on PATH", + "message": "By disabling this option, IPFS will no longer be available on your command line as \"ipfs\".", + "action": "Disable" + }, + "enableGlobalTakeScreenshotShortcut": { + "title": "Enable screenshot shortcut", + "message": "By enabling this, the shortcut \"{ accelerator }\" will be available to take screenshots as long as IPFS Desktop is running." + }, + "enableGlobalDownloadShortcut": { + "title": "Enable download shortcut", + "message": "By enabling this, the shortcut \"{ accelerator }\" will be available to download files as long as IPFS Desktop is running." + }, + "installNpmOnIpfsWarning": { + "title": "Install npm on IPFS", + "message": "This experimental feature installs the \"ipfs-npm\" package on your system. It requires Node.js to be installed.", + "action": "Install" + }, + "unableToInstallNpmOnIpfs": { + "title": "Error", + "message": "It was not possible to install \"ipfs-npm\" package on your system. Please check the logs for more information or try installing it manually by running \"npm install -g ipfs-npm\" on your command line." + }, + "unableToUninstallNpmOnIpfs": { + "title": "Error", + "message": "It was not possible to uninstall \"ipfs-npm\" package on your system. Please check the logs for more information or try uninstalling it manually by running \"npm uninstall -g ipfs-npm\" on your command line." + }, + "settings": { + "settings": "Settings", + "preferences": "Preferences", + "openNodeSettings": "Open Node Settings", + "desktopIntegrations": "Desktop Integrations", + "launchOnStartup": "Launch at Login", + "ipfsCommandLineTools": "Command Line Tools", + "takeScreenshotShortcut": "Global Screenshot Shortcut", + "downloadHashShortcut": "Global Download Shortcut", + "experiments": "Experiments", + "npmOnIpfs": "npm on IPFS" } } diff --git a/package.json b/package.json index bb863f08a..3675f39ad 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "clean:webui": "shx rm -rf assets/webui/", "build": "run-s clean:webui build:*", "build:webui": "run-s build:webui:*", - "build:webui:download": "npx ipfs-or-gateway -c bafybeidatpz2hli6fgu3zul5woi27ujesdf5o5a7bu622qj6ugharciwjq -p assets/webui/ -t 360000 --verbose", + "build:webui:download": "npx ipfs-or-gateway -c bafybeicevz7mmgt45zitnmg73khkmvioz2ozixvd6guujphhtsenu2pt2a -p assets/webui/ -t 360000 --verbose", "build:webui:minimize": "shx rm -rf assets/webui/static/js/*.map && shx rm -rf assets/webui/static/css/*.map", "build:binaries": "electron-builder --publish onTag" }, diff --git a/src/auto-launch.js b/src/auto-launch.js index 05501cde9..0ae30f56b 100644 --- a/src/auto-launch.js +++ b/src/auto-launch.js @@ -1,4 +1,5 @@ const { app } = require('electron') +const i18n = require('i18next') const os = require('os') const path = require('path') const fs = require('fs-extra') @@ -7,6 +8,7 @@ const createToggler = require('./create-toggler') const logger = require('./common/logger') const store = require('./common/store') const { IS_MAC, IS_WIN } = require('./common/consts') +const { showDialog, recoverableErrorDialog } = require('./dialogs') const CONFIG_KEY = 'autoLaunch' @@ -47,22 +49,40 @@ async function disable () { await fs.remove(getDesktopFile()) } -module.exports = async function (ctx) { - const activate = async (value, oldValue) => { +module.exports = async function () { + const activate = async ({ newValue, oldValue, feedback }) => { if (process.env.NODE_ENV === 'development') { logger.info('[launch on startup] unavailable during development') + + if (feedback) { + showDialog({ + title: 'Launch at Login', + message: 'Not available during development.', + buttons: [i18n.t('close')] + }) + } + return } if (!isSupported()) { logger.info('[launch on startup] not supported on this platform') + + if (feedback) { + showDialog({ + title: i18n.t('launchAtLoginNotSupported.title'), + message: i18n.t('launchAtLoginNotSupported.message'), + buttons: [i18n.t('close')] + }) + } + return false } - if (value === oldValue) return + if (newValue === oldValue) return try { - if (value === true) { + if (newValue === true) { await enable() logger.info('[launch on startup] enabled') } else { @@ -73,10 +93,21 @@ module.exports = async function (ctx) { return true } catch (err) { logger.error(`[launch on startup] ${err.toString()}`) + + if (feedback) { + recoverableErrorDialog(err, { + title: i18n.t('launchAtLoginFailed.title'), + message: i18n.t('launchAtLoginFailed.message') + }) + } + return false } } - activate(store.get(CONFIG_KEY, false)) - createToggler(ctx, CONFIG_KEY, activate) + activate({ newValue: store.get(CONFIG_KEY, false) }) + createToggler(CONFIG_KEY, activate) } + +module.exports.CONFIG_KEY = CONFIG_KEY +module.exports.isSupported = isSupported diff --git a/src/create-toggler.js b/src/create-toggler.js index 6f3b42a97..884df4a7e 100644 --- a/src/create-toggler.js +++ b/src/create-toggler.js @@ -1,31 +1,22 @@ const { ipcMain } = require('electron') -const os = require('os') const store = require('./common/store') const logger = require('./common/logger') -module.exports = function ({ webui }, settingsOption, activate) { - ipcMain.on('config.toggle', async (_, opt) => { - if (opt !== settingsOption) { - return - } - +module.exports = function (settingsOption, activate) { + ipcMain.on(`toggle_${settingsOption}`, async () => { const oldValue = store.get(settingsOption, null) const newValue = !oldValue - let success = false - if (await activate(newValue, oldValue)) { + if (await activate({ newValue, oldValue, feedback: true })) { store.set(settingsOption, newValue) - success = true const action = newValue ? 'enabled' : 'disabled' logger.info(`[${settingsOption}] ${action}`) } - webui.webContents.send('config.changed', { - config: store.store, - changed: settingsOption, - platform: os.platform(), - success - }) + // We always emit the event so any handlers for it can act upon + // the current configuration, whether it was successfully + // updated or not. + ipcMain.emit('configUpdated') }) } diff --git a/src/dialogs/errors.js b/src/dialogs/errors.js index bd6851667..6aa10eb60 100644 --- a/src/dialogs/errors.js +++ b/src/dialogs/errors.js @@ -47,7 +47,7 @@ function criticalErrorDialog (e) { // Shows a recoverable error dialog with the default title and message. // Passing an options object alongside the error can be used to override // the title and message. -function recoverableErrorDialog (e, options = {}) { +function recoverableErrorDialog (e, options) { const cfg = { title: i18n.t('recoverableErrorDialog.title'), message: i18n.t('recoverableErrorDialog.message'), @@ -59,12 +59,14 @@ function recoverableErrorDialog (e, options = {}) { ] } - if (options.title) { - cfg.title = options.title - } + if (options) { + if (options.title) { + cfg.title = options.title + } - if (options.message) { - cfg.message = options.message + if (options.message) { + cfg.message = options.message + } } const option = dialog(cfg) diff --git a/src/download-cid.js b/src/download-cid.js index 4bc2f05c3..6dd408253 100644 --- a/src/download-cid.js +++ b/src/download-cid.js @@ -131,7 +131,11 @@ async function downloadCid (ctx) { } module.exports = function (ctx) { - setupGlobalShortcut(ctx, { + setupGlobalShortcut({ + confirmationDialog: { + title: i18n.t('enableGlobalDownloadShortcut.title'), + message: i18n.t('enableGlobalDownloadShortcut.message', { accelerator: SHORTCUT }) + }, settingsOption: CONFIG_KEY, accelerator: SHORTCUT, action: () => { @@ -142,3 +146,4 @@ module.exports = function (ctx) { module.exports.downloadCid = downloadCid module.exports.SHORTCUT = SHORTCUT +module.exports.CONFIG_KEY = CONFIG_KEY diff --git a/src/exec-or-sudo.js b/src/exec-or-sudo.js index 2a05a4c95..1163e4e2f 100644 --- a/src/exec-or-sudo.js +++ b/src/exec-or-sudo.js @@ -15,7 +15,7 @@ const env = { sudo: 'env ELECTRON_RUN_AS_NODE=1' } -const getResult = (err, stdout, stderr, scope, failSilently) => { +const getResult = (err, stdout, stderr, scope, failSilently, errorOptions) => { if (stdout) { logger.info(`[${scope}] sudo: stdout: ${stdout.toString().trim()}`) } @@ -37,14 +37,14 @@ const getResult = (err, stdout, stderr, scope, failSilently) => { } else if (str.includes('User did not grant permission')) { dialog.showErrorBox(i18n.t('noPermissionDialog.title'), i18n.t('noPermissionDialog.message')) } else { - recoverableErrorDialog(err) + recoverableErrorDialog(err, errorOptions) } } return false } -module.exports = async function ({ script, scope, failSilently, trySudo = true }) { +module.exports = async function ({ script, scope, failSilently, trySudo = true, errorOptions }) { const dataArg = `--data="${app.getPath('userData')}"` let err = null @@ -61,7 +61,7 @@ module.exports = async function ({ script, scope, failSilently, trySudo = true } if (!trySudo) { if (!failSilently) { - recoverableErrorDialog(err) + recoverableErrorDialog(err, errorOptions) } return false @@ -71,7 +71,7 @@ module.exports = async function ({ script, scope, failSilently, trySudo = true } const command = `${env.sudo} "${process.execPath}" "${script}" ${dataArg}` return new Promise(resolve => { sudo.exec(command, { name: 'IPFS Desktop' }, (err, stdout, stderr) => { - resolve(getResult(err, stdout, stderr, scope, failSilently)) + resolve(getResult(err, stdout, stderr, scope, failSilently, errorOptions)) }) }) } diff --git a/src/ipfs-on-path/index.js b/src/ipfs-on-path/index.js index 1d1bdab79..25440ca9a 100644 --- a/src/ipfs-on-path/index.js +++ b/src/ipfs-on-path/index.js @@ -7,20 +7,57 @@ const execOrSudo = require('../exec-or-sudo') const logger = require('../common/logger') const store = require('../common/store') const { IS_WIN } = require('../common/consts') -const { recoverableErrorDialog } = require('../dialogs') +const { showDialog, recoverableErrorDialog } = require('../dialogs') const CONFIG_KEY = 'ipfsOnPath' -module.exports = async function (ctx) { - createToggler(ctx, CONFIG_KEY, async (value, oldValue) => { - if (value === oldValue || (oldValue === null && !value)) return - if (value === true) return run('install') +const errorMessage = { + title: i18n.t('cantAddIpfsToPath.title'), + message: i18n.t('cantAddIpfsToPath.message') +} + +module.exports = async function () { + createToggler(CONFIG_KEY, async ({ newValue, oldValue }) => { + if (newValue === oldValue || (oldValue === null && !newValue)) { + return + } + + if (newValue === true) { + if (showDialog({ + title: i18n.t('enableIpfsOnPath.title'), + message: i18n.t('enableIpfsOnPath.message'), + buttons: [ + i18n.t('enableIpfsOnPath.action'), + i18n.t('cancel') + ] + }) !== 0) { + // User canceled + return + } + + return run('install') + } + + if (showDialog({ + title: i18n.t('disableIpfsOnPath.title'), + message: i18n.t('disableIpfsOnPath.message'), + buttons: [ + i18n.t('disableIpfsOnPath.action'), + i18n.t('cancel') + ] + }) !== 0) { + // User canceled + return + } + return run('uninstall') }) firstTime() } +module.exports.CONFIG_KEY = CONFIG_KEY + async function firstTime () { // Check if we've done this before. if (store.get(CONFIG_KEY, null) !== null) { @@ -54,10 +91,7 @@ async function runWindows (script, { failSilently }) { logger.error(`[ipfs on path] ${err.toString()}`) if (!failSilently) { - recoverableErrorDialog(err, { - title: i18n.t('cantAddIpfsToPath.title'), - message: i18n.t('cantAddIpfsToPath.message') - }) + recoverableErrorDialog(err, errorMessage) } return resolve(false) @@ -78,6 +112,7 @@ async function run (script, { trySudo = true, failSilently = false } = {}) { script: join(__dirname, `./scripts/${script}.js`), scope: 'ipfs on path', trySudo, - failSilently + failSilently, + errorOptions: errorMessage }) } diff --git a/src/npm-on-ipfs/index.js b/src/npm-on-ipfs/index.js index 13dd1833b..2ee0a2537 100644 --- a/src/npm-on-ipfs/index.js +++ b/src/npm-on-ipfs/index.js @@ -1,48 +1,32 @@ const which = require('which') +const i18n = require('i18next') const pkg = require('./package') const logger = require('../common/logger') const store = require('../common/store') +const { showDialog } = require('../dialogs') const createToggler = require('../create-toggler') const CONFIG_KEY = 'experiments.npmOnIpfs' module.exports = function (ctx) { - let interval = null + // Every 12 hours, check if `ipfs-npm` is installed and, if it is, + // tries to update it to the latest version. + setInterval(existsAndUpdate, 43200000) - createToggler(ctx, CONFIG_KEY, async (value, oldValue) => { - if (value === oldValue || oldValue === null) return true + // Configure toggler + createToggler(CONFIG_KEY, toggle) - // If the user is telling to (un)install even though they have (un)installed - // ipfs-npm package manually. - const manual = isPkgInstalled() === value - - if (value === true) { - if (!manual && !await pkg.install()) return false - interval = setInterval(existsAndUpdate, 43200000) // every 12 hours - return true - } - - clearInterval(interval) - return manual || pkg.uninstall() - }) - - let opt = store.get(CONFIG_KEY, null) - const exists = isPkgInstalled() - - if (opt === null) { + // When running for the first time, update the config to know if `ipfs-npm` + // is installed or not. + if (store.get(CONFIG_KEY, null) === null) { + const exists = isPkgInstalled() logger.info(`[npm on ipfs] 1st time running and package is ${exists ? 'installed' : 'not installed'}`) store.set(CONFIG_KEY, exists) - opt = exists - } - - if (opt === true) { - logger.info('[npm on ipfs] set to update every 12 hours') - interval = setInterval(existsAndUpdate, 43200000) // every 12 hours - } else { - logger.info('[npm on ipfs] no action taken') } } +module.exports.CONFIG_KEY = CONFIG_KEY + function isPkgInstalled () { return !!which.sync('ipfs-npm', { nothrow: true }) } @@ -54,3 +38,34 @@ function existsAndUpdate () { store.set(CONFIG_KEY, false) } } + +async function toggle ({ newValue, oldValue }) { + if (newValue === oldValue || oldValue === null) { + return true + } + + // If the user is telling to (un)install even though they have (un)installed + // ipfs-npm package manually. + const manual = isPkgInstalled() === newValue + + if (!newValue) { + return manual || pkg.uninstall() + } + + const opt = showDialog({ + type: 'warning', + title: i18n.t('installNpmOnIpfsWarning.title'), + message: i18n.t('installNpmOnIpfsWarning.message'), + buttons: [ + i18n.t('installNpmOnIpfsWarning.action'), + i18n.t('cancel') + ] + }) + + if (opt !== 0) { + // User canceled + return + } + + return manual || pkg.install() +} diff --git a/src/npm-on-ipfs/package.js b/src/npm-on-ipfs/package.js index 980ca6860..11ff37eb5 100644 --- a/src/npm-on-ipfs/package.js +++ b/src/npm-on-ipfs/package.js @@ -1,6 +1,8 @@ const util = require('util') +const i18n = require('i18next') const logger = require('../common/logger') const { IS_WIN } = require('../common/consts') +const { recoverableErrorDialog } = require('../dialogs') const childProcess = require('child_process') const execFile = util.promisify(childProcess.execFile) @@ -31,8 +33,12 @@ async function install () { await execFile(npmBin, ['install', '-g', 'ipfs-npm']) logger.info('[npm on ipfs] ipfs-npm: installed globally') return true - } catch (e) { - logger.error(`[npm on ipfs] ${e.toString()}`) + } catch (err) { + logger.error(`[npm on ipfs] ${err.toString()}`) + recoverableErrorDialog(err, { + title: i18n.t('unableToInstallNpmOnIpfs.title'), + message: i18n.t('unableToInstallNpmOnIpfs.message') + }) return false } } @@ -42,8 +48,12 @@ async function uninstall () { await execFile(npmBin, ['uninstall', '-g', 'ipfs-npm']) logger.info('[npm on ipfs] ipfs-npm: uninstalled globally') return true - } catch (e) { - logger.error(`[npm on ipfs] ${e.toString()}`) + } catch (err) { + logger.error(`[npm on ipfs] ${err.toString()}`) + recoverableErrorDialog(err, { + title: i18n.t('unableToUninstallNpmOnIpfs.title'), + message: i18n.t('unableToUninstallNpmOnIpfs.message') + }) return false } } diff --git a/src/setup-global-shortcut.js b/src/setup-global-shortcut.js index 2d219f9fa..dc1607148 100644 --- a/src/setup-global-shortcut.js +++ b/src/setup-global-shortcut.js @@ -1,15 +1,30 @@ const { globalShortcut, ipcMain } = require('electron') +const i18n = require('i18next') const createToggler = require('./create-toggler') const store = require('./common/store') const { IS_MAC } = require('./common/consts') +const { showDialog } = require('./dialogs') // This function registers a global shortcut/accelerator with a certain action // and (de)activates it according to its 'settingsOption' value on settings. -module.exports = function (ctx, { settingsOption, accelerator, action }) { - const activate = (value, oldValue) => { - if (value === oldValue) return +module.exports = function ({ settingsOption, accelerator, action, confirmationDialog }) { + const activate = ({ newValue, oldValue, feedback }) => { + if (newValue === oldValue) return + + if (newValue === true) { + if (feedback && confirmationDialog) { + if (showDialog({ + ...confirmationDialog, + buttons: [ + i18n.t('enable'), + i18n.t('cancel') + ] + }) !== 0) { + // User canceled + return + } + } - if (value === true) { globalShortcut.register(accelerator, action) } else { globalShortcut.unregister(accelerator) @@ -18,8 +33,8 @@ module.exports = function (ctx, { settingsOption, accelerator, action }) { return true } - activate(store.get(settingsOption, false)) - createToggler(ctx, settingsOption, activate) + activate({ newValue: store.get(settingsOption, false) }) + createToggler(settingsOption, activate) if (!IS_MAC) { return diff --git a/src/take-screenshot.js b/src/take-screenshot.js index 0d5bcad94..c87ec385a 100644 --- a/src/take-screenshot.js +++ b/src/take-screenshot.js @@ -102,7 +102,11 @@ function takeScreenshot (ctx) { } module.exports = function (ctx) { - setupGlobalShortcut(ctx, { + setupGlobalShortcut({ + confirmationDialog: { + title: i18n.t('enableGlobalTakeScreenshotShortcut.title'), + message: i18n.t('enableGlobalTakeScreenshotShortcut.message', { accelerator: SHORTCUT }) + }, settingsOption: CONFIG_KEY, accelerator: SHORTCUT, action: () => { @@ -115,3 +119,4 @@ module.exports = function (ctx) { module.exports.takeScreenshot = takeScreenshot module.exports.SHORTCUT = SHORTCUT +module.exports.CONFIG_KEY = CONFIG_KEY diff --git a/src/tray.js b/src/tray.js index 0b9aa0e34..33dbefcd6 100644 --- a/src/tray.js +++ b/src/tray.js @@ -1,15 +1,37 @@ const { Menu, Tray, shell, app, ipcMain } = require('electron') const i18n = require('i18next') const path = require('path') -const { SHORTCUT: SCREENSHOT_SHORTCUT, takeScreenshot } = require('./take-screenshot') -const { SHORTCUT: DOWNLOAD_SHORTCUT, downloadCid } = require('./download-cid') const addToIpfs = require('./add-to-ipfs') -const { STATUS } = require('./daemon') const logger = require('./common/logger') const store = require('./common/store') -const { IS_MAC, IS_WIN, VERSION, GO_IPFS_VERSION } = require('./common/consts') const moveRepositoryLocation = require('./move-repository-location') const runGarbageCollector = require('./run-gc') +const { STATUS } = require('./daemon') +const { IS_MAC, IS_WIN, VERSION, GO_IPFS_VERSION } = require('./common/consts') + +const { CONFIG_KEY: SCREENSHOT_KEY, SHORTCUT: SCREENSHOT_SHORTCUT, takeScreenshot } = require('./take-screenshot') +const { CONFIG_KEY: DOWNLOAD_KEY, SHORTCUT: DOWNLOAD_SHORTCUT, downloadCid } = require('./download-cid') +const { CONFIG_KEY: AUTO_LAUNCH_KEY, isSupported: supportsLaunchAtLogin } = require('./auto-launch') +const { CONFIG_KEY: IPFS_PATH_KEY } = require('./ipfs-on-path') +const { CONFIG_KEY: NPM_IPFS_KEY } = require('./npm-on-ipfs') + +const CONFIG_KEYS = [ + AUTO_LAUNCH_KEY, + IPFS_PATH_KEY, + NPM_IPFS_KEY, + SCREENSHOT_KEY, + DOWNLOAD_KEY +] + +function buildCheckbox (key, label) { + return { + id: key, + label: i18n.t(label), + click: () => { ipcMain.emit(`toggle_${key}`) }, + type: 'checkbox', + checked: false + } +} // Notes on this: we are only supporting accelerators on macOS for now because // they natively work as soon as the menu opens. They don't work like that on Windows @@ -60,8 +82,8 @@ function buildMenu (ctx) { click: () => { ctx.launchWebUI('/files') } }, { - label: i18n.t('settings'), - click: () => { ctx.launchWebUI('/settings') } + label: i18n.t('peers'), + click: () => { ctx.launchWebUI('/peers') } }, { type: 'separator' }, { @@ -79,6 +101,30 @@ function buildMenu (ctx) { enabled: false }, { type: 'separator' }, + { + label: IS_MAC ? i18n.t('settings.preferences') : i18n.t('settings.settings'), + submenu: [ + { + label: i18n.t('settings.openNodeSettings'), + click: () => { ctx.launchWebUI('/settings') } + }, + { type: 'separator' }, + { + label: i18n.t('settings.desktopIntegrations'), + enabled: false + }, + buildCheckbox(AUTO_LAUNCH_KEY, 'settings.launchOnStartup'), + buildCheckbox(IPFS_PATH_KEY, 'settings.ipfsCommandLineTools'), + buildCheckbox(SCREENSHOT_KEY, 'settings.takeScreenshotShortcut'), + buildCheckbox(DOWNLOAD_KEY, 'settings.downloadHashShortcut'), + { type: 'separator' }, + { + label: i18n.t('settings.experiments'), + enabled: false + }, + buildCheckbox(NPM_IPFS_KEY, 'settings.npmOnIpfs') + ] + }, { label: i18n.t('advanced'), submenu: [ @@ -211,6 +257,7 @@ module.exports = function (ctx) { menu.getMenuItemById('stopIpfs').enabled = !gcRunning menu.getMenuItemById('restartIpfs').enabled = !gcRunning + menu.getMenuItemById(AUTO_LAUNCH_KEY).enabled = supportsLaunchAtLogin() menu.getMenuItemById('takeScreenshot').enabled = status === STATUS.STARTING_FINISHED menu.getMenuItemById('downloadCid').enabled = status === STATUS.STARTING_FINISHED @@ -223,6 +270,11 @@ module.exports = function (ctx) { tray.setImage(icon(off)) } + // Update configuration checkboxes. + for (const key of CONFIG_KEYS) { + menu.getMenuItemById(key).checked = store.get(key, false) + } + if (!IS_MAC && !IS_WIN) { // On Linux, in order for changes made to individual MenuItems to take effect, // you have to call setContextMenu again - https://electronjs.org/docs/api/tray @@ -245,6 +297,7 @@ module.exports = function (ctx) { updateMenu() }) + ipcMain.on('configUpdated', () => { updateMenu() }) ipcMain.on('languageUpdated', () => { setupMenu() }) setupMenu() diff --git a/src/webui/preload.js b/src/webui/preload.js index fca175db4..3bb21231e 100644 --- a/src/webui/preload.js +++ b/src/webui/preload.js @@ -53,22 +53,6 @@ window.ipfsDesktop = { version: VERSION, - onConfigChanged: (listener) => { - ipcRenderer.on('config.changed', (_, config) => { - listener(config) - }) - - ipcRenderer.send('config.get') - }, - - toggleSetting: (setting) => { - ipcRenderer.send('config.toggle', setting) - }, - - configHasChanged: () => { - ipcRenderer.send('ipfsConfigChanged') - }, - selectDirectory: () => { return new Promise(resolve => { remote.dialog.showOpenDialog(remote.getCurrentWindow(), { diff --git a/test/unit/create-toggler.spec.js b/test/unit/create-toggler.spec.js index 0a2019cf2..adb60cc5a 100644 --- a/test/unit/create-toggler.spec.js +++ b/test/unit/create-toggler.spec.js @@ -3,11 +3,9 @@ const sinon = require('sinon') const chai = require('chai') const { expect } = chai -const os = require('os') const dirtyChai = require('dirty-chai') const mockElectron = require('./mocks/electron') const mockStore = require('./mocks/store') -const mockWebUI = require('./mocks/webui') const mockLogger = require('./mocks/logger') const proxyquire = require('proxyquire').noCallThru() @@ -15,12 +13,11 @@ chai.use(dirtyChai) describe('Create toggler', () => { const option = 'OPT' - let electron, store, webui, createToggler, logger + let electron, store, createToggler, logger beforeEach(() => { electron = mockElectron() store = mockStore() - webui = mockWebUI() logger = mockLogger() createToggler = proxyquire('../../src/create-toggler', { electron: electron, @@ -30,107 +27,76 @@ describe('Create toggler', () => { }) it('activate option with success', (done) => { + const spy = sinon.spy() const activate = sinon.stub().returns(true) - createToggler({ webui: webui }, option, activate) - electron.ipcMain.emit('config.toggle', null, option) + createToggler(option, activate) + + electron.ipcMain.on('configUpdated', spy) + electron.ipcMain.emit(`toggle_${option}`) setImmediate(() => { expect(store.get.callCount).to.equal(1) expect(activate.callCount).to.equal(1) expect(store.set.callCount).to.equal(1) - - const args = webui.webContents.send.lastCall.args - - expect(args[0]).to.equal('config.changed') - expect(args[1]).to.deep.equal({ - changed: 'OPT', - platform: os.platform(), - config: { - OPT: true - }, - success: true - }) - + expect(spy.calledOnce).to.equal(true) + expect(store.get(option)).to.equal(true) done() }) }) it('activate option with error', (done) => { + const spy = sinon.spy() + store.set(option, false) const activate = sinon.stub().returns(false) - createToggler({ webui: webui }, option, activate) - electron.ipcMain.emit('config.toggle', null, option) + createToggler(option, activate) + + electron.ipcMain.on('configUpdated', spy) + electron.ipcMain.emit(`toggle_${option}`) setImmediate(() => { expect(store.get.callCount).to.equal(1) expect(activate.callCount).to.equal(1) - expect(store.set.callCount).to.equal(0) - - const args = webui.webContents.send.lastCall.args - - expect(args[0]).to.equal('config.changed') - expect(args[1]).to.deep.equal({ - changed: 'OPT', - platform: os.platform(), - config: {}, - success: false - }) - + expect(store.set.callCount).to.equal(1) + expect(spy.calledOnce).to.equal(true) + expect(store.get(option)).to.equal(false) done() }) }) it('disable option with success', (done) => { - const activate = sinon.stub().returns(true) store.set(option, true) + const spy = sinon.spy() + const activate = sinon.stub().returns(true) + createToggler(option, activate) - createToggler({ webui: webui }, option, activate) - electron.ipcMain.emit('config.toggle', null, option) + electron.ipcMain.on('configUpdated', spy) + electron.ipcMain.emit(`toggle_${option}`) setImmediate(() => { expect(store.get.callCount).to.equal(1) expect(activate.callCount).to.equal(1) expect(store.set.callCount).to.equal(2) - - const args = webui.webContents.send.lastCall.args - - expect(args[0]).to.equal('config.changed') - expect(args[1]).to.deep.equal({ - changed: 'OPT', - platform: os.platform(), - config: { - OPT: false - }, - success: true - }) - + expect(spy.calledOnce).to.equal(true) + expect(store.get(option)).to.equal(false) done() }) }) it('disable option with error', (done) => { - const activate = sinon.stub().returns(false) store.set(option, true) + const spy = sinon.spy() + const activate = sinon.stub().returns(false) + createToggler(option, activate) - createToggler({ webui: webui }, option, activate) - electron.ipcMain.emit('config.toggle', null, option) + electron.ipcMain.on('configUpdated', spy) + electron.ipcMain.emit(`toggle_${option}`) setImmediate(() => { expect(store.get.callCount).to.equal(1) expect(activate.callCount).to.equal(1) expect(store.set.callCount).to.equal(1) - - const args = webui.webContents.send.lastCall.args - - expect(args[0]).to.equal('config.changed') - expect(args[1]).to.deep.equal({ - changed: 'OPT', - platform: os.platform(), - config: { - OPT: true - }, - success: false - }) - + expect(spy.calledOnce).to.equal(true) + expect(store.get(option)).to.equal(true) done() }) }) @@ -138,9 +104,9 @@ describe('Create toggler', () => { it('enable and disable option with success', (done) => { const activate = sinon.stub().returns(true) - createToggler({ webui: webui }, option, activate) - electron.ipcMain.emit('config.toggle', null, option) - electron.ipcMain.emit('config.toggle', null, option) + createToggler(option, activate) + electron.ipcMain.emit(`toggle_${option}`) + electron.ipcMain.emit(`toggle_${option}`) setImmediate(() => { expect(store.get.callCount).to.equal(2) @@ -149,18 +115,4 @@ describe('Create toggler', () => { done() }) }) - - it('do not trigger anything on different option', (done) => { - const activate = sinon.spy() - - createToggler({ webui: webui }, option, activate) - electron.ipcMain.emit('config.toggle', null, 'ANOTHER_OPTION') - - setImmediate(() => { - expect(store.get.callCount).to.equal(0) - expect(activate.callCount).to.equal(0) - expect(store.set.callCount).to.equal(0) - done() - }) - }) }) diff --git a/test/unit/mocks/webui.js b/test/unit/mocks/webui.js deleted file mode 100644 index 345d648d9..000000000 --- a/test/unit/mocks/webui.js +++ /dev/null @@ -1,9 +0,0 @@ -const sinon = require('sinon') - -module.exports = function mockWebUI () { - return { - webContents: { - send: sinon.spy() - } - } -}