Initial copy of files from the riot-web repo
None of this will work as it will need tweaking (at the very least I've not copied the origin migrator because that's had long enough) but these are files which already existed in their current state and so don't need re-reviewing.
25
build/entitlements.mac.plist
Normal file
@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<!-- Entitlements from electron-builder's defaults
|
||||
(https://github.com/electron-userland/electron-builder/blob/master/packages/app-builder-lib/templates/entitlements.mac.plist)
|
||||
nb. This does *not* include the app sandbox: at the time of adding this file,
|
||||
we were using electron-builder 21.2.0 which does not have the sandbox entitlement.
|
||||
Latest electron-builder does, but it appears to be causing issues:
|
||||
(https://github.com/electron-userland/electron-builder/issues/4390)
|
||||
-->
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<!-- https://github.com/electron-userland/electron-builder/issues/3940 -->
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
|
||||
<!-- Our own additional entitlements (we need to access the camera and
|
||||
mic for VoIP calls -->
|
||||
<key>com.apple.security.device.camera</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.audio-input</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
BIN
build/icon.icns
Normal file
BIN
build/icon.ico
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
build/icons/128x128.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
BIN
build/icons/16x16.png
Normal file
After Width: | Height: | Size: 702 B |
BIN
build/icons/24x24.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
build/icons/256x256.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
build/icons/48x48.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
build/icons/512x512.png
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
build/icons/64x64.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
build/icons/96x96.png
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
build/install-spinner.gif
Normal file
After Width: | Height: | Size: 5.4 KiB |
14
build/linux/after-install.tpl
Normal file
@ -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
|
BIN
res/img/riot.ico
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
res/img/riot.png
Normal file
After Width: | Height: | Size: 13 KiB |
34
riot.im/New_Vector_Ltd.pem
Normal file
@ -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-----
|
6
riot.im/README
Normal file
@ -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.
|
40
riot.im/config.json
Normal file
@ -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
|
||||
}
|
||||
}
|
1
riot.im/env.sh
Normal file
@ -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'
|
24
scripts/electron_afterSign.js
Normal file
@ -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',
|
||||
});
|
||||
}
|
||||
};
|
69
scripts/electron_winSign.js
Normal file
@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
626
src/electron-main.js
Normal file
@ -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');
|
20
src/preload.js
Normal file
@ -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;
|
51
src/squirrelhooks.js
Normal file
@ -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;
|
106
src/tray.js
Normal file
@ -0,0 +1,106 @@
|
||||
/*
|
||||
Copyright 2017 Karl Glatz <karl@glatz.biz>
|
||||
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);
|
||||
});
|
||||
};
|
84
src/updater.js
Normal file
@ -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);
|
||||
});
|
139
src/vectormenu.js
Normal file
@ -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);
|
||||
|
196
src/webcontents-handler.js
Normal file
@ -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);
|
||||
}
|
||||
});
|
||||
};
|