diff --git a/build/entitlements.mac.plist b/build/entitlements.mac.plist
new file mode 100644
index 0000000..9dd26e6
--- /dev/null
+++ b/build/entitlements.mac.plist
@@ -0,0 +1,25 @@
+
+
+
+
+
+ com.apple.security.cs.allow-unsigned-executable-memory
+
+
+ com.apple.security.cs.disable-library-validation
+
+
+
+ com.apple.security.device.camera
+
+ com.apple.security.device.audio-input
+
+
+
diff --git a/build/icon.icns b/build/icon.icns
new file mode 100644
index 0000000..eba95ec
Binary files /dev/null and b/build/icon.icns differ
diff --git a/build/icon.ico b/build/icon.ico
new file mode 100644
index 0000000..1305b7d
Binary files /dev/null and b/build/icon.ico differ
diff --git a/build/icons/128x128.png b/build/icons/128x128.png
new file mode 100644
index 0000000..9c52d66
Binary files /dev/null and b/build/icons/128x128.png differ
diff --git a/build/icons/16x16.png b/build/icons/16x16.png
new file mode 100644
index 0000000..7435c1b
Binary files /dev/null and b/build/icons/16x16.png differ
diff --git a/build/icons/24x24.png b/build/icons/24x24.png
new file mode 100644
index 0000000..f484995
Binary files /dev/null and b/build/icons/24x24.png differ
diff --git a/build/icons/256x256.png b/build/icons/256x256.png
new file mode 100644
index 0000000..f52d02c
Binary files /dev/null and b/build/icons/256x256.png differ
diff --git a/build/icons/48x48.png b/build/icons/48x48.png
new file mode 100644
index 0000000..fcbcbc4
Binary files /dev/null and b/build/icons/48x48.png differ
diff --git a/build/icons/512x512.png b/build/icons/512x512.png
new file mode 100644
index 0000000..801e150
Binary files /dev/null and b/build/icons/512x512.png differ
diff --git a/build/icons/64x64.png b/build/icons/64x64.png
new file mode 100644
index 0000000..4a86e56
Binary files /dev/null and b/build/icons/64x64.png differ
diff --git a/build/icons/96x96.png b/build/icons/96x96.png
new file mode 100644
index 0000000..8a99898
Binary files /dev/null and b/build/icons/96x96.png differ
diff --git a/build/install-spinner.gif b/build/install-spinner.gif
new file mode 100644
index 0000000..4a68529
Binary files /dev/null and b/build/install-spinner.gif differ
diff --git a/build/linux/after-install.tpl b/build/linux/after-install.tpl
new file mode 100644
index 0000000..6ee772d
--- /dev/null
+++ b/build/linux/after-install.tpl
@@ -0,0 +1,14 @@
+#!/bin/bash
+
+# Link to the binary
+ln -sf '/opt/${productFilename}/${executable}' '/usr/bin/${executable}'
+
+# SUID chrome-sandbox for Electron 5+
+# Remove this entire file (after-install.tpl) and remove the reference in
+# package.json once this change has been upstreamed so we go back to the copy
+# from upstream.
+# https://github.com/electron-userland/electron-builder/pull/4163
+chmod 4755 '/opt/${productFilename}/chrome-sandbox' || true
+
+update-mime-database /usr/share/mime || true
+update-desktop-database /usr/share/applications || true
diff --git a/res/img/riot.ico b/res/img/riot.ico
new file mode 100644
index 0000000..1305b7d
Binary files /dev/null and b/res/img/riot.ico differ
diff --git a/res/img/riot.png b/res/img/riot.png
new file mode 100644
index 0000000..f52d02c
Binary files /dev/null and b/res/img/riot.png differ
diff --git a/riot.im/New_Vector_Ltd.pem b/riot.im/New_Vector_Ltd.pem
new file mode 100644
index 0000000..1a34127
--- /dev/null
+++ b/riot.im/New_Vector_Ltd.pem
@@ -0,0 +1,34 @@
+-----BEGIN CERTIFICATE-----
+MIIF0jCCBLqgAwIBAgIRAISYBqZi3VvCUeSfHXF+cbwwDQYJKoZIhvcNAQELBQAw
+gZExCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO
+BgNVBAcTB1NhbGZvcmQxGjAYBgNVBAoTEUNPTU9ETyBDQSBMaW1pdGVkMTcwNQYD
+VQQDEy5DT01PRE8gUlNBIEV4dGVuZGVkIFZhbGlkYXRpb24gQ29kZSBTaWduaW5n
+IENBMB4XDTE3MDgyMzAwMDAwMFoXDTIwMDgyMjIzNTk1OVowgdgxETAPBgNVBAUT
+CDEwODczNjYxMRMwEQYLKwYBBAGCNzwCAQMTAkdCMR0wGwYDVQQPExRQcml2YXRl
+IE9yZ2FuaXphdGlvbjELMAkGA1UEBhMCR0IxETAPBgNVBBEMCFdDMVIgNEFHMQ8w
+DQYDVQQIDAZMb25kb24xDzANBgNVBAcMBkxvbmRvbjEbMBkGA1UECQwSMjYgUmVk
+IExpb24gU3F1YXJlMRcwFQYDVQQKDA5OZXcgVmVjdG9yIEx0ZDEXMBUGA1UEAwwO
+TmV3IFZlY3RvciBMdGQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC7
+X0HP3oM/SVr6PboD03ndtYTONZDcJ/GJ3EyYi6UNrcbKjuDHwPktx9hjAhNjcVkG
+lmuTEPluPj9DbvjaTrers0cQsAS1vJ0RHjLfA93Flg1ys9Q6OThUMw77FtFPtiJU
+z5cSYzfFAhn/4dv7BcgGptn+Mv/8CaTu+RUZJUgoSlRWcT1TREmxkzWotbblqsHO
+zjDmUg20tL5/qpt6BSWsNespf5udKQFXMtqkczBcLvBLmql0vurVcQy8BibB+Q89
+QKwRzwLgaIa7O8WEssFcW8uJe9s0SNtUy8ehbuoSxpA/DbHFwsiDbNA78vp7HrqM
+qY6t6OIgLtDYBFCfe/btAgMBAAGjggHaMIIB1jAfBgNVHSMEGDAWgBTfj/MgDOnK
+pgTYW1g3Kj2rRtyDSTAdBgNVHQ4EFgQUH+mDOdRkF3bYDxCWEaGB4lxiCxcwDgYD
+VR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwMw
+EQYJYIZIAYb4QgEBBAQDAgQQMEYGA1UdIAQ/MD0wOwYMKwYBBAGyMQECAQYBMCsw
+KQYIKwYBBQUHAgEWHWh0dHBzOi8vc2VjdXJlLmNvbW9kby5jb20vQ1BTMFUGA1Ud
+HwROMEwwSqBIoEaGRGh0dHA6Ly9jcmwuY29tb2RvY2EuY29tL0NPTU9ET1JTQUV4
+dGVuZGVkVmFsaWRhdGlvbkNvZGVTaWduaW5nQ0EuY3JsMIGGBggrBgEFBQcBAQR6
+MHgwUAYIKwYBBQUHMAKGRGh0dHA6Ly9jcnQuY29tb2RvY2EuY29tL0NPTU9ET1JT
+QUV4dGVuZGVkVmFsaWRhdGlvbkNvZGVTaWduaW5nQ0EuY3J0MCQGCCsGAQUFBzAB
+hhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wJgYDVR0RBB8wHaAbBggrBgEFBQcI
+A6APMA0MC0dCLTEwODczNjYxMA0GCSqGSIb3DQEBCwUAA4IBAQBJ2aH4aixh0aiz
+4WKlK+LMVLHpQ2POE3FZYNpAW7o1q2YDGEADXdGrygPE9NCGNBXKo0CAemCYNWfX
+Ov/jdoiMfeqW3vrZ66oEy8OqbvJSwK1xmomWuYw3wYPWcPVG+YbWYD2CGdQu8jTz
+fzAJCpvAuY3Wji3fQjiecAC7JCSB4fBHa0ALJOmiSqKQUUpkXs5kW7O0lPBnHzNF
+2tQGltXMSIrq1QfFtcreMyKlwDOxPIh360dv5aHhaeSRDRKxq7uq5ikQF2gjKx4k
+ieg2HRbAW6fVPpFr4zRS5umpeZV3i06i11VQQPS/mA/OBEXyaqzx4mr6B7U6ptrp
+jMqiUv2w
+-----END CERTIFICATE-----
diff --git a/riot.im/README b/riot.im/README
new file mode 100644
index 0000000..8e463c2
--- /dev/null
+++ b/riot.im/README
@@ -0,0 +1,6 @@
+This directory contains the config file for the official riot.im distribution
+of Riot Desktop.
+
+You probably do not want to build with this config unless you're building the
+official riot.im distribution, or you'll find your builds will replace
+themselves with the riot.im build.
diff --git a/riot.im/config.json b/riot.im/config.json
new file mode 100644
index 0000000..cedb4cc
--- /dev/null
+++ b/riot.im/config.json
@@ -0,0 +1,40 @@
+{
+ "update_base_url": "https://packages.riot.im/desktop/update/",
+ "default_server_name": "matrix.org",
+ "brand": "Riot",
+ "integrations_ui_url": "https://scalar.vector.im/",
+ "integrations_rest_url": "https://scalar.vector.im/api",
+ "integrations_widgets_urls": [
+ "https://scalar.vector.im/_matrix/integrations/v1",
+ "https://scalar.vector.im/api",
+ "https://scalar-staging.vector.im/_matrix/integrations/v1",
+ "https://scalar-staging.vector.im/api",
+ "https://scalar-staging.riot.im/scalar/api"
+ ],
+ "hosting_signup_link": "https://modular.im/?utm_source=riot-web&utm_medium=web",
+ "bug_report_endpoint_url": "https://riot.im/bugreports/submit",
+ "welcomeUserId": "@riot-bot:matrix.org",
+ "roomDirectory": {
+ "servers": [
+ "matrix.org"
+ ]
+ },
+ "piwik": {
+ "url": "https://piwik.riot.im/",
+ "siteId": 1,
+ "policyUrl": "https://matrix.org/docs/guides/riot_im_cookie_policy"
+ },
+ "phasedRollOut": {
+ "feature_lazyloading": {
+ "offset": 1539684000000,
+ "period": 604800000
+ }
+ },
+ "features": {
+ "feature_lazyloading": "enable"
+ },
+ "enable_presence_by_hs_url": {
+ "https://matrix.org": false,
+ "https://matrix-client.matrix.org": false
+ }
+}
diff --git a/riot.im/env.sh b/riot.im/env.sh
new file mode 100644
index 0000000..0ee8105
--- /dev/null
+++ b/riot.im/env.sh
@@ -0,0 +1 @@
+export OSSLSIGNCODE_SIGNARGS='-pkcs11module /Library/Frameworks/eToken.framework/Versions/Current/libeToken.dylib -pkcs11engine /usr/local/lib/engines/engine_pkcs11.so -certs electron_app/riot.im/New_Vector_Ltd.pem -key 0a3271cbc1ec0fd8afb37f6bbe0cd65ba08d3b4d -t http://timestamp.comodoca.com -verbose'
diff --git a/scripts/electron_afterSign.js b/scripts/electron_afterSign.js
new file mode 100644
index 0000000..5952976
--- /dev/null
+++ b/scripts/electron_afterSign.js
@@ -0,0 +1,24 @@
+const { notarize } = require('electron-notarize');
+
+exports.default = async function(context) {
+ const { electronPlatformName, appOutDir } = context;
+
+ if (electronPlatformName === 'darwin') {
+ const appName = context.packager.appInfo.productFilename;
+ // We get the password from keychain. The keychain stores
+ // user IDs too, but apparently altool can't get the user ID
+ // from the keychain, so we need to get it from the environment.
+ const userId = process.env.NOTARIZE_APPLE_ID;
+ if (userId === undefined) {
+ throw new Error("User ID not found. Set NOTARIZE_APPLE_ID.");
+ }
+
+ console.log("Notarising macOS app. This may be some time.");
+ return await notarize({
+ appBundleId: 'im.riot.app',
+ appPath: `${appOutDir}/${appName}.app`,
+ appleId: userId,
+ appleIdPassword: '@keychain:NOTARIZE_CREDS',
+ });
+ }
+};
diff --git a/scripts/electron_winSign.js b/scripts/electron_winSign.js
new file mode 100644
index 0000000..d55b852
--- /dev/null
+++ b/scripts/electron_winSign.js
@@ -0,0 +1,69 @@
+const { exec, execFile } = require('child_process');
+const fs = require('fs');
+const path = require('path');
+const shellescape = require('shell-escape');
+
+exports.default = async function(options) {
+ const inPath = options.path;
+ const appOutDir = path.dirname(inPath);
+
+ // get the token passphrase from the keychain
+ const tokenPassphrase = await new Promise((resolve, reject) => {
+ execFile(
+ 'security',
+ ['find-generic-password', '-s', 'riot_signing_token', '-w'],
+ {},
+ (err, stdout) => {
+ if (err) {
+ console.error("Couldn't find signing token in keychain", err);
+ // electron-builder seems to print '[object Object]' on the
+ // console whether you reject with an Error or a string...
+ reject(err);
+ } else {
+ resolve(stdout.trim());
+ }
+ },
+ );
+ });
+
+ return new Promise((resolve, reject) => {
+ let cmdLine = 'osslsigncode sign ';
+ if (process.env.OSSLSIGNCODE_SIGNARGS) {
+ cmdLine += process.env.OSSLSIGNCODE_SIGNARGS + ' ';
+ }
+ const tmpFile = path.join(
+ appOutDir,
+ 'tmp_' + Math.random().toString(36).substring(2, 15) + '.exe',
+ );
+ const args = [
+ '-h', options.hash,
+ '-pass', tokenPassphrase,
+ '-in', inPath,
+ '-out', tmpFile,
+ ];
+ if (options.isNest) args.push('-nest');
+ cmdLine += shellescape(args);
+
+ let signStdout;
+ const signproc = exec(cmdLine, {}, (error, stdout) => {
+ signStdout = stdout;
+ });
+ signproc.on('exit', (code) => {
+ if (code !== 0) {
+ console.log("Running", cmdLine);
+ console.log(signStdout);
+ console.error("osslsigncode failed with code " + code);
+ reject("osslsigncode failed with code " + code);
+ return;
+ }
+ fs.rename(tmpFile, inPath, (err) => {
+ if (err) {
+ console.error("Error renaming file", err);
+ reject(err);
+ } else {
+ resolve();
+ }
+ });
+ });
+ });
+};
diff --git a/src/electron-main.js b/src/electron-main.js
new file mode 100644
index 0000000..eaedf6b
--- /dev/null
+++ b/src/electron-main.js
@@ -0,0 +1,626 @@
+/*
+Copyright 2016 Aviral Dasgupta
+Copyright 2016 OpenMarket Ltd
+Copyright 2018, 2019 New Vector Ltd
+Copyright 2017, 2019 Michael Telatynski <7t3chguy@gmail.com>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Squirrel on windows starts the app with various flags
+// as hooks to tell us when we've been installed/uninstalled
+// etc.
+const checkSquirrelHooks = require('./squirrelhooks');
+if (checkSquirrelHooks()) return;
+
+const argv = require('minimist')(process.argv, {
+ alias: {help: "h"},
+});
+
+const {app, ipcMain, powerSaveBlocker, BrowserWindow, Menu, autoUpdater, protocol} = require('electron');
+const AutoLaunch = require('auto-launch');
+const path = require('path');
+
+const tray = require('./tray');
+const vectorMenu = require('./vectormenu');
+const webContentsHandler = require('./webcontents-handler');
+const updater = require('./updater');
+const { migrateFromOldOrigin } = require('./originMigrator');
+
+const windowStateKeeper = require('electron-window-state');
+const Store = require('electron-store');
+
+const fs = require('fs');
+const afs = fs.promises;
+
+let Seshat = null;
+
+try {
+ Seshat = require('matrix-seshat');
+} catch (e) {
+ console.warn("seshat unavailable", e);
+}
+
+if (argv["help"]) {
+ console.log("Options:");
+ console.log(" --profile-dir {path}: Path to where to store the profile.");
+ console.log(" --profile {name}: Name of alternate profile to use, allows for running multiple accounts.");
+ console.log(" --devtools: Install and use react-devtools and react-perf.");
+ console.log(" --no-update: Disable automatic updating.");
+ console.log(" --hidden: Start the application hidden in the system tray.");
+ console.log(" --help: Displays this help message.");
+ console.log("And more such as --proxy, see:" +
+ "https://github.com/electron/electron/blob/master/docs/api/chrome-command-line-switches.md");
+ app.exit();
+}
+
+// boolean flag set whilst we are doing one-time origin migration
+// We only serve the origin migration script while we're actually
+// migrating to mitigate any risk of it being used maliciously.
+let migratingOrigin = false;
+
+if (argv['profile-dir']) {
+ app.setPath('userData', argv['profile-dir']);
+} else if (argv['profile']) {
+ app.setPath('userData', `${app.getPath('userData')}-${argv['profile']}`);
+}
+
+let vectorConfig = {};
+try {
+ vectorConfig = require('../../webapp/config.json');
+} catch (e) {
+ // it would be nice to check the error code here and bail if the config
+ // is unparseable, but we get MODULE_NOT_FOUND in the case of a missing
+ // file or invalid json, so node is just very unhelpful.
+ // Continue with the defaults (ie. an empty config)
+}
+
+try {
+ // Load local config and use it to override values from the one baked with the build
+ const localConfig = require(path.join(app.getPath('userData'), 'config.json'));
+
+ // If the local config has a homeserver defined, don't use the homeserver from the build
+ // config. This is to avoid a problem where Riot thinks there are multiple homeservers
+ // defined, and panics as a result.
+ const homeserverProps = ['default_is_url', 'default_hs_url', 'default_server_name', 'default_server_config'];
+ if (Object.keys(localConfig).find(k => homeserverProps.includes(k))) {
+ // Rip out all the homeserver options from the vector config
+ vectorConfig = Object.keys(vectorConfig)
+ .filter(k => !homeserverProps.includes(k))
+ .reduce((obj, key) => {obj[key] = vectorConfig[key]; return obj;}, {});
+ }
+
+ vectorConfig = Object.assign(vectorConfig, localConfig);
+} catch (e) {
+ // Could not load local config, this is expected in most cases.
+}
+
+const eventStorePath = path.join(app.getPath('userData'), 'EventStore');
+const store = new Store({ name: "electron-config" });
+
+let eventIndex = null;
+
+let mainWindow = null;
+global.appQuitting = false;
+
+// It's important to call `path.join` so we don't end up with the packaged asar in the final path.
+const iconFile = `riot.${process.platform === 'win32' ? 'ico' : 'png'}`;
+const iconPath = path.join(__dirname, "..", "..", "img", iconFile);
+const trayConfig = {
+ icon_path: iconPath,
+ brand: vectorConfig.brand || 'Riot',
+};
+
+// handle uncaught errors otherwise it displays
+// stack traces in popup dialogs, which is terrible (which
+// it will do any time the auto update poke fails, and there's
+// no other way to catch this error).
+// Assuming we generally run from the console when developing,
+// this is far preferable.
+process.on('uncaughtException', function(error) {
+ console.log('Unhandled exception', error);
+});
+
+let focusHandlerAttached = false;
+ipcMain.on('setBadgeCount', function(ev, count) {
+ app.setBadgeCount(count);
+ if (count === 0 && mainWindow) {
+ mainWindow.flashFrame(false);
+ }
+});
+
+ipcMain.on('loudNotification', function() {
+ if (process.platform === 'win32' && mainWindow && !mainWindow.isFocused() && !focusHandlerAttached) {
+ mainWindow.flashFrame(true);
+ mainWindow.once('focus', () => {
+ mainWindow.flashFrame(false);
+ focusHandlerAttached = false;
+ });
+ focusHandlerAttached = true;
+ }
+});
+
+let powerSaveBlockerId = null;
+ipcMain.on('app_onAction', function(ev, payload) {
+ switch (payload.action) {
+ case 'call_state':
+ if (powerSaveBlockerId !== null && powerSaveBlocker.isStarted(powerSaveBlockerId)) {
+ if (payload.state === 'ended') {
+ powerSaveBlocker.stop(powerSaveBlockerId);
+ powerSaveBlockerId = null;
+ }
+ } else {
+ if (powerSaveBlockerId === null && payload.state === 'connected') {
+ powerSaveBlockerId = powerSaveBlocker.start('prevent-display-sleep');
+ }
+ }
+ break;
+ }
+});
+
+autoUpdater.on('update-downloaded', (ev, releaseNotes, releaseName, releaseDate, updateURL) => {
+ if (!mainWindow) return;
+ // forward to renderer
+ mainWindow.webContents.send('update-downloaded', {
+ releaseNotes,
+ releaseName,
+ releaseDate,
+ updateURL,
+ });
+});
+
+ipcMain.on('ipcCall', async function(ev, payload) {
+ if (!mainWindow) return;
+
+ const args = payload.args || [];
+ let ret;
+
+ switch (payload.name) {
+ case 'getUpdateFeedUrl':
+ ret = autoUpdater.getFeedURL();
+ break;
+ case 'getAutoLaunchEnabled':
+ ret = await launcher.isEnabled();
+ break;
+ case 'setAutoLaunchEnabled':
+ if (args[0]) {
+ launcher.enable();
+ } else {
+ launcher.disable();
+ }
+ break;
+ case 'getMinimizeToTrayEnabled':
+ ret = tray.hasTray();
+ break;
+ case 'setMinimizeToTrayEnabled':
+ if (args[0]) {
+ // Create trayIcon icon
+ tray.create(trayConfig);
+ } else {
+ tray.destroy();
+ }
+ store.set('minimizeToTray', args[0]);
+ break;
+ case 'getAutoHideMenuBarEnabled':
+ ret = global.mainWindow.isMenuBarAutoHide();
+ break;
+ case 'setAutoHideMenuBarEnabled':
+ store.set('autoHideMenuBar', args[0]);
+ global.mainWindow.setAutoHideMenuBar(args[0]);
+ global.mainWindow.setMenuBarVisibility(!args[0]);
+ break;
+ case 'getAppVersion':
+ ret = app.getVersion();
+ break;
+ case 'focusWindow':
+ if (mainWindow.isMinimized()) {
+ mainWindow.restore();
+ } else if (!mainWindow.isVisible()) {
+ mainWindow.show();
+ } else {
+ mainWindow.focus();
+ }
+ break;
+ case 'origin_migrate':
+ migratingOrigin = true;
+ await migrateFromOldOrigin();
+ migratingOrigin = false;
+ break;
+ case 'getConfig':
+ ret = vectorConfig;
+ break;
+
+ default:
+ mainWindow.webContents.send('ipcReply', {
+ id: payload.id,
+ error: "Unknown IPC Call: " + payload.name,
+ });
+ return;
+ }
+
+ mainWindow.webContents.send('ipcReply', {
+ id: payload.id,
+ reply: ret,
+ });
+});
+
+ipcMain.on('seshat', async function(ev, payload) {
+ if (!mainWindow) return;
+
+ const sendError = (id, e) => {
+ const error = {
+ message: e.message
+ }
+
+ mainWindow.webContents.send('seshatReply', {
+ id:id,
+ error: error
+ });
+ }
+
+ const args = payload.args || [];
+ let ret;
+
+ switch (payload.name) {
+ case 'supportsEventIndexing':
+ if (Seshat === null) ret = false;
+ else ret = true;
+ break;
+
+ case 'initEventIndex':
+ if (eventIndex === null) {
+ try {
+ await afs.mkdir(eventStorePath, {recursive: true});
+ eventIndex = new Seshat(eventStorePath, {passphrase: "DEFAULT_PASSPHRASE"});
+ } catch (e) {
+ sendError(payload.id, e);
+ return;
+ }
+ }
+ break;
+
+ case 'closeEventIndex':
+ eventIndex = null;
+ break;
+
+ case 'deleteEventIndex':
+ const deleteFolderRecursive = async(p) => {
+ for (let entry of await afs.readdir(p)) {
+ const curPath = path.join(p, entry);
+ await afs.unlink(curPath);
+ }
+ }
+
+ try {
+ await deleteFolderRecursive(eventStorePath);
+ } catch (e) {
+ }
+
+ break;
+
+ case 'isEventIndexEmpty':
+ if (eventIndex === null) ret = true;
+ else ret = await eventIndex.isEmpty();
+ break;
+
+ case 'addEventToIndex':
+ try {
+ eventIndex.addEvent(args[0], args[1]);
+ } catch (e) {
+ sendError(payload.id, e);
+ return;
+ }
+ break;
+
+ case 'commitLiveEvents':
+ try {
+ ret = await eventIndex.commit();
+ } catch (e) {
+ sendError(payload.id, e);
+ return;
+ }
+ break;
+
+ case 'searchEventIndex':
+ try {
+ ret = await eventIndex.search(args[0]);
+ } catch (e) {
+ sendError(payload.id, e);
+ return;
+ }
+ break;
+
+ case 'addHistoricEvents':
+ if (eventIndex === null) ret = false;
+ else {
+ try {
+ ret = await eventIndex.addHistoricEvents(
+ args[0], args[1], args[2]);
+ } catch (e) {
+ sendError(payload.id, e);
+ return;
+ }
+ }
+ break;
+
+ case 'removeCrawlerCheckpoint':
+ if (eventIndex === null) ret = false;
+ else {
+ try {
+ ret = await eventIndex.removeCrawlerCheckpoint(args[0]);
+ } catch (e) {
+ sendError(payload.id, e);
+ return;
+ }
+ }
+ break;
+
+ case 'addCrawlerCheckpoint':
+ if (eventIndex === null) ret = false;
+ else {
+ try {
+ ret = await eventIndex.addCrawlerCheckpoint(args[0]);
+ } catch (e) {
+ sendError(payload.id, e);
+ return;
+ }
+ }
+ break;
+
+ case 'loadCheckpoints':
+ if (eventIndex === null) ret = [];
+ else {
+ try {
+ ret = await eventIndex.loadCheckpoints();
+ } catch (e) {
+ ret = [];
+ }
+ }
+ break;
+
+ default:
+ mainWindow.webContents.send('seshatReply', {
+ id: payload.id,
+ error: "Unknown IPC Call: " + payload.name,
+ });
+ return;
+ }
+
+ mainWindow.webContents.send('seshatReply', {
+ id: payload.id,
+ reply: ret,
+ });
+});
+
+app.commandLine.appendSwitch('--enable-usermedia-screen-capturing');
+
+const gotLock = app.requestSingleInstanceLock();
+if (!gotLock) {
+ console.log('Other instance detected: exiting');
+ app.exit();
+}
+
+const launcher = new AutoLaunch({
+ name: vectorConfig.brand || 'Riot',
+ isHidden: true,
+ mac: {
+ useLaunchAgent: true,
+ },
+});
+
+// Register the scheme the app is served from as 'standard'
+// which allows things like relative URLs and IndexedDB to
+// work.
+// Also mark it as secure (ie. accessing resources from this
+// protocol and HTTPS won't trigger mixed content warnings).
+protocol.registerSchemesAsPrivileged([{
+ scheme: 'vector',
+ privileges: {
+ standard: true,
+ secure: true,
+ supportFetchAPI: true,
+ },
+}]);
+
+app.on('ready', () => {
+ if (argv['devtools']) {
+ try {
+ const { default: installExt, REACT_DEVELOPER_TOOLS, REACT_PERF } = require('electron-devtools-installer');
+ installExt(REACT_DEVELOPER_TOOLS)
+ .then((name) => console.log(`Added Extension: ${name}`))
+ .catch((err) => console.log('An error occurred: ', err));
+ installExt(REACT_PERF)
+ .then((name) => console.log(`Added Extension: ${name}`))
+ .catch((err) => console.log('An error occurred: ', err));
+ } catch (e) {
+ console.log(e);
+ }
+ }
+
+ protocol.registerFileProtocol('vector', (request, callback) => {
+ if (request.method !== 'GET') {
+ callback({error: -322}); // METHOD_NOT_SUPPORTED from chromium/src/net/base/net_error_list.h
+ return null;
+ }
+
+ const parsedUrl = new URL(request.url);
+ if (parsedUrl.protocol !== 'vector:') {
+ callback({error: -302}); // UNKNOWN_URL_SCHEME
+ return;
+ }
+ if (parsedUrl.host !== 'vector') {
+ callback({error: -105}); // NAME_NOT_RESOLVED
+ return;
+ }
+
+ const target = parsedUrl.pathname.split('/');
+
+ // path starts with a '/'
+ if (target[0] !== '') {
+ callback({error: -6}); // FILE_NOT_FOUND
+ return;
+ }
+
+ if (target[target.length - 1] == '') {
+ target[target.length - 1] = 'index.html';
+ }
+
+ let baseDir;
+ // first part of the path determines where we serve from
+ if (migratingOrigin && target[1] === 'origin_migrator_dest') {
+ // the origin migrator destination page
+ // (only the destination script needs to come from the
+ // custom protocol: the source part is loaded from a
+ // file:// as that's the origin we're migrating from).
+ baseDir = __dirname + "/../../origin_migrator/dest";
+ } else if (target[1] === 'webapp') {
+ baseDir = __dirname + "/../../webapp";
+ } else {
+ callback({error: -6}); // FILE_NOT_FOUND
+ return;
+ }
+
+ // Normalise the base dir and the target path separately, then make sure
+ // the target path isn't trying to back out beyond its root
+ baseDir = path.normalize(baseDir);
+
+ const relTarget = path.normalize(path.join(...target.slice(2)));
+ if (relTarget.startsWith('..')) {
+ callback({error: -6}); // FILE_NOT_FOUND
+ return;
+ }
+ const absTarget = path.join(baseDir, relTarget);
+
+ callback({
+ path: absTarget,
+ });
+ }, (error) => {
+ if (error) console.error('Failed to register protocol');
+ });
+
+ if (argv['no-update']) {
+ console.log('Auto update disabled via command line flag "--no-update"');
+ } else if (vectorConfig['update_base_url']) {
+ console.log(`Starting auto update with base URL: ${vectorConfig['update_base_url']}`);
+ updater.start(vectorConfig['update_base_url']);
+ } else {
+ console.log('No update_base_url is defined: auto update is disabled');
+ }
+
+ // Load the previous window state with fallback to defaults
+ const mainWindowState = windowStateKeeper({
+ defaultWidth: 1024,
+ defaultHeight: 768,
+ });
+
+ const preloadScript = path.normalize(`${__dirname}/preload.js`);
+ mainWindow = global.mainWindow = new BrowserWindow({
+ icon: iconPath,
+ show: false,
+ autoHideMenuBar: store.get('autoHideMenuBar', true),
+
+ x: mainWindowState.x,
+ y: mainWindowState.y,
+ width: mainWindowState.width,
+ height: mainWindowState.height,
+ webPreferences: {
+ preload: preloadScript,
+ nodeIntegration: false,
+ sandbox: true,
+ enableRemoteModule: false,
+ // We don't use this: it's useful for the preload script to
+ // share a context with the main page so we can give select
+ // objects to the main page. The sandbox option isolates the
+ // main page from the background script.
+ contextIsolation: false,
+ webgl: false,
+ },
+ });
+ mainWindow.loadURL('vector://vector/webapp/');
+ Menu.setApplicationMenu(vectorMenu);
+
+ // Create trayIcon icon
+ if (store.get('minimizeToTray', true)) tray.create(trayConfig);
+
+ mainWindow.once('ready-to-show', () => {
+ mainWindowState.manage(mainWindow);
+
+ if (!argv['hidden']) {
+ mainWindow.show();
+ } else {
+ // hide here explicitly because window manage above sometimes shows it
+ mainWindow.hide();
+ }
+ });
+
+ mainWindow.on('closed', () => {
+ mainWindow = global.mainWindow = null;
+ });
+ mainWindow.on('close', (e) => {
+ // If we are not quitting and have a tray icon then minimize to tray
+ if (!global.appQuitting && (tray.hasTray() || process.platform === 'darwin')) {
+ // On Mac, closing the window just hides it
+ // (this is generally how single-window Mac apps
+ // behave, eg. Mail.app)
+ e.preventDefault();
+ mainWindow.hide();
+ return false;
+ }
+ });
+
+ if (process.platform === 'win32') {
+ // Handle forward/backward mouse buttons in Windows
+ mainWindow.on('app-command', (e, cmd) => {
+ if (cmd === 'browser-backward' && mainWindow.webContents.canGoBack()) {
+ mainWindow.webContents.goBack();
+ } else if (cmd === 'browser-forward' && mainWindow.webContents.canGoForward()) {
+ mainWindow.webContents.goForward();
+ }
+ });
+ }
+
+ webContentsHandler(mainWindow.webContents);
+});
+
+app.on('window-all-closed', () => {
+ app.quit();
+});
+
+app.on('activate', () => {
+ mainWindow.show();
+});
+
+app.on('before-quit', () => {
+ global.appQuitting = true;
+ if (mainWindow) {
+ mainWindow.webContents.send('before-quit');
+ }
+});
+
+app.on('second-instance', (ev, commandLine, workingDirectory) => {
+ // If other instance launched with --hidden then skip showing window
+ if (commandLine.includes('--hidden')) return;
+
+ // Someone tried to run a second instance, we should focus our window.
+ if (mainWindow) {
+ if (!mainWindow.isVisible()) mainWindow.show();
+ if (mainWindow.isMinimized()) mainWindow.restore();
+ mainWindow.focus();
+ }
+});
+
+// Set the App User Model ID to match what the squirrel
+// installer uses for the shortcut icon.
+// This makes notifications work on windows 8.1 (and is
+// a noop on other platforms).
+app.setAppUserModelId('com.squirrel.riot-web.Riot');
diff --git a/src/preload.js b/src/preload.js
new file mode 100644
index 0000000..0862ec6
--- /dev/null
+++ b/src/preload.js
@@ -0,0 +1,20 @@
+/*
+Copyright 2018, 2019 New Vector Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+const { ipcRenderer } = require('electron');
+
+// expose ipcRenderer to the renderer process
+window.ipcRenderer = ipcRenderer;
diff --git a/src/squirrelhooks.js b/src/squirrelhooks.js
new file mode 100644
index 0000000..728c9cf
--- /dev/null
+++ b/src/squirrelhooks.js
@@ -0,0 +1,51 @@
+/*
+Copyright 2017 OpenMarket Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+const path = require('path');
+const spawn = require('child_process').spawn;
+const {app} = require('electron');
+
+function runUpdateExe(args, done) {
+ // Invokes Squirrel's Update.exe which will do things for us like create shortcuts
+ // Note that there's an Update.exe in the app-x.x.x directory and one in the parent
+ // directory: we need to run the one in the parent directory, because it discovers
+ // information about the app by inspecting the directory it's run from.
+ const updateExe = path.resolve(path.dirname(process.execPath), '..', 'Update.exe');
+ console.log(`Spawning '${updateExe}' with args '${args}'`);
+ spawn(updateExe, args, {
+ detached: true,
+ }).on('close', done);
+}
+
+function checkSquirrelHooks() {
+ if (process.platform !== 'win32') return false;
+
+ const cmd = process.argv[1];
+ const target = path.basename(process.execPath);
+ if (cmd === '--squirrel-install' || cmd === '--squirrel-updated') {
+ runUpdateExe(['--createShortcut=' + target + ''], app.quit);
+ return true;
+ } else if (cmd === '--squirrel-uninstall') {
+ runUpdateExe(['--removeShortcut=' + target + ''], app.quit);
+ return true;
+ } else if (cmd === '--squirrel-obsolete') {
+ app.quit();
+ return true;
+ }
+ return false;
+}
+
+module.exports = checkSquirrelHooks;
diff --git a/src/tray.js b/src/tray.js
new file mode 100644
index 0000000..04aaa1f
--- /dev/null
+++ b/src/tray.js
@@ -0,0 +1,106 @@
+/*
+Copyright 2017 Karl Glatz
+Copyright 2017 OpenMarket Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+const {app, Tray, Menu, nativeImage} = require('electron');
+const pngToIco = require('png-to-ico');
+const path = require('path');
+const fs = require('fs');
+
+let trayIcon = null;
+
+exports.hasTray = function hasTray() {
+ return (trayIcon !== null);
+};
+
+exports.destroy = function() {
+ if (trayIcon) {
+ trayIcon.destroy();
+ trayIcon = null;
+ }
+};
+
+exports.create = function(config) {
+ // no trays on darwin
+ if (process.platform === 'darwin' || trayIcon) return;
+
+ const toggleWin = function() {
+ if (global.mainWindow.isVisible() && !global.mainWindow.isMinimized()) {
+ global.mainWindow.hide();
+ } else {
+ if (global.mainWindow.isMinimized()) global.mainWindow.restore();
+ if (!global.mainWindow.isVisible()) global.mainWindow.show();
+ global.mainWindow.focus();
+ }
+ };
+
+ const contextMenu = Menu.buildFromTemplate([
+ {
+ label: `Show/Hide ${config.brand}`,
+ click: toggleWin,
+ },
+ { type: 'separator' },
+ {
+ label: 'Quit',
+ click: function() {
+ app.quit();
+ },
+ },
+ ]);
+
+ const defaultIcon = nativeImage.createFromPath(config.icon_path);
+
+ trayIcon = new Tray(defaultIcon);
+ trayIcon.setToolTip(config.brand);
+ trayIcon.setContextMenu(contextMenu);
+ trayIcon.on('click', toggleWin);
+
+ let lastFavicon = null;
+ global.mainWindow.webContents.on('page-favicon-updated', async function(ev, favicons) {
+ if (!favicons || favicons.length <= 0 || !favicons[0].startsWith('data:')) {
+ if (lastFavicon !== null) {
+ global.mainWindow.setIcon(defaultIcon);
+ trayIcon.setImage(defaultIcon);
+ lastFavicon = null;
+ }
+ return;
+ }
+
+ // No need to change, shortcut
+ if (favicons[0] === lastFavicon) return;
+ lastFavicon = favicons[0];
+
+ let newFavicon = nativeImage.createFromDataURL(favicons[0]);
+
+ // Windows likes ico's too much.
+ if (process.platform === 'win32') {
+ try {
+ const icoPath = path.join(app.getPath('temp'), 'win32_riot_icon.ico');
+ fs.writeFileSync(icoPath, await pngToIco(newFavicon.toPNG()));
+ newFavicon = nativeImage.createFromPath(icoPath);
+ } catch (e) {
+ console.error("Failed to make win32 ico", e);
+ }
+ }
+
+ trayIcon.setImage(newFavicon);
+ global.mainWindow.setIcon(newFavicon);
+ });
+
+ global.mainWindow.webContents.on('page-title-updated', function(ev, title) {
+ trayIcon.setToolTip(title);
+ });
+};
diff --git a/src/updater.js b/src/updater.js
new file mode 100644
index 0000000..49fa4e0
--- /dev/null
+++ b/src/updater.js
@@ -0,0 +1,84 @@
+const { app, autoUpdater, ipcMain } = require('electron');
+
+const UPDATE_POLL_INTERVAL_MS = 60 * 60 * 1000;
+const INITIAL_UPDATE_DELAY_MS = 30 * 1000;
+
+function installUpdate() {
+ // for some reason, quitAndInstall does not fire the
+ // before-quit event, so we need to set the flag here.
+ global.appQuitting = true;
+ autoUpdater.quitAndInstall();
+}
+
+function pollForUpdates() {
+ try {
+ autoUpdater.checkForUpdates();
+ } catch (e) {
+ console.log('Couldn\'t check for update', e);
+ }
+}
+
+module.exports = {};
+module.exports.start = function startAutoUpdate(updateBaseUrl) {
+ if (updateBaseUrl.slice(-1) !== '/') {
+ updateBaseUrl = updateBaseUrl + '/';
+ }
+ try {
+ let url;
+ // For reasons best known to Squirrel, the way it checks for updates
+ // is completely different between macOS and windows. On macOS, it
+ // hits a URL that either gives it a 200 with some json or
+ // 204 No Content. On windows it takes a base path and looks for
+ // files under that path.
+ if (process.platform === 'darwin') {
+ // include the current version in the URL we hit. Electron doesn't add
+ // it anywhere (apart from the User-Agent) so it's up to us. We could
+ // (and previously did) just use the User-Agent, but this doesn't
+ // rely on NSURLConnection setting the User-Agent to what we expect,
+ // and also acts as a convenient cache-buster to ensure that when the
+ // app updates it always gets a fresh value to avoid update-looping.
+ url = `${updateBaseUrl}macos/?localVersion=${encodeURIComponent(app.getVersion())}`;
+
+ } else if (process.platform === 'win32') {
+ url = `${updateBaseUrl}win32/${process.arch}/`;
+ } else {
+ // Squirrel / electron only supports auto-update on these two platforms.
+ // I'm not even going to try to guess which feed style they'd use if they
+ // implemented it on Linux, or if it would be different again.
+ console.log('Auto update not supported on this platform');
+ }
+
+ if (url) {
+ autoUpdater.setFeedURL(url);
+ // We check for updates ourselves rather than using 'updater' because we need to
+ // do it in the main process (and we don't really need to check every 10 minutes:
+ // every hour should be just fine for a desktop app)
+ // However, we still let the main window listen for the update events.
+ // We also wait a short time before checking for updates the first time because
+ // of squirrel on windows and it taking a small amount of time to release a
+ // lock file.
+ setTimeout(pollForUpdates, INITIAL_UPDATE_DELAY_MS);
+ setInterval(pollForUpdates, UPDATE_POLL_INTERVAL_MS);
+ }
+ } catch (err) {
+ // will fail if running in debug mode
+ console.log('Couldn\'t enable update checking', err);
+ }
+}
+
+ipcMain.on('install_update', installUpdate);
+ipcMain.on('check_updates', pollForUpdates);
+
+function ipcChannelSendUpdateStatus(status) {
+ if (global.mainWindow) {
+ global.mainWindow.webContents.send('check_updates', status);
+ }
+}
+
+autoUpdater.on('update-available', function() {
+ ipcChannelSendUpdateStatus(true);
+}).on('update-not-available', function() {
+ ipcChannelSendUpdateStatus(false);
+}).on('error', function(error) {
+ ipcChannelSendUpdateStatus(error.message);
+});
diff --git a/src/vectormenu.js b/src/vectormenu.js
new file mode 100644
index 0000000..a8f998b
--- /dev/null
+++ b/src/vectormenu.js
@@ -0,0 +1,139 @@
+/*
+Copyright 2016 OpenMarket Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+const {app, shell, Menu} = require('electron');
+
+// Menu template from http://electron.atom.io/docs/api/menu/, edited
+const template = [
+ {
+ label: '&Edit',
+ submenu: [
+ { role: 'undo' },
+ { role: 'redo' },
+ { type: 'separator' },
+ { role: 'cut' },
+ { role: 'copy' },
+ { role: 'paste' },
+ { role: 'pasteandmatchstyle' },
+ { role: 'delete' },
+ { role: 'selectall' },
+ ],
+ },
+ {
+ label: '&View',
+ submenu: [
+ { type: 'separator' },
+ { role: 'resetzoom' },
+ { role: 'zoomin', accelerator: 'CommandOrControl+=' },
+ { role: 'zoomout' },
+ { type: 'separator' },
+ { role: 'togglefullscreen' },
+ { role: 'toggledevtools' },
+ ],
+ },
+ {
+ label: '&Window',
+ role: 'window',
+ submenu: [
+ { role: 'minimize' },
+ { role: 'close' },
+ ],
+ },
+ {
+ label: '&Help',
+ role: 'help',
+ submenu: [
+ {
+ label: 'Riot Help',
+ click() { shell.openExternal('https://about.riot.im/help'); },
+ },
+ ],
+ },
+];
+
+// macOS has specific menu conventions...
+if (process.platform === 'darwin') {
+ // first macOS menu is the name of the app
+ const name = app.getName();
+ template.unshift({
+ label: name,
+ submenu: [
+ { role: 'about' },
+ { type: 'separator' },
+ {
+ role: 'services',
+ submenu: [],
+ },
+ { type: 'separator' },
+ { role: 'hide' },
+ { role: 'hideothers' },
+ { role: 'unhide' },
+ { type: 'separator' },
+ { role: 'quit' },
+ ],
+ });
+ // Edit menu.
+ // This has a 'speech' section on macOS
+ template[1].submenu.push(
+ { type: 'separator' },
+ {
+ label: 'Speech',
+ submenu: [
+ { role: 'startspeaking' },
+ { role: 'stopspeaking' },
+ ],
+ });
+
+ // Window menu.
+ // This also has specific functionality on macOS
+ template[3].submenu = [
+ {
+ label: 'Close',
+ accelerator: 'CmdOrCtrl+W',
+ role: 'close',
+ },
+ {
+ label: 'Minimize',
+ accelerator: 'CmdOrCtrl+M',
+ role: 'minimize',
+ },
+ {
+ label: 'Zoom',
+ role: 'zoom',
+ },
+ {
+ type: 'separator',
+ },
+ {
+ label: 'Bring All to Front',
+ role: 'front',
+ },
+ ];
+} else {
+ template.unshift({
+ label: '&File',
+ submenu: [
+ // For some reason, 'about' does not seem to work on windows.
+ /*{
+ role: 'about'
+ },*/
+ { role: 'quit' },
+ ],
+ });
+}
+
+module.exports = Menu.buildFromTemplate(template);
+
diff --git a/src/webcontents-handler.js b/src/webcontents-handler.js
new file mode 100644
index 0000000..2880cf3
--- /dev/null
+++ b/src/webcontents-handler.js
@@ -0,0 +1,196 @@
+const {clipboard, nativeImage, Menu, MenuItem, shell, dialog} = require('electron');
+const url = require('url');
+const fs = require('fs');
+const request = require('request');
+
+const MAILTO_PREFIX = "mailto:";
+
+const PERMITTED_URL_SCHEMES = [
+ 'http:',
+ 'https:',
+ MAILTO_PREFIX,
+];
+
+function safeOpenURL(target) {
+ // openExternal passes the target to open/start/xdg-open,
+ // so put fairly stringent limits on what can be opened
+ // (for instance, open /bin/sh does indeed open a terminal
+ // with a shell, albeit with no arguments)
+ const parsedUrl = url.parse(target);
+ if (PERMITTED_URL_SCHEMES.indexOf(parsedUrl.protocol) > -1) {
+ // explicitly use the URL re-assembled by the url library,
+ // so we know the url parser has understood all the parts
+ // of the input string
+ const newTarget = url.format(parsedUrl);
+ shell.openExternal(newTarget);
+ }
+}
+
+function onWindowOrNavigate(ev, target) {
+ // always prevent the default: if something goes wrong,
+ // we don't want to end up opening it in the electron
+ // app, as we could end up opening any sort of random
+ // url in a window that has node scripting access.
+ ev.preventDefault();
+ safeOpenURL(target);
+}
+
+function onLinkContextMenu(ev, params) {
+ let url = params.linkURL || params.srcURL;
+
+ if (url.startsWith('vector://vector/webapp')) {
+ url = "https://riot.im/app/" + url.substring(23);
+ }
+
+ const popupMenu = new Menu();
+ // No point trying to open blob: URLs in an external browser: it ain't gonna work.
+ if (!url.startsWith('blob:')) {
+ popupMenu.append(new MenuItem({
+ label: url,
+ click() {
+ safeOpenURL(url);
+ },
+ }));
+ }
+
+ let addSaveAs = false;
+ if (params.mediaType && params.mediaType === 'image' && !url.startsWith('file://')) {
+ popupMenu.append(new MenuItem({
+ label: 'Copy image',
+ click() {
+ if (url.startsWith('data:')) {
+ clipboard.writeImage(nativeImage.createFromDataURL(url));
+ } else {
+ ev.sender.copyImageAt(params.x, params.y);
+ }
+ },
+ }));
+
+ // We want the link to be ordered below the copy stuff, but don't want to duplicate
+ // the `if` statement, so use a flag.
+ addSaveAs = true;
+ }
+
+ // No point offering to copy a blob: URL either
+ if (!url.startsWith('blob:')) {
+ // Special-case e-mail URLs to strip the `mailto:` like modern browsers do
+ if (url.startsWith(MAILTO_PREFIX)) {
+ popupMenu.append(new MenuItem({
+ label: 'Copy email address',
+ click() {
+ clipboard.writeText(url.substr(MAILTO_PREFIX.length));
+ },
+ }));
+ } else {
+ popupMenu.append(new MenuItem({
+ label: 'Copy link address',
+ click() {
+ clipboard.writeText(url);
+ },
+ }));
+ }
+ }
+
+ if (addSaveAs) {
+ popupMenu.append(new MenuItem({
+ label: 'Save image as...',
+ click() {
+ const targetFileName = params.titleText || "image.png";
+ const filePath = dialog.showSaveDialog({
+ defaultPath: targetFileName,
+ });
+
+ if (!filePath) return; // user cancelled dialog
+
+ try {
+ if (url.startsWith("data:")) {
+ fs.writeFileSync(filePath, nativeImage.createFromDataURL(url));
+ } else {
+ request.get(url).pipe(fs.createWriteStream(filePath));
+ }
+ } catch (err) {
+ console.error(err);
+ dialog.showMessageBox({
+ type: "error",
+ title: "Failed to save image",
+ message: "The image failed to save",
+ });
+ }
+ },
+ }));
+ }
+
+ // popup() requires an options object even for no options
+ popupMenu.popup({});
+ ev.preventDefault();
+}
+
+function _CutCopyPasteSelectContextMenus(params) {
+ return [{
+ role: 'cut',
+ enabled: params.editFlags.canCut,
+ }, {
+ role: 'copy',
+ enabled: params.editFlags.canCopy,
+ }, {
+ role: 'paste',
+ enabled: params.editFlags.canPaste,
+ }, {
+ role: 'pasteandmatchstyle',
+ enabled: params.editFlags.canPaste,
+ }, {
+ role: 'selectall',
+ enabled: params.editFlags.canSelectAll,
+ }];
+}
+
+function onSelectedContextMenu(ev, params) {
+ const items = _CutCopyPasteSelectContextMenus(params);
+ const popupMenu = Menu.buildFromTemplate(items);
+
+ // popup() requires an options object even for no options
+ popupMenu.popup({});
+ ev.preventDefault();
+}
+
+function onEditableContextMenu(ev, params) {
+ const items = [
+ { role: 'undo' },
+ { role: 'redo', enabled: params.editFlags.canRedo },
+ { type: 'separator' },
+ ].concat(_CutCopyPasteSelectContextMenus(params));
+
+ const popupMenu = Menu.buildFromTemplate(items);
+
+ // popup() requires an options object even for no options
+ popupMenu.popup({});
+ ev.preventDefault();
+}
+
+
+module.exports = (webContents) => {
+ webContents.on('new-window', onWindowOrNavigate);
+ // XXX: The below now does absolutely nothing because of
+ // https://github.com/electron/electron/issues/8841
+ // Whilst this isn't a security issue since without
+ // node integration and with the sandbox, it should be
+ // no worse than opening the site in Chrome, it obviously
+ // means the user has to restart Riot to make it usable
+ // again (often unintuitive because it minimises to the
+ // system tray). We therefore need to be vigilant about
+ // putting target="_blank" on links in Riot (although
+ // we should generally be doing this anyway since links
+ // navigating you away from Riot in the browser is
+ // also annoying).
+ webContents.on('will-navigate', onWindowOrNavigate);
+
+ webContents.on('context-menu', function(ev, params) {
+ if (params.linkURL || params.srcURL) {
+ onLinkContextMenu(ev, params);
+ } else if (params.selectionText) {
+ onSelectedContextMenu(ev, params);
+ } else if (params.isEditable) {
+ onEditableContextMenu(ev, params);
+ }
+ });
+};