ethereum/mist

View on GitHub
modules/menuItems.js

Summary

Maintainability
F
1 wk
Test Coverage
const {
  app,
  BrowserWindow,
  ipcMain: ipc,
  Menu,
  shell,
  dialog
} = require('electron');
const fs = require('fs');
const path = require('path');
const Windows = require('./windows');
const Settings = require('./settings');
const log = require('./utils/logger').create('menuItems');
const swarmLog = require('./utils/logger').create('swarm');
const updateChecker = require('./updateChecker');
const ethereumNode = require('./ethereumNode.js');
const ClientBinaryManager = require('./clientBinaryManager');

import {
  setLanguage,
  toggleSwarm,
  toggleSwarmOnStart
} from './core/settings/actions';
import { changeNetwork, changeSyncMode } from './core/nodes/actions';
import { SwarmState } from './core/settings/reducer';
import swarmNode from './swarmNode.js';

// Make easier to return values for specific systems
const switchForSystem = function(options) {
  if (process.platform in options) {
    return options[process.platform];
  } else if ('default' in options) {
    return options.default;
  }
  return null;
};

// create menu
// null -> null
const createMenu = function(webviews) {
  webviews = webviews || [];

  const menu = Menu.buildFromTemplate(menuTempl(webviews));
  Menu.setApplicationMenu(menu);
};

const restartNode = function(newType, newNetwork, syncMode, webviews) {
  newNetwork = newNetwork || ethereumNode.network;

  log.info('Switch node', newType, newNetwork);

  store.dispatch(changeNetwork(newNetwork));

  return ethereumNode
    .restart(newType, newNetwork, syncMode)
    .then(() => {
      Windows.getByType('main').load(global.interfaceAppUrl);
      createMenu(webviews);
      log.info('Node switch successful.');
    })
    .catch(err => {
      log.error('Error switching node', err);
    });
};

const changeNodeNetwork = function(network, webviews) {
  store.dispatch(changeNetwork(network));

  Settings.saveUserData('network', network);

  restartNode(ethereumNode.type, network, ethereumNode.syncMode, webviews);

  createMenu(webviews);
};

const changeNodeSyncMode = function(syncMode, webviews) {
  store.dispatch(changeSyncMode(syncMode));

  Settings.saveUserData('syncmode', syncMode);

  restartNode(ethereumNode.type, ethereumNode.network, syncMode, webviews);

  createMenu(webviews);
};

const startMining = webviews => {
  ethereumNode
    .send('miner_start', [1])
    .then(ret => {
      log.info('miner_start', ret.result);

      if (ret.result) {
        global.mining = true;
        createMenu(webviews);
      }
    })
    .catch(err => {
      log.error('miner_start', err);
    });
};

const stopMining = webviews => {
  ethereumNode
    .send('miner_stop', [1])
    .then(ret => {
      log.info('miner_stop', ret.result);

      if (ret.result) {
        global.mining = false;
        createMenu(webviews);
      }
    })
    .catch(err => {
      log.error('miner_stop', err);
    });
};

