Electron support

From https://github.com/vector-im/vector-web/pull/2511 but with
just the actual electron changes
pull/2535/head
David Baker 2016-11-02 18:49:28 +00:00
parent 8c3fed7559
commit caa3cb7d89
9 changed files with 481 additions and 3 deletions

1
.gitignore vendored
View File

@ -13,3 +13,4 @@
/vector/olm.* /vector/olm.*
.DS_Store .DS_Store
npm-debug.log npm-debug.log
electron/dist

BIN
electron/build/icon.icns Normal file

Binary file not shown.

BIN
electron/build/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -0,0 +1,157 @@
// @flow
/*
Copyright 2016 Aviral Dasgupta and 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 electron = require('electron');
const url = require('url');
const VectorMenu = require('./vectormenu');
const PERMITTED_URL_SCHEMES = [
'http:',
'https:',
'mailto:',
];
const UPDATE_POLL_INTERVAL_MS = 60 * 60 * 1000;
let mainWindow = null;
let appQuitting = false;
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 parsed_url = url.parse(target);
if (PERMITTED_URL_SCHEMES.indexOf(parsed_url.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 new_target = url.format(parsed_url);
electron.shell.openExternal(new_target);
}
}
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) {
const popup_menu = new electron.Menu();
popup_menu.append(new electron.MenuItem({
label: params.linkURL,
click() {
safeOpenURL(params.linkURL);
},
}));
popup_menu.popup();
ev.preventDefault();
}
function installUpdate() {
// for some reason, quitAndInstall does not fire the
// before-quit event, so we need to set the flag here.
appQuitting = true;
electron.autoUpdater.quitAndInstall();
}
function pollForUpdates() {
try {
electron.autoUpdater.checkForUpdates();
} catch (e) {
console.log("Couldn't check for update", e);
}
}
electron.ipcMain.on('install_update', installUpdate);
electron.app.on('ready', () => {
try {
// 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') {
electron.autoUpdater.setFeedURL("https://riot.im/autoupdate/desktop/");
} else if (process.platform == 'win32') {
electron.autoUpdater.setFeedURL("https://riot.im/download/desktop/win32/");
} 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");
}
// 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.
pollForUpdates();
setInterval(pollForUpdates, UPDATE_POLL_INTERVAL_MS);
} catch (err) {
// will fail if running in debug mode
console.log("Couldn't enable update checking", err);
}
mainWindow = new electron.BrowserWindow({
icon: `${__dirname}/../../vector/img/logo.png`,
width: 1024, height: 768,
});
mainWindow.loadURL(`file://${__dirname}/../../vector/index.html`);
electron.Menu.setApplicationMenu(VectorMenu);
mainWindow.on('closed', () => {
mainWindow = null;
});
mainWindow.on('close', (e) => {
if (process.platform == 'darwin' && !appQuitting) {
// 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;
}
});
mainWindow.webContents.on('new-window', onWindowOrNavigate);
mainWindow.webContents.on('will-navigate', onWindowOrNavigate);
mainWindow.webContents.on('context-menu', function(ev, params) {
if (params.linkURL) {
onLinkContextMenu(ev, params);
}
});
});
electron.app.on('window-all-closed', () => {
electron.app.quit();
});
electron.app.on('activate', () => {
mainWindow.show();
});
electron.app.on('before-quit', () => {
appQuitting = true;
});

184
electron/src/vectormenu.js Normal file
View File

@ -0,0 +1,184 @@
/*
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 electron = 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'
},
{
role: 'zoomout'
},
{
type: 'separator'
},
{
role: 'togglefullscreen'
},
{
label: 'Toggle Developer Tools',
accelerator: process.platform == 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I',
click: function(item, focusedWindow) {
if (focusedWindow) focusedWindow.toggleDevTools();
}
}
]
},
{
role: 'window',
submenu: [
{
role: 'minimize'
},
{
role: 'close'
}
]
},
{
role: 'help',
submenu: [
{
label: 'riot.im',
click () { electron.shell.openExternal('https://riot.im/') }
}
]
}
];
if (process.platform === 'darwin') {
const name = electron.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.
template[1].submenu.push(
{
type: 'separator'
},
{
label: 'Speech',
submenu: [
{
role: 'startspeaking'
},
{
role: 'stopspeaking'
}
]
}
)
// Window menu.
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'
}
]
};
module.exports = electron.Menu.buildFromTemplate(template)

View File

@ -1,8 +1,10 @@
{ {
"name": "vector-web", "name": "vector-web",
"productName": "Riot",
"main": "electron/src/electron-main.js",
"version": "0.8.4-rc.2", "version": "0.8.4-rc.2",
"description": "Vector webapp", "description": "A feature-rich client for Matrix.org",
"author": "matrix.org", "author": "Vector Creations Ltd.",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/vector-im/vector-web" "url": "https://github.com/vector-im/vector-web"
@ -31,6 +33,7 @@
"build:compile": "babel --source-maps -d lib src", "build:compile": "babel --source-maps -d lib src",
"build:bundle": "NODE_ENV=production webpack -p --progress", "build:bundle": "NODE_ENV=production webpack -p --progress",
"build:bundle:dev": "webpack --optimize-occurence-order --progress", "build:bundle:dev": "webpack --optimize-occurence-order --progress",
"build:electron": "build -lwm",
"build": "node scripts/babelcheck.js && npm run build:emojione && npm run build:css && npm run build:bundle", "build": "node scripts/babelcheck.js && npm run build:emojione && npm run build:css && npm run build:bundle",
"build:dev": "npm run build:emojione && npm run build:css && npm run build:bundle:dev", "build:dev": "npm run build:emojione && npm run build:css && npm run build:bundle:dev",
"dist": "scripts/package.sh", "dist": "scripts/package.sh",
@ -91,6 +94,7 @@
"catw": "^1.0.1", "catw": "^1.0.1",
"cpx": "^1.3.2", "cpx": "^1.3.2",
"css-raw-loader": "^0.1.1", "css-raw-loader": "^0.1.1",
"electron-builder": "^7.10.2",
"emojione": "^2.2.3", "emojione": "^2.2.3",
"expect": "^1.16.0", "expect": "^1.16.0",
"fs-extra": "^0.30.0", "fs-extra": "^0.30.0",
@ -117,5 +121,24 @@
}, },
"optionalDependencies": { "optionalDependencies": {
"olm": "https://matrix.org/packages/npm/olm/olm-2.0.0.tgz" "olm": "https://matrix.org/packages/npm/olm/olm-2.0.0.tgz"
},
"build": {
"appId": "im.riot.app",
"category": "Network",
"electronVersion": "1.4.2",
"//asar=false": "https://github.com/electron-userland/electron-builder/issues/675",
"asar": false,
"dereference": true,
"//files": "We bundle everything, so we only need to include vector/",
"files": [
"!**/*",
"electron/src/**",
"vector/**",
"package.json"
]
},
"directories": {
"buildResources": "electron/build",
"output": "electron/dist"
} }
} }

