Merge branch 'release-v0.17.9'

This commit is contained in:
David Baker 2019-01-22 11:51:01 +00:00
commit 09485f4b9d
24 changed files with 821 additions and 203 deletions

View File

@ -1,3 +1,29 @@
Changes in [0.17.9](https://github.com/vector-im/riot-web/releases/tag/v0.17.9) (2019-01-22)
============================================================================================
[Full Changelog](https://github.com/vector-im/riot-web/compare/v0.17.9-rc.1...v0.17.9)
* Bugfix in react-sdk for setting DM rooms
Changes in [0.17.9-rc.1](https://github.com/vector-im/riot-web/releases/tag/v0.17.9-rc.1) (2019-01-17)
======================================================================================================
[Full Changelog](https://github.com/vector-im/riot-web/compare/v0.17.8...v0.17.9-rc.1)
* Merge develop into experimental
[\#8003](https://github.com/vector-im/riot-web/pull/8003)
* Electron: Load app from custom protocol
[\#7943](https://github.com/vector-im/riot-web/pull/7943)
* Fix the IndexedDB worker
[\#7920](https://github.com/vector-im/riot-web/pull/7920)
* Make clear that the Debian package is for desktop
[\#7919](https://github.com/vector-im/riot-web/pull/7919)
* Run the Desktop app in a sandbox
[\#7907](https://github.com/vector-im/riot-web/pull/7907)
* Update to new electron single instance API
[\#7908](https://github.com/vector-im/riot-web/pull/7908)
* Update the tests to match https://github.com/matrix-org/matrix-react-
sdk/pull/2340
[\#7834](https://github.com/vector-im/riot-web/pull/7834)
Changes in [0.17.8](https://github.com/vector-im/riot-web/releases/tag/v0.17.8) (2018-12-10)
============================================================================================
[Full Changelog](https://github.com/vector-im/riot-web/compare/v0.17.8-rc.1...v0.17.8)

View File

@ -1,4 +1,4 @@
Contributing code to Riot
=========================
Riot follows the same pattern as https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.rst.
Riot follows the same pattern as https://github.com/matrix-org/matrix-js-sdk/blob/master/CONTRIBUTING.rst.

View File

@ -28,7 +28,8 @@ Note that Chrome does not allow microphone or webcam access for sites served
over http (except localhost), so for working VoIP you will need to serve Riot
over https.
### Installation Steps for Debian Stretch
### Desktop Installation for Debian Stretch
1. Add the repository to your sources.list using either of the following two options:
- Directly to sources.list: `echo "deb https://riot.im/packages/debian/ stretch main" | sudo tee -a /etc/apt/sources.list`
- As a separate entry in sources.list.d: `echo "deb https://riot.im/packages/debian/ stretch main" | sudo tee /etc/apt/sources.list.d/riot.list`

View File

@ -2,7 +2,7 @@
"name": "riot-web",
"productName": "Riot",
"main": "src/electron-main.js",
"version": "0.17.8",
"version": "0.17.9",
"description": "A feature-rich client for Matrix.org",
"author": "New Vector Ltd.",
"dependencies": {

View File

@ -2,6 +2,7 @@
Copyright 2016 Aviral Dasgupta
Copyright 2016 OpenMarket Ltd
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2018 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.
@ -23,16 +24,23 @@ const checkSquirrelHooks = require('./squirrelhooks');
if (checkSquirrelHooks()) return;
const argv = require('minimist')(process.argv);
const {app, ipcMain, powerSaveBlocker, BrowserWindow, Menu} = require('electron');
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');
// 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']) {
app.setPath('userData', `${app.getPath('userData')}-${argv['profile']}`);
}
@ -97,27 +105,75 @@ ipcMain.on('app_onAction', function(ev, payload) {
}
});
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 = launcher.isEnabled;
break;
case 'setAutoLaunchEnabled':
if (args[0]) {
launcher.enable();
} else {
launcher.disable();
}
break;
case 'getAppVersion':
ret = app.getVersion();
break;
case 'focusWindow':
if (mainWindow.isMinimized()) {
mainWindow.restore();
} else if (!mainWindow.isVisible()) {
mainWindow.show();
} else {
mainWindow.focus();
}
case 'origin_migrate':
migratingOrigin = true;
await migrateFromOldOrigin();
migratingOrigin = false;
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,
});
});
app.commandLine.appendSwitch('--enable-usermedia-screen-capturing');
const shouldQuit = app.makeSingleInstance((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();
}
});
if (shouldQuit) {
const gotLock = app.requestSingleInstanceLock();
if (!gotLock) {
console.log('Other instance detected: exiting');
app.exit();
}
const launcher = new AutoLaunch({
name: vectorConfig.brand || 'Riot',
isHidden: true,
@ -126,39 +182,12 @@ const launcher = new AutoLaunch({
},
});
const settings = {
'auto-launch': {
get: launcher.isEnabled,
set: function(bool) {
if (bool) {
return launcher.enable();
} else {
return launcher.disable();
}
},
},
};
ipcMain.on('settings_get', async function(ev) {
const data = {};
try {
await Promise.all(Object.keys(settings).map(async function (setting) {
data[setting] = await settings[setting].get();
}));
ev.sender.send('settings', data);
} catch (e) {
console.error(e);
}
});
ipcMain.on('settings_set', function(ev, key, value) {
console.log(key, value);
if (settings[key] && settings[key].set) {
settings[key].set(value);
}
});
// 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.registerStandardSchemes(['vector'], {secure: true});
app.on('ready', () => {
if (argv['devtools']) {
@ -175,6 +204,66 @@ app.on('ready', () => {
}
}
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 (vectorConfig['update_base_url']) {
console.log(`Starting auto update with base URL: ${vectorConfig['update_base_url']}`);
@ -191,6 +280,7 @@ app.on('ready', () => {
defaultHeight: 768,
});
const preloadScript = path.normalize(`${__dirname}/preload.js`);
mainWindow = global.mainWindow = new BrowserWindow({
icon: iconPath,
show: false,
@ -200,8 +290,20 @@ app.on('ready', () => {
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(`file://${__dirname}/../../webapp/index.html`);
mainWindow.loadURL('vector://vector/webapp/');
Menu.setApplicationMenu(vectorMenu);
// explicitly hide because setApplicationMenu on Linux otherwise shows...
@ -268,6 +370,18 @@ app.on('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

View File

@ -0,0 +1,62 @@
/*
Copyright 2018 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 { BrowserWindow, ipcMain } = require('electron');
const path = require('path');
async function migrateFromOldOrigin() {
console.log("Attempting to migrate data between origins");
// We can use the same preload script: we just need ipcRenderer exposed
const preloadScript = path.normalize(`${__dirname}/preload.js`);
await new Promise(resolve => {
const migrateWindow = new BrowserWindow({
show: false,
webPreferences: {
preload: preloadScript,
nodeIntegration: false,
sandbox: true,
enableRemoteModule: false,
webgl: false,
},
});
ipcMain.on('origin_migration_complete', (e, success, sentSummary, storedSummary) => {
if (success) {
console.log("Origin migration completed successfully!");
} else {
console.error("Origin migration failed!");
}
console.error("Data sent", sentSummary);
console.error("Data stored", storedSummary);
migrateWindow.close();
resolve();
});
ipcMain.on('origin_migration_nodata', (e) => {
console.log("No session to migrate from old origin");
migrateWindow.close();
resolve();
});
// Normalise the path because in the distribution, __dirname will be inside the
// electron asar.
const sourcePagePath = path.normalize(__dirname + '/../../origin_migrator/source.html');
console.log("Loading path: " + sourcePagePath);
migrateWindow.loadURL('file://' + sourcePagePath);
});
}
module.exports = {
migrateFromOldOrigin,
};

View File

@ -1,8 +1,5 @@
// @flow
/*
Copyright 2016 Aviral Dasgupta
Copyright 2016 OpenMarket Ltd
Copyright 2018 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.
@ -17,13 +14,16 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
let Platform = null;
const { ipcRenderer, webFrame } = require('electron');
if (window && window.process && window.process && window.process.type === 'renderer') {
// we're running inside electron
Platform = require('./ElectronPlatform');
} else {
Platform = require('./WebPlatform');
}
// expose ipcRenderer to the renderer process
window.ipcRenderer = ipcRenderer;
export default Platform;
// Allow the fetch API to load resources from this
// protocol: this is necessary to load olm.wasm.
// (Also mark it a secure although we've already
// done this in the main process).
webFrame.registerURLSchemeAsPrivileged('vector', {
secure: true,
supportFetchAPI: true,
});

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,6 @@
<html>
<body>
<script src="browser-matrix.min.js"></script>
<script src="dest.js"></script>
</body>
</html>

View File

@ -0,0 +1,125 @@
/*
Copyright 2018 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 SOURCE_ORIGIN = 'file://';
const IndexedDBCryptoStore = window.matrixcs.IndexedDBCryptoStore;
const cryptoStore = new IndexedDBCryptoStore(window.indexedDB, 'matrix-js-sdk:crypto');
let accountStored = 0;
let sessionsStored = 0;
let inboundGroupSessionsStored = 0;
let deviceDataStored = 0;
let roomsStored = 0;
let localStorageKeysStored = 0;
const promises = [];
async function onMessage(e) {
if (e.origin !== SOURCE_ORIGIN) return;
const data = e.data.data; // bleh, naming clash
switch (e.data.cmd) {
case 'init':
// start with clean stores before we migrate data in
window.localStorage.clear();
await cryptoStore.deleteAllData();
e.source.postMessage({
cmd: 'initOK',
}, SOURCE_ORIGIN);
break;
case 'storeAccount':
promises.push(cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
cryptoStore.storeAccount(txn, data);
},
).then(() => {
++accountStored;
}));
break;
case 'storeSessions':
promises.push(cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS],
(txn) => {
for (const sess of data) {
cryptoStore.storeEndToEndSession(sess.deviceKey, sess.sessionId, sess, txn);
}
},
).then(() => {
sessionsStored += data.length;
}));
break;
case 'storeInboundGroupSessions':
promises.push(cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS],
(txn) => {
for (const sess of data) {
cryptoStore.addEndToEndInboundGroupSession(
sess.senderKey, sess.sessionId, sess.sessionData, txn,
);
}
},
).then(() => {
inboundGroupSessionsStored += data.length;
}));
break;
case 'storeDeviceData':
promises.push(cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_DEVICE_DATA],
(txn) => {
cryptoStore.storeEndToEndDeviceData(data, txn);
},
).then(() => {
++deviceDataStored;
}));
break;
case 'storeRooms':
promises.push(cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ROOMS],
(txn) => {
for (const [roomId, roomInfo] of Object.entries(data)) {
cryptoStore.storeEndToEndRoom(roomId, roomInfo, txn);
}
},
).then(() => {
++roomsStored;
}));
break;
case 'storeLocalStorage':
window.localStorage.setItem(data.key, data.val);
++localStorageKeysStored;
break;
case 'getSummary':
await Promise.all(promises);
e.source.postMessage({
cmd: 'summary',
data: {
accountStored,
sessionsStored,
inboundGroupSessionsStored,
deviceDataStored,
roomsStored,
localStorageKeysStored,
},
}, SOURCE_ORIGIN);
break;
}
}
window.addEventListener('message', onMessage);

View File

@ -0,0 +1,7 @@
<html>
<body>
<script src="dest/browser-matrix.min.js"></script>
<script src="source.js"></script>
<iframe name="dest" src="vector://vector/origin_migrator_dest/dest.html" onload="doMigrate()"></iframe>
</body>
</html>

210
origin_migrator/source.js Normal file
View File

@ -0,0 +1,210 @@
/*
Copyright 2018 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 TARGET_ORIGIN = 'vector://vector';
const BATCH_SIZE = 500;
let destFrame;
let initResolver = null;
let getSummaryResolver = null;
function onMessage(e) {
if (e.origin !== TARGET_ORIGIN) return;
if (e.data.cmd === 'initOK' && initResolver) {
initResolver();
initResolver = null;
} else if (e.data.cmd === 'summary' && getSummaryResolver) {
getSummaryResolver(e.data.data);
getSummaryResolver = null;
}
}
async function initDestFrame() {
return new Promise(resolve => {
initResolver = resolve;
destFrame.postMessage({
cmd: 'init',
}, TARGET_ORIGIN);
});
}
async function getSummary() {
return new Promise(resolve => {
getSummaryResolver = resolve;
destFrame.postMessage({
cmd: 'getSummary',
}, TARGET_ORIGIN);
});
}
async function doMigrate() {
let accountSent = 0;
let sessionsSent = 0;
let inboundGroupSessionsSent = 0;
let deviceDataSent = 0;
let roomsSent = 0;
let localStorageKeysSent = 0;
if (!window.ipcRenderer) {
console.error("ipcRenderer not found");
return;
}
if (window.localStorage.getItem('mx_user_id') === null) {
window.ipcRenderer.send("origin_migration_nodata");
return;
}
destFrame = window.parent.frames.dest;
await initDestFrame();
const IndexedDBCryptoStore = window.matrixcs.IndexedDBCryptoStore;
const cryptoStore = new IndexedDBCryptoStore(window.indexedDB, 'matrix-js-sdk:crypto');
await cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
cryptoStore.getAccount(txn, (account) => {
destFrame.postMessage({
cmd: 'storeAccount',
data: account,
}, TARGET_ORIGIN);
++accountSent;
});
},
);
await cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_SESSIONS],
(txn) => {
let sessBatch = [];
cryptoStore.getAllEndToEndSessions(txn, (sessInfo) => {
if (sessInfo) {
++sessionsSent;
sessBatch.push(sessInfo);
}
if (sessBatch.length >= BATCH_SIZE || sessInfo === null) {
destFrame.postMessage({
cmd: 'storeSessions',
data: sessBatch,
}, TARGET_ORIGIN);
sessBatch = [];
}
});
},
);
await cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS],
(txn) => {
let sessBatch = [];
cryptoStore.getAllEndToEndInboundGroupSessions(txn, (sessInfo) => {
if (sessInfo) {
++inboundGroupSessionsSent;
sessBatch.push(sessInfo);
}
if (sessBatch.length >= BATCH_SIZE || sessInfo === null) {
destFrame.postMessage({
cmd: 'storeInboundGroupSessions',
data: sessBatch,
}, TARGET_ORIGIN);
sessBatch = [];
}
});
},
);
await cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_DEVICE_DATA],
(txn) => {
cryptoStore.getEndToEndDeviceData(txn, (deviceData) => {
destFrame.postMessage({
cmd: 'storeDeviceData',
data: deviceData,
}, TARGET_ORIGIN);
++deviceDataSent;
});
},
);
await cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_ROOMS],
(txn) => {
cryptoStore.getEndToEndRooms(txn, (rooms) => {
destFrame.postMessage({
cmd: 'storeRooms',
data: rooms,
}, TARGET_ORIGIN);
++roomsSent;
});
},
);
// we don't bother migrating;
// * sync data (we can just initialsync again)
// * logs
// * key requests (worst case they'll just be re-sent)
// * sessions needing backup (feature isn't available on Electron)
for (let i = 0; i < window.localStorage.length; ++i) {
const key = window.localStorage.key(i);
const val = window.localStorage.getItem(key);
destFrame.postMessage({
cmd: 'storeLocalStorage',
data: { key, val },
}, TARGET_ORIGIN);
++localStorageKeysSent;
}
const summary = await getSummary();
let success = false;
if (
summary.accountStored === accountSent &&
summary.sessionsStored === sessionsSent &&
summary.inboundGroupSessionsStored === inboundGroupSessionsSent &&
summary.deviceDataStored === deviceDataSent &&
summary.roomsStored === roomsSent &&
summary.localStorageKeysStored === localStorageKeysSent
) {
success = true;
window.localStorage.clear();
await cryptoStore.deleteAllData();
// we don't bother migrating them, but also blow away the sync & logs db,
// otherwise they'll just hang about taking up space
await new Promise(resolve => {
const req = window.indexedDB.deleteDatabase('matrix-js-sdk:riot-web-sync');
req.onsuccess = resolve;
req.onerror = resolve;
});
await new Promise(resolve => {
const req = window.indexedDB.deleteDatabase('logs');
req.onsuccess = resolve;
req.onerror = resolve;
});
}
window.ipcRenderer.send("origin_migration_complete", success, {
accountSent, sessionsSent, inboundGroupSessionsSent,
deviceDataSent, roomsSent, localStorageKeysSent,
}, summary);
}
window.addEventListener('message', onMessage);

View File

@ -2,7 +2,7 @@
"name": "riot-web",
"productName": "Riot",
"main": "electron_app/src/electron-main.js",
"version": "0.17.8",
"version": "0.17.9",
"description": "A feature-rich client for Matrix.org",
"author": "New Vector Ltd.",
"repository": {
@ -70,8 +70,8 @@
"gemini-scrollbar": "github:matrix-org/gemini-scrollbar#b302279",
"gfm.css": "^1.1.2",
"highlight.js": "^9.13.1",
"matrix-js-sdk": "0.14.2",
"matrix-react-sdk": "0.14.7",
"matrix-js-sdk": "0.14.3",
"matrix-react-sdk": "0.14.8",
"modernizr": "^3.6.0",
"prop-types": "^15.6.2",
"react": "^15.6.0",
@ -150,14 +150,15 @@
},
"build": {
"appId": "im.riot.app",
"electronVersion": "3.0.10",
"electronVersion": "4.0.1",
"files": [
"node_modules/**",
"src/**",
"img/**"
],
"extraResources": [
"webapp/**/*"
"webapp/**/*",
"origin_migrator/**/*"
],
"linux": {
"target": "deb",

View File

@ -47,4 +47,4 @@ git commit package.json -m "$tag"
cd ..
exec ./node_modules/matrix-js-sdk/release.sh -z "$orig_args"
exec ./node_modules/matrix-js-sdk/release.sh -u vector-im -z "$orig_args"

View File

@ -36,28 +36,10 @@
<body style="height: 100%;">
<section id="matrixchat" style="height: 100%;"></section>
<noscript>Sorry, Riot requires JavaScript to be enabled.</noscript> <!-- TODO: Translate this? -->
<% for (var i=0; i < htmlWebpackPlugin.files.js.length; i++) {
if (_.endsWith(htmlWebpackPlugin.files.js[i], 'olm.js')) {
var array = htmlWebpackPlugin.files.js;
htmlWebpackPlugin.files.js.unshift(htmlWebpackPlugin.files.js[i]);
htmlWebpackPlugin.files.js.splice(i, 1);
}
}
for (var i=0; i < htmlWebpackPlugin.files.js.length; i++) {
// Not a particularly graceful way of not putting the indexeddb worker script
// into the main page
if (_.endsWith(htmlWebpackPlugin.files.js[i], 'indexeddb-worker.js')) {
%>
<script>
window.vector_indexeddb_worker_script = '<%= htmlWebpackPlugin.files.js[i] %>';
</script>
<%
continue;
}
%>
<script src="<%= htmlWebpackPlugin.files.js[i] %>"></script>
<% } %>
<script>
window.vector_indexeddb_worker_script = '<%= htmlWebpackPlugin.files.chunks['indexeddb-worker'].entry %>';
</script>
<script src="<%= htmlWebpackPlugin.files.chunks['bundle'].entry %>"></script>
<img src="img/warning.svg" width="24" height="23" style="visibility: hidden; position: absolute; top: 0px; left: 0px;"/>
<audio id="messageAudio">
<source src="media/message.ogg" type="audio/ogg" />

View File

@ -47,7 +47,9 @@ import * as languageHandler from 'matrix-react-sdk/lib/languageHandler';
import url from 'url';
import {parseQs, parseQsFromFragment} from './url_utils';
import Platform from './platform';
import ElectronPlatform from './platform/ElectronPlatform';
import WebPlatform from './platform/WebPlatform';
import MatrixClientPeg from 'matrix-react-sdk/lib/MatrixClientPeg';
import SettingsStore from "matrix-react-sdk/lib/settings/SettingsStore";
@ -207,6 +209,13 @@ function onTokenLoginCompleted() {
}
async function loadApp() {
if (window.vector_indexeddb_worker_script === undefined) {
// If this is missing, something has probably gone wrong with
// the bundling. The js-sdk will just fall back to accessing
// indexeddb directly with no worker script, but we want to
// make sure the indexeddb script is present, so fail hard.
throw new Error("Missing indexeddb worker script!");
}
MatrixClientPeg.setIndexedDbWorkerScript(window.vector_indexeddb_worker_script);
CallHandler.setConferenceHandler(VectorConferenceHandler);
@ -219,8 +228,23 @@ async function loadApp() {
const fragparts = parseQsFromFragment(window.location);
const params = parseQs(window.location);
// set the platform for react sdk (our Platform object automatically picks the right one)
PlatformPeg.set(new Platform());
// set the platform for react sdk
if (window.ipcRenderer) {
console.log("Using Electron platform");
const plaf = new ElectronPlatform();
PlatformPeg.set(plaf);
// Electron only: see if we need to do a one-time data
// migration
if (window.localStorage.getItem('mx_user_id') === null) {
console.log("Migrating session from old origin...");
await plaf.migrateFromOldOrigin();
console.log("Origin migration complete");
}
} else {
console.log("Using Web platform");
PlatformPeg.set(new WebPlatform());
}
// Load the config file. First try to load up a domain-specific config of the
// form "config.$domain.json" and if that fails, fall back to config.json.

View File

@ -18,4 +18,4 @@ import {IndexedDBStoreWorker} from 'matrix-js-sdk/lib/indexeddb-worker.js';
const remoteWorker = new IndexedDBStoreWorker(postMessage);
export const onmessage = remoteWorker.onMessage;
global.onmessage = remoteWorker.onMessage;

View File

@ -3,6 +3,7 @@
/*
Copyright 2016 Aviral Dasgupta
Copyright 2016 OpenMarket Ltd
Copyright 2018 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.
@ -21,44 +22,26 @@ import VectorBasePlatform, {updateCheckStatusEnum} from './VectorBasePlatform';
import dis from 'matrix-react-sdk/lib/dispatcher';
import { _t } from 'matrix-react-sdk/lib/languageHandler';
import Promise from 'bluebird';
import {remote, ipcRenderer, desktopCapturer} from 'electron';
import rageshake from 'matrix-react-sdk/lib/rageshake/rageshake';
remote.autoUpdater.on('update-downloaded', onUpdateDownloaded);
// try to flush the rageshake logs to indexeddb before quit.
ipcRenderer.on('before-quit', function() {
console.log('riot-desktop closing');
rageshake.flush();
});
function onUpdateDownloaded(ev: Event, releaseNotes: string, ver: string, date: Date, updateURL: string) {
dis.dispatch({
action: 'new_version',
currentVersion: remote.app.getVersion(),
newVersion: ver,
releaseNotes: releaseNotes,
});
}
const ipcRenderer = window.ipcRenderer;
function platformFriendlyName(): string {
console.log(window.process);
switch (window.process.platform) {
case 'darwin':
return 'macOS';
case 'freebsd':
return 'FreeBSD';
case 'openbsd':
return 'OpenBSD';
case 'sunos':
return 'SunOS';
case 'win32':
return 'Windows';
default:
// Sorry, Linux users: you get lumped into here,
// but only because Linux's capitalisation is
// normal. We do care about you.
return window.process.platform[0].toUpperCase() + window.process.platform.slice(1);
// used to use window.process but the same info is available here
if (navigator.userAgent.indexOf('Macintosh')) {
return 'macOS';
} else if (navigator.userAgent.indexOf('FreeBSD')) {
return 'FreeBSD';
} else if (navigator.userAgent.indexOf('OpenBSD')) {
return 'OpenBSD';
} else if (navigator.userAgent.indexOf('SunOS')) {
return 'SunOS';
} else if (navigator.userAgent.indexOf('Windows')) {
return 'Windows';
} else if (navigator.userAgent.indexOf('Linux')) {
return 'Linux';
} else {
return 'Unknown';
}
}
@ -85,9 +68,11 @@ function getUpdateCheckStatus(status) {
export default class ElectronPlatform extends VectorBasePlatform {
constructor() {
super();
dis.register(_onAction);
this.updatable = Boolean(remote.autoUpdater.getFeedURL());
this._pendingIpcCalls = {};
this._nextIpcCallId = 0;
dis.register(_onAction);
/*
IPC Call `check_updates` returns:
true if there is an update available
@ -103,10 +88,28 @@ export default class ElectronPlatform extends VectorBasePlatform {
this.showUpdateCheck = false;
});
// try to flush the rageshake logs to indexeddb before quit.
ipcRenderer.on('before-quit', function() {
console.log('riot-desktop closing');
rageshake.flush();
});
ipcRenderer.on('ipcReply', this._onIpcReply.bind(this));
ipcRenderer.on('update-downloaded', this.onUpdateDownloaded.bind(this));
this.startUpdateCheck = this.startUpdateCheck.bind(this);
this.stopUpdateCheck = this.stopUpdateCheck.bind(this);
}
async onUpdateDownloaded(ev, updateInfo) {
dis.dispatch({
action: 'new_version',
currentVersion: await this.getAppVersion(),
newVersion: updateInfo,
releaseNotes: updateInfo.releaseNotes,
});
}
getHumanReadableName(): string {
return 'Electron Platform'; // no translation required: only used for analytics
}
@ -133,7 +136,7 @@ export default class ElectronPlatform extends VectorBasePlatform {
// maybe we should pass basic styling (italics, bold, underline) through from MD
// we only have to strip out < and > as the spec doesn't include anything about things like &amp;
// so we shouldn't assume that all implementations will treat those properly. Very basic tag parsing is done.
if (window.process.platform === 'linux') {
if (navigator.userAgent.indexOf('Linux')) {
msg = msg.replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
@ -147,17 +150,13 @@ export default class ElectronPlatform extends VectorBasePlatform {
},
);
notification.onclick = function() {
notification.onclick = () => {
dis.dispatch({
action: 'view_room',
room_id: room.roomId,
});
global.focus();
const win = remote.getCurrentWindow();
if (win.isMinimized()) win.restore();
else if (!win.isVisible()) win.show();
else win.focus();
this._ipcCall('focusWindow');
};
return notification;
@ -171,8 +170,25 @@ export default class ElectronPlatform extends VectorBasePlatform {
notif.close();
}
getAppVersion(): Promise<string> {
return Promise.resolve(remote.app.getVersion());
async getAppVersion(): Promise<string> {
return await this._ipcCall('getAppVersion');
}
supportsAutoLaunch() {
return true;
}
async getAutoLaunchEnabled() {
return await this._ipcCall('getAutoLaunchEnabled');
}
async setAutoLaunchEnabled(enabled) {
return await this._ipcCall('setAutoLaunchEnabled', enabled);
}
async canSelfUpdate(): boolean {
const feedUrl = await this._ipcCall('getUpdateFeedUrl');
return Boolean(feedUrl);
}
startUpdateCheck() {
@ -197,52 +213,47 @@ export default class ElectronPlatform extends VectorBasePlatform {
return null;
}
isElectron(): boolean { return true; }
requestNotificationPermission(): Promise<string> {
return Promise.resolve('granted');
}
reload() {
remote.getCurrentWebContents().reload();
// we used to remote to the main process to get it to
// reload the webcontents, but in practice this is unnecessary:
// the normal way works fine.
window.location.reload(false);
}
/* BEGIN copied and slightly-modified code
* setupScreenSharingForIframe function from:
* https://github.com/jitsi/jitsi-meet-electron-utils
* Copied directly here to avoid the need for a native electron module for
* 'just a bit of JavaScript'
* NOTE: Apache v2.0 licensed
*/
setupScreenSharingForIframe(iframe: Object) {
iframe.contentWindow.JitsiMeetElectron = {
/**
* Get sources available for screensharing. The callback is invoked
* with an array of DesktopCapturerSources.
*
* @param {Function} callback - The success callback.
* @param {Function} errorCallback - The callback for errors.
* @param {Object} options - Configuration for getting sources.
* @param {Array} options.types - Specify the desktop source types
* to get, with valid sources being "window" and "screen".
* @param {Object} options.thumbnailSize - Specify how big the
* preview images for the sources should be. The valid keys are
* height and width, e.g. { height: number, width: number}. By
* default electron will return images with height and width of
* 150px.
*/
obtainDesktopStreams(callback, errorCallback, options = {}) {
desktopCapturer.getSources(options,
(error, sources) => {
if (error) {
errorCallback(error);
return;
}
callback(sources);
});
},
};
async migrateFromOldOrigin() {
return this._ipcCall('origin_migrate');
}
async _ipcCall(name, ...args) {
const ipcCallId = ++this._nextIpcCallId;
return new Promise((resolve, reject) => {
this._pendingIpcCalls[ipcCallId] = {resolve, reject};
window.ipcRenderer.send('ipcCall', {id: ipcCallId, name, args});
// Maybe add a timeout to these? Probably not necessary.
});
}
_onIpcReply(ev, payload) {
if (payload.id === undefined) {
console.warn("Ignoring IPC reply with no ID");
return;
}
if (this._pendingIpcCalls[payload.id] === undefined) {
console.warn("Unknown IPC payload ID: " + payload.id);
return;
}
const callbacks = this._pendingIpcCalls[payload.id];
delete this._pendingIpcCalls[payload.id];
if (payload.error) {
callbacks.reject(payload.error);
} else {
callbacks.resolve(payload.reply);
}
}
/* END of copied and slightly-modified code */
}

View File

@ -3,6 +3,7 @@
/*
Copyright 2016 Aviral Dasgupta
Copyright 2016 OpenMarket Ltd
Copyright 2018 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.
@ -45,7 +46,6 @@ export default class VectorBasePlatform extends BasePlatform {
this.favicon = new Favico({animation: 'none'});
this.showUpdateCheck = false;
this._updateFavicon();
this.updatable = true;
this.startUpdateCheck = this.startUpdateCheck.bind(this);
this.stopUpdateCheck = this.stopUpdateCheck.bind(this);
@ -88,6 +88,19 @@ export default class VectorBasePlatform extends BasePlatform {
this._updateFavicon();
}
supportsAutoLaunch() {
return false;
}
// XXX: Surely this should be a setting like any other?
async getAutoLaunchEnabled() {
return false;
}
async setAutoLaunchEnabled(enabled) {
throw new Error("Unimplemented");
}
/**
* Begin update polling, if applicable
*/
@ -97,8 +110,8 @@ export default class VectorBasePlatform extends BasePlatform {
/**
* Whether we can call checkForUpdate on this platform build
*/
canSelfUpdate(): boolean {
return this.updatable;
async canSelfUpdate(): boolean {
return false;
}
startUpdateCheck() {
@ -136,4 +149,12 @@ export default class VectorBasePlatform extends BasePlatform {
getDefaultDeviceDisplayName(): string {
return _t("Unknown device");
}
/**
* Migrate account data from a previous origin
* Used only for the electron app
*/
async migrateFromOldOrigin() {
return false;
}
}

View File

@ -142,6 +142,10 @@ export default class WebPlatform extends VectorBasePlatform {
setInterval(this.pollForUpdate.bind(this), POKE_RATE_MS);
}
async canSelfUpdate(): boolean {
return true;
}
pollForUpdate() {
return this._getVersion().then((ver) => {
if (this.runningVersion === null) {

View File

@ -26,11 +26,13 @@ limitations under the License.
*/
import rageshake from "matrix-react-sdk/lib/rageshake/rageshake";
import SdkConfig from "matrix-react-sdk/src/SdkConfig";
import SdkConfig from "matrix-react-sdk/lib/SdkConfig";
function initRageshake() {
rageshake.init().then(() => {
console.log("Initialised rageshake: See https://bugs.chromium.org/p/chromium/issues/detail?id=583193 to fix line numbers on Chrome.");
console.log("Initialised rageshake.");
console.log("To fix line numbers in Chrome: " +
"Meatball menu → Settings → Blackboxing → Add /rageshake\\.js$");
window.addEventListener('beforeunload', (e) => {
console.log('riot-web closing');

View File

@ -17,7 +17,7 @@ limitations under the License.
/* joining.js: tests for the various paths when joining a room */
import PlatformPeg from 'matrix-react-sdk/lib/PlatformPeg';
import Platform from '../../src/vector/platform';
import WebPlatform from '../../src/vector/platform/WebPlatform';
require('skin-sdk');
@ -88,7 +88,7 @@ describe('joining a room', function() {
localStorage.setItem("mx_access_token", ACCESS_TOKEN );
localStorage.setItem("mx_user_id", USER_ID);
PlatformPeg.set(new Platform());
PlatformPeg.set(new WebPlatform());
const mc = (
<MatrixChat config={{}}

View File

@ -17,7 +17,7 @@ limitations under the License.
/* loading.js: test the myriad paths we have for loading the application */
import PlatformPeg from 'matrix-react-sdk/lib/PlatformPeg';
import Platform from '../../src/vector/platform';
import WebPlatform from '../../src/vector/platform/WebPlatform';
import 'skin-sdk';
@ -43,6 +43,17 @@ import {parseQs, parseQsFromFragment} from '../../src/vector/url_utils';
const DEFAULT_HS_URL='http://my_server';
const DEFAULT_IS_URL='http://my_is';
expect.extend({
toStartWith(prefix) {
expect.assert(
this.actual.startsWith(prefix),
'expected %s to start with %s',
this.actual, prefix,
);
return this;
}
});
describe('loading:', function() {
let parentDiv;
let httpBackend;
@ -140,7 +151,7 @@ describe('loading:', function() {
default_is_url: DEFAULT_IS_URL,
}, opts.config || {});
PlatformPeg.set(new Platform());
PlatformPeg.set(new WebPlatform());
const params = parseQs(windowLocation);
matrixChat = ReactDOM.render(
@ -437,10 +448,7 @@ describe('loading:', function() {
}).done(done, done);
});
it('uses the last known homeserver to register with', function(done) {
localStorage.setItem("mx_hs_url", "https://homeserver" );
localStorage.setItem("mx_is_url", "https://idserver" );
it('uses the default homeserver to register with', function(done) {
loadApp();
Promise.delay(1).then(() => {
@ -449,7 +457,7 @@ describe('loading:', function() {
assertAtLoadingSpinner(matrixChat);
httpBackend.when('POST', '/register').check(function(req) {
expect(req.path).toMatch(new RegExp("^https://homeserver/"));
expect(req.path).toStartWith(DEFAULT_HS_URL);
expect(req.queryParams.kind).toEqual('guest');
}).respond(200, {
user_id: "@guest:localhost",
@ -462,15 +470,15 @@ describe('loading:', function() {
}).then(() => {
return expectAndAwaitSync();
}).then((req) => {
expect(req.path).toMatch(new RegExp("^https://homeserver/"));
expect(req.path).toStartWith(DEFAULT_HS_URL);
// once the sync completes, we should have a home page
httpBackend.verifyNoOutstandingExpectation();
ReactTestUtils.findRenderedComponentWithType(
matrixChat, sdk.getComponent('structures.HomePage'));
expect(windowLocation.hash).toEqual("#/home");
expect(MatrixClientPeg.get().baseUrl).toEqual("https://homeserver");
expect(MatrixClientPeg.get().idBaseUrl).toEqual("https://idserver");
expect(MatrixClientPeg.get().baseUrl).toEqual(DEFAULT_HS_URL);
expect(MatrixClientPeg.get().idBaseUrl).toEqual(DEFAULT_IS_URL);
}).done(done, done);
});

View File

@ -108,11 +108,6 @@ module.exports = {
"matrix-js-sdk": path.resolve('./node_modules/matrix-js-sdk'),
},
},
externals: {
// 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: [
new webpack.DefinePlugin({
'process.env': {