// create a menu template
// null -> obj
let menuTempl = function(webviews) {
  const menu = [];
  webviews = webviews || [];

  // APP
  const fileMenu = [];

  if (process.platform === 'darwin') {
    fileMenu.push(
      {
        label: i18n.t('mist.applicationMenu.app.about', {
          app: Settings.appName
        }),
        click() {
          Windows.createPopup('about');
        }
      },
      {
        label: i18n.t('mist.applicationMenu.app.checkForUpdates'),
        click() {
          updateChecker.runVisibly();
        }
      },
      {
        label: i18n.t('mist.applicationMenu.app.checkForNodeUpdates'),
        click() {
          // remove skipVersion
          fs.writeFileSync(
            path.join(Settings.userDataPath, 'skippedNodeVersion.json'),
            '' // write no version
          );

          // true = will restart after updating and user consent
          ClientBinaryManager.init(true);
        }
      },
      {
        type: 'separator'
      },
      {
        label: i18n.t('mist.applicationMenu.app.services', {
          app: Settings.appName
        }),
        role: 'services',
        submenu: []
      },
      {
        type: 'separator'
      },
      {
        label: i18n.t('mist.applicationMenu.app.hide', {
          app: Settings.appName
        }),
        accelerator: 'Command+H',
        role: 'hide'
      },
      {
        label: i18n.t('mist.applicationMenu.app.hideOthers', {
          app: Settings.appName
        }),
        accelerator: 'Command+Alt+H',
        role: 'hideothers'
      },
      {
        label: i18n.t('mist.applicationMenu.app.showAll', {
          app: Settings.appName
        }),
        role: 'unhide'
      },
      {
        type: 'separator'
      }
    );
  }

  fileMenu.push({
    label: i18n.t('mist.applicationMenu.app.quit', {
      app: Settings.appName
    }),
    accelerator: 'CommandOrControl+Q',
    click() {
      app.quit();
    }
  });

  menu.push({
    label: i18n.t('mist.applicationMenu.app.label', {
      app: Settings.appName
    }),
    submenu: fileMenu
  });

  let swarmUpload = [];
  if (global.mode !== 'wallet') {
    swarmUpload.push(
      {
        type: 'separator'
      },
      {
        label: i18n.t('mist.applicationMenu.file.swarmUpload'),
        accelerator: 'Shift+CommandOrControl+U',
        enabled: store.getState().settings.swarmState == SwarmState.Enabled,
        click() {
          const focusedWindow = BrowserWindow.getFocusedWindow();
          const paths = dialog.showOpenDialog(focusedWindow, {
            properties: ['openFile', 'openDirectory']
          });
          if (paths && paths.length === 1) {
            const isDir = fs.lstatSync(paths[0]).isDirectory();
            const defaultPath = path.join(paths[0], 'index.html');
            const uploadConfig = {
              path: paths[0],
              kind: isDir ? 'directory' : 'file',
              defaultFile: fs.existsSync(defaultPath) ? '/index.html' : null
            };
            swarmNode
              .upload(uploadConfig)
              .then(hash => {
                focusedWindow.webContents.executeJavaScript(`
                          Tabs.update('browser', {$set: {
                              url: 'bzz://${hash}',
                              redirect: 'bzz://${hash}'
                          }});
                          LocalStore.set('selectedTab', 'browser');
                          `);
                swarmLog.info('Hash uploaded:', hash);
              })
              .catch(e => swarmLog.error(e));
          }
        }
      }
    );
  }

  menu.push({
    label: i18n.t('mist.applicationMenu.file.label'),
    submenu: [
      {
        label: i18n.t('mist.applicationMenu.file.newAccount'),
        accelerator: 'CommandOrControl+N',
        click() {
          Windows.createPopup('requestAccount');
        }
      },
      {
        label: i18n.t('mist.applicationMenu.file.importPresale'),
        accelerator: 'CommandOrControl+I',
        enabled: ethereumNode.isMainNetwork,
        click() {
          Windows.createPopup('importAccount');
        }
      },
      {
        type: 'separator'
      },
      {
        label: i18n.t('mist.applicationMenu.file.backup'),
        submenu: [
          {
            label: i18n.t('mist.applicationMenu.file.backupKeyStore'),
            click() {
              let userPath = Settings.userHomePath;

              // eth
              if (ethereumNode.isEth) {
                if (process.platform === 'win32') {
                  userPath = `${Settings.appDataPath}\\Web3\\keys`;
                } else {
                  userPath += '/.web3/keys';
                }

                // geth
              } else {
                if (process.platform === 'darwin') {
                  userPath += '/Library/Ethereum/keystore';
                }

                if (
                  process.platform === 'freebsd' ||
                  process.platform === 'linux' ||
                  process.platform === 'sunos'
                ) {
                  userPath += '/.ethereum/keystore';
                }

                if (process.platform === 'win32') {
                  userPath = `${Settings.appDataPath}\\Ethereum\\keystore`;
                }
              }

              shell.showItemInFolder(userPath);
            }
          },
          {
            label: i18n.t('mist.applicationMenu.file.backupMist'),
            click() {
              shell.openItem(Settings.userDataPath);
            }
          }
        ]
      },
      ...swarmUpload
    ]
  });

  // EDIT
  menu.push({
    label: i18n.t('mist.applicationMenu.edit.label'),
    submenu: [
      {
        label: i18n.t('mist.applicationMenu.edit.undo'),
        accelerator: 'CommandOrControl+Z',
        role: 'undo'
      },
      {
        label: i18n.t('mist.applicationMenu.edit.redo'),
        accelerator: 'Shift+CommandOrControl+Z',
        role: 'redo'
      },
      {
        type: 'separator'
      },
      {
        label: i18n.t('mist.applicationMenu.edit.cut'),
        accelerator: 'CommandOrControl+X',
        role: 'cut'
      },
      {
        label: i18n.t('mist.applicationMenu.edit.copy'),
        accelerator: 'CommandOrControl+C',
        role: 'copy'
      },
      {
        label: i18n.t('mist.applicationMenu.edit.paste'),
        accelerator: 'CommandOrControl+V',
        role: 'paste'
      },
      {
        label: i18n.t('mist.applicationMenu.edit.selectAll'),
        accelerator: 'CommandOrControl+A',
        role: 'selectall'
      }
    ]
  });

  // LANGUAGE (VIEW)
  const switchLang = langCode => (menuItem, browserWindow) => {
    store.dispatch(setLanguage(langCode, browserWindow));
  };

  const currentLanguage = Settings.language;
  const languageMenu = Object.keys(i18n.options.resources)
    .filter(langCode => langCode !== 'dev')
    .map(langCode => {
      const menuItem = {
        label: i18n.t(`mist.applicationMenu.view.langCodes.${langCode}`),
        type: 'checkbox',
        checked: langCode === currentLanguage,
        click: switchLang(langCode)
      };
      return menuItem;
    });

  languageMenu.unshift(
    {
      label: i18n.t('mist.applicationMenu.view.default'),
      click: switchLang(i18n.getBestMatchedLangCode(app.getLocale()))
    },
    {
      type: 'separator'
    }
  );

  // VIEW
  menu.push({
    label: i18n.t('mist.applicationMenu.view.label'),
    submenu: [
      {
        label: i18n.t('mist.applicationMenu.view.txHistory'),
        accelerator: 'CommandOrControl+Shift+H',
        click() {
          Windows.createPopup('txHistory');
        }
      },
      {
        label: i18n.t('mist.applicationMenu.view.fullscreen'),
        accelerator: switchForSystem({
          darwin: 'Command+Control+F',
          default: 'F11'
        }),
        click() {
          const mainWindow = Windows.getByType('main');

          mainWindow.window.setFullScreen(!mainWindow.window.isFullScreen());
        }
      },
      {
        label: i18n.t('mist.applicationMenu.view.languages'),
        submenu: languageMenu
      }
    ]
  });

  // DEVELOP
  const devToolsMenu = [];
  let devtToolsSubMenu;
  let curWindow;

  // change for wallet
  if (Settings.uiMode === 'mist') {
    devtToolsSubMenu = [
      {
        label: i18n.t('mist.applicationMenu.develop.devToolsMistUI'),
        accelerator: 'Alt+CommandOrControl+I',
        click() {
          curWindow = BrowserWindow.getFocusedWindow();
          if (curWindow) {
            curWindow.toggleDevTools();
          }
        }
      },
      {
        type: 'separator'
      }
    ];

    // add webviews
    webviews.forEach(webview => {
      devtToolsSubMenu.push({
        label: i18n.t('mist.applicationMenu.develop.devToolsWebview', {
          webview: webview.name
        }),
        click() {
          Windows.getByType('main').send(
            'uiAction_toggleWebviewDevTool',
            webview._id
          );
        }
      });
    });

    // wallet
  } else {
    devtToolsSubMenu = [
      {
        label: i18n.t('mist.applicationMenu.develop.devToolsWalletUI'),
        accelerator: 'Alt+CommandOrControl+I',
        click() {
          curWindow = BrowserWindow.getFocusedWindow();
          if (curWindow) {
            curWindow.toggleDevTools();
          }
        }
      }
    ];
  }

  devToolsMenu.push({
    label: i18n.t('mist.applicationMenu.develop.devTools'),
    submenu: devtToolsSubMenu
  });

  if (Settings.uiMode === 'mist') {
    devToolsMenu.push({
      label: i18n.t('mist.applicationMenu.develop.openRemix'),
      enabled: true,
      click() {
        Windows.createPopup('remix');
      }
    });
  }

  devToolsMenu.push({
    label: i18n.t('mist.applicationMenu.develop.runTests'),
    enabled: Settings.uiMode === 'mist',
    click() {
      Windows.getByType('main').send('uiAction_runTests', 'webview');
    }
  });

  devToolsMenu.push({
    label: i18n.t('mist.applicationMenu.develop.logFiles'),
    click() {
      try {
        const shown = shell.showItemInFolder(
          path.join(Settings.userDataPath, 'logs', 'all.log')
        );
        if (!shown) {
          shell.showItemInFolder(
            path.join(Settings.userDataPath, 'logs', 'all.log.0')
          );
        }
      } catch (error) {
        log.error(error);
      }
    }
  });

  // add node switching menu
  devToolsMenu.push({
    type: 'separator'
  });

  // add node switch
  if (process.platform === 'darwin' || process.platform === 'win32') {
    const nodeSubmenu = [];

    const ethClient = ClientBinaryManager.getClient('eth');
    const gethClient = ClientBinaryManager.getClient('geth');

    if (gethClient) {
      nodeSubmenu.push({
        label: `Geth ${gethClient.version}`,
        checked: ethereumNode.isOwnNode && ethereumNode.isGeth,
        enabled: ethereumNode.isOwnNode,
        type: 'checkbox',
        click() {
          restartNode('geth', null, 'fast', webviews);
        }
      });
    }

    if (ethClient) {
      nodeSubmenu.push({
        label: `Eth ${ethClient.version} (C++)`,
        checked: ethereumNode.isOwnNode && ethereumNode.isEth,
        enabled: ethereumNode.isOwnNode,
        // enabled: false,
        type: 'checkbox',
        click() {
          restartNode('eth');
        }
      });
    }

    devToolsMenu.push({
      label: i18n.t('mist.applicationMenu.develop.ethereumNode'),
      submenu: nodeSubmenu
    });
  }

  // add network switch
  devToolsMenu.push({
    label: i18n.t('mist.applicationMenu.develop.network'),
    submenu: [
      {
        label: i18n.t('mist.applicationMenu.develop.mainNetwork'),
        accelerator: 'CommandOrControl+Alt+1',
        checked: store.getState().nodes.network === 'main',
        enabled: store.getState().nodes.network !== 'private',
        type: 'checkbox',
        click() {
          changeNodeNetwork('main', webviews);
        }
      },
      {
        label: 'Ropsten - Test network',
        accelerator: 'CommandOrControl+Alt+2',
        checked: store.getState().nodes.network === 'ropsten',
        enabled: store.getState().nodes.network !== 'private',
        type: 'checkbox',
        click() {
          changeNodeNetwork('ropsten', webviews);
        }
      },
      {
        label: 'Rinkeby - Test network',
        accelerator: 'CommandOrControl+Alt+3',
        checked: store.getState().nodes.network === 'rinkeby',
        enabled: store.getState().nodes.network !== 'private',
        type: 'checkbox',
        click() {
          changeNodeNetwork('rinkeby', webviews);
        }
      }
      // {
      //   label: 'Solo network',
      //   accelerator: 'CommandOrControl+Alt+4',
      //   checked: ethereumNode.isOwnNode && ethereumNode.isDevNetwork,
      //   enabled: ethereumNode.isOwnNode,
      //   type: 'checkbox',
      //   click() {
      //     restartNode(ethereumNode.type, 'dev');
      //   }
      // }
    ]
  });

  // add sync mode switch
  devToolsMenu.push({
    label: i18n.t('mist.applicationMenu.develop.syncMode'),
    submenu: [
      {
        label: i18n.t('mist.applicationMenu.develop.syncModeLight'),
        enabled: ethereumNode.isOwnNode && !ethereumNode.isDevNetwork,
        checked: store.getState().nodes.local.syncMode === 'light',
        type: 'checkbox',
        click() {
          changeNodeSyncMode('light', webviews);
        }
      },
      {
        label: i18n.t('mist.applicationMenu.develop.syncModeFast'),
        enabled: ethereumNode.isOwnNode && !ethereumNode.isDevNetwork,
        checked: store.getState().nodes.local.syncMode === 'fast',
        type: 'checkbox',
        click() {
          changeNodeSyncMode('fast', webviews);
        }
      },
      {
        label: i18n.t('mist.applicationMenu.develop.syncModeFull'),
        enabled: ethereumNode.isOwnNode,
        checked: store.getState().nodes.local.syncMode === 'full',
        type: 'checkbox',
        click() {
          changeNodeSyncMode('full', webviews);
        }
      },
      {
        label: i18n.t('mist.applicationMenu.develop.syncModeNoSync'),
        enabled: ethereumNode.isOwnNode && !ethereumNode.isDevNetwork,
        checked: store.getState().nodes.local.syncMode === 'nosync',
        type: 'checkbox',
        click() {
          changeNodeSyncMode('nosync', webviews);
        }
      }
    ]
  });

  // Enables mining menu: only in Solo mode and Ropsten network (testnet)
  if (
    ethereumNode.isOwnNode &&
    (ethereumNode.isTestNetwork || ethereumNode.isDevNetwork)
  ) {
    devToolsMenu.push(
      {
        type: 'separator'
      },
      {
        label: global.mining
          ? i18n.t('mist.applicationMenu.develop.stopMining')
          : i18n.t('mist.applicationMenu.develop.startMining'),
        accelerator: 'CommandOrControl+Shift+M',
        enabled: true,
        click() {
          if (global.mining) {
            stopMining(webviews);
          } else {
            startMining(webviews);
          }
        }
      }
    );
  }

  if (global.mode !== 'wallet') {
    devToolsMenu.push(
      {
        type: 'separator'
      },
      {
        label: i18n.t('mist.applicationMenu.develop.enableSwarm'),
        enabled: true,
        checked: [SwarmState.Enabling, SwarmState.Enabled].includes(
          global.store.getState().settings.swarmState
        ),
        type: 'checkbox',
        click() {
          store.dispatch(toggleSwarm());
        }
      }
    );
  }

  menu.push({
    label:
      (global.mining ? '⛏ ' : '') +
      i18n.t('mist.applicationMenu.develop.label'),
    submenu: devToolsMenu
  });

  // WINDOW
  menu.push({
    label: i18n.t('mist.applicationMenu.window.label'),
    role: 'window',
    submenu: [
      {
        label: i18n.t('mist.applicationMenu.window.minimize'),
        accelerator: 'CommandOrControl+M',
        role: 'minimize'
      },
      {
        label: i18n.t('mist.applicationMenu.window.close'),
        accelerator: 'CommandOrControl+W',
        role: 'close'
      },
      {
        type: 'separator'
      },
      {
        label: i18n.t('mist.applicationMenu.window.toFront'),
        role: 'front'
      }
    ]
  });

  // HELP
  const helpMenu = [];

  if (
    process.platform === 'freebsd' ||
    process.platform === 'linux' ||
    process.platform === 'sunos' ||
    process.platform === 'win32'
  ) {
    helpMenu.push(
      {
        label: i18n.t('mist.applicationMenu.app.about', {
          app: Settings.appName
        }),
        click() {
          Windows.createPopup('about');
        }
      },
      {
        label: i18n.t('mist.applicationMenu.app.checkForUpdates'),
        click() {
          updateChecker.runVisibly();
        }
      }
    );
  }
  helpMenu.push(
    {
      label: i18n.t('mist.applicationMenu.help.mistWiki'),
      click() {
        shell.openExternal('https://github.com/ethereum/mist/wiki');
      }
    },
    {
      label: i18n.t('mist.applicationMenu.help.gitter'),
      click() {
        shell.openExternal('https://gitter.im/ethereum/mist');
      }
    },
    {
      label: i18n.t('mist.applicationMenu.help.reportBug'),
      click() {
        shell.openExternal('https://github.com/ethereum/mist/issues');
      }
    }
  );

  menu.push({
    label: i18n.t('mist.applicationMenu.help.label'),
    role: 'help',
    submenu: helpMenu
  });
  return menu;
};

module.exports = createMenu;