View File

@ -0,0 +1,102 @@
// @flow
/*
Copyright 2016 Aviral Dasgupta
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.
*/
import VectorBasePlatform from './VectorBasePlatform';
import dis from 'matrix-react-sdk/lib/dispatcher';
function onUpdateDownloaded(ev, releaseNotes, ver, date, updateURL) {
dis.dispatch({
action: 'new_version',
currentVersion: electron.remote.app.getVersion(),
newVersion: ver,
releaseNotes: releaseNotes,
});
}
// index.js imports us unconditionally, so we need this check here as well
let electron = null, remote = null;
if (window && window.process && window.process && window.process.type === 'renderer') {
electron = require('electron');
electron.remote.autoUpdater.on('update-downloaded', onUpdateDownloaded);
remote = electron.remote;
}
export default class ElectronPlatform extends VectorBasePlatform {
setNotificationCount(count: number) {
super.setNotificationCount(count);
// this sometimes throws because electron is made of fail:
// https://github.com/electron/electron/issues/7351
// For now, let's catch the error, but I suspect it may
// continue to fail and we might just have to accept that
// electron's remote RPC is a non-starter for now and use IPC
try {
remote.app.setBadgeCount(count);
} catch (e) {
console.error("Failed to set notification count", e);
}
}
supportsNotifications() : boolean {
return true;
}
maySendNotifications() : boolean {
return true;
}
displayNotification(title: string, msg: string, avatarUrl: string): Notification {
// Notifications in Electron use the HTML5 notification API
const notification = new global.Notification(
title,
{
body: msg,
icon: avatarUrl,
tag: "vector",
silent: true, // we play our own sounds
}
);
notification.onclick = function() {
dis.dispatch({
action: 'view_room',
room_id: room.roomId
});
global.focus();
};
return notification;
}
clearNotification(notif: Notification) {
notif.close();
}
pollForUpdate() {
// In electron we control the update process ourselves, since
// it needs to run in the main process, so we just run the timer
// loop in the main electron process instead.
}
installUpdate() {
// IPC to the main process to install the update, since quitAndInstall
// doesn't fire the before-quit event so the main process needs to know
// it should exit.
electron.ipcRenderer.send('install_update');
}
}

View File

@ -16,8 +16,16 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import ElectronPlatform from './ElectronPlatform';
import WebPlatform from './WebPlatform'; import WebPlatform from './WebPlatform';
let Platform = WebPlatform; let Platform = null;
if (window && window.process && window.process && window.process.type === 'renderer') {
// we're running inside electron
Platform = ElectronPlatform;
} else {
Platform = WebPlatform;
}
export default Platform; export default Platform;

View File

@ -75,6 +75,9 @@ module.exports = {
}, },
externals: { externals: {
"olm": "Olm", "olm": "Olm",
// Don't try to bundle electron: leave it as a commonjs dependency
// (the 'commonjs' here means it will output a 'require')
"electron": "commonjs electron",
}, },
plugins: [ plugins: [
new webpack.DefinePlugin({ new webpack.DefinePlugin({