diff --git a/scripts/build.mjs b/scripts/build.mjs index 15d090be..d4393d50 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -84,10 +84,11 @@ async function build() { // also creates the directories where the theme css bundles are placed in, // so do it first const themeAssets = await copyThemeAssets(themes, legacy); - const jsBundlePath = await buildJs(); - const jsLegacyBundlePath = await buildJsLegacy(); + const jsBundlePath = await buildJs("src/main.js", `${PROJECT_ID}.js`); + const jsLegacyBundlePath = await buildJsLegacy("src/main.js", `${PROJECT_ID}-legacy.js`); + const jsWorkerPath = await buildWorkerJsLegacy("src/worker.js", `worker.js`); const cssBundlePaths = await buildCssBundles(legacy ? buildCssLegacy : buildCss, themes, themeAssets); - const assetPaths = createAssetPaths(jsBundlePath, jsLegacyBundlePath, cssBundlePaths, themeAssets); + const assetPaths = createAssetPaths(jsBundlePath, jsLegacyBundlePath, jsWorkerPath, cssBundlePaths, themeAssets); let manifestPath; if (offline) { @@ -98,7 +99,7 @@ async function build() { console.log(`built ${PROJECT_ID} ${version} successfully`); } -function createAssetPaths(jsBundlePath, jsLegacyBundlePath, cssBundlePaths, themeAssets) { +function createAssetPaths(jsBundlePath, jsLegacyBundlePath, jsWorkerPath, cssBundlePaths, themeAssets) { function trim(path) { if (!path.startsWith(targetDir)) { throw new Error("invalid target path: " + targetDir); @@ -108,6 +109,7 @@ function createAssetPaths(jsBundlePath, jsLegacyBundlePath, cssBundlePaths, them return { jsBundle: () => trim(jsBundlePath), jsLegacyBundle: () => trim(jsLegacyBundlePath), + jsWorker: () => trim(jsWorkerPath), cssMainBundle: () => trim(cssBundlePaths.main), cssThemeBundle: themeName => trim(cssBundlePaths.themes[themeName]), cssThemeBundles: () => Object.values(cssBundlePaths.themes).map(a => trim(a)), @@ -180,23 +182,24 @@ async function buildHtml(doc, version, assetPaths, manifestPath) { await fs.writeFile(path.join(targetDir, "index.html"), doc.html(), "utf8"); } -async function buildJs() { +async function buildJs(inputFile, outputName) { // create js bundle const bundle = await rollup({ - input: 'src/main.js', + input: inputFile, plugins: [removeJsComments({comments: "none"})] }); const {output} = await bundle.generate({ format: 'es', + // TODO: can remove this? name: `${PROJECT_ID}Bundle` }); const code = output[0].code; - const bundlePath = resource(`${PROJECT_ID}.js`, code); + const bundlePath = resource(outputName, code); await fs.writeFile(bundlePath, code, "utf8"); return bundlePath; } -async function buildJsLegacy() { +async function buildJsLegacy(inputFile, outputName) { // compile down to whatever IE 11 needs const babelPlugin = babel.babel({ babelHelpers: 'bundled', @@ -214,7 +217,7 @@ async function buildJsLegacy() { }); // create js bundle const rollupConfig = { - input: ['src/legacy-polyfill.js', 'src/main.js'], + input: ['src/legacy-polyfill.js', inputFile], plugins: [multi(), commonjs(), nodeResolve(), babelPlugin, removeJsComments({comments: "none"})] }; const bundle = await rollup(rollupConfig); @@ -223,7 +226,39 @@ async function buildJsLegacy() { name: `${PROJECT_ID}Bundle` }); const code = output[0].code; - const bundlePath = resource(`${PROJECT_ID}-legacy.js`, code); + const bundlePath = resource(outputName, code); + await fs.writeFile(bundlePath, code, "utf8"); + return bundlePath; +} + +async function buildWorkerJsLegacy(inputFile, outputName) { + // compile down to whatever IE 11 needs + const babelPlugin = babel.babel({ + babelHelpers: 'bundled', + exclude: 'node_modules/**', + presets: [ + [ + "@babel/preset-env", + { + useBuiltIns: "entry", + corejs: "3", + targets: "IE 11" + } + ] + ] + }); + // create js bundle + const rollupConfig = { + input: ['src/worker-polyfill.js', inputFile], + plugins: [multi(), commonjs(), nodeResolve(), babelPlugin, removeJsComments({comments: "none"})] + }; + const bundle = await rollup(rollupConfig); + const {output} = await bundle.generate({ + format: 'iife', + name: `${PROJECT_ID}Bundle` + }); + const code = output[0].code; + const bundlePath = resource(outputName, code); await fs.writeFile(bundlePath, code, "utf8"); return bundlePath; } diff --git a/src/domain/session/room/timeline/TimelineViewModel.js b/src/domain/session/room/timeline/TimelineViewModel.js index 1527d3ae..16d529cb 100644 --- a/src/domain/session/room/timeline/TimelineViewModel.js +++ b/src/domain/session/room/timeline/TimelineViewModel.js @@ -54,7 +54,7 @@ export class TimelineViewModel extends ViewModel { if (firstTile.shape === "gap") { return firstTile.fill(); } else { - await this._timeline.loadAtTop(50); + await this._timeline.loadAtTop(10); return false; } } diff --git a/src/legacy-polyfill.js b/src/legacy-polyfill.js index 5665158c..a48416c7 100644 --- a/src/legacy-polyfill.js +++ b/src/legacy-polyfill.js @@ -23,4 +23,4 @@ if (!Element.prototype.remove) { Element.prototype.remove = function remove() { this.parentNode.removeChild(this); }; -} \ No newline at end of file +} diff --git a/src/matrix/e2ee/megolm/Decryption.js b/src/matrix/e2ee/megolm/Decryption.js index d4352926..077e9df4 100644 --- a/src/matrix/e2ee/megolm/Decryption.js +++ b/src/matrix/e2ee/megolm/Decryption.js @@ -21,6 +21,7 @@ import {SessionInfo} from "./decryption/SessionInfo.js"; import {DecryptionPreparation} from "./decryption/DecryptionPreparation.js"; import {SessionDecryption} from "./decryption/SessionDecryption.js"; import {SessionCache} from "./decryption/SessionCache.js"; +import {DecryptionWorker} from "./decryption/DecryptionWorker.js"; function getSenderKey(event) { return event.content?.["sender_key"]; @@ -38,7 +39,9 @@ export class Decryption { constructor({pickleKey, olm}) { this._pickleKey = pickleKey; this._olm = olm; - // this._worker = new MessageHandler(new Worker("worker-2580578233.js")); + // this._decryptor = new DecryptionWorker(new Worker("./src/worker.js")); + this._decryptor = new DecryptionWorker(new Worker("worker-3074010154.js")); + this._initPromise = this._decryptor.init(); } createSessionCache(fallback) { @@ -55,6 +58,7 @@ export class Decryption { * @return {DecryptionPreparation} */ async prepareDecryptAll(roomId, events, sessionCache, txn) { + await this._initPromise; const errors = new Map(); const validEvents = []; @@ -85,7 +89,7 @@ export class Decryption { errors.set(event.event_id, new DecryptionError("MEGOLM_NO_SESSION", event)); } } else { - sessionDecryptions.push(new SessionDecryption(sessionInfo, eventsForSession)); + sessionDecryptions.push(new SessionDecryption(sessionInfo, eventsForSession, this._decryptor)); } })); diff --git a/src/matrix/e2ee/megolm/decryption/DecryptionWorker.js b/src/matrix/e2ee/megolm/decryption/DecryptionWorker.js new file mode 100644 index 00000000..df7bb748 --- /dev/null +++ b/src/matrix/e2ee/megolm/decryption/DecryptionWorker.js @@ -0,0 +1,64 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +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. +*/ + +export class DecryptionWorker { + constructor(worker) { + this._worker = worker; + this._requests = new Map(); + this._counter = 0; + this._worker.addEventListener("message", this); + } + + handleEvent(e) { + if (e.type === "message") { + const message = e.data; + console.log("worker reply", message); + const request = this._requests.get(message.replyToId); + if (request) { + if (message.type === "success") { + request.resolve(message.payload); + } else if (message.type === "error") { + request.reject(new Error(message.stack)); + } + this._requests.delete(message.ref_id); + } + } + } + + _send(message) { + this._counter += 1; + message.id = this._counter; + let resolve; + let reject; + const promise = new Promise((_resolve, _reject) => { + resolve = _resolve; + reject = _reject; + }); + this._requests.set(message.id, {reject, resolve}); + this._worker.postMessage(message); + return promise; + } + + decrypt(session, ciphertext) { + const sessionKey = session.export_session(session.first_known_index()); + return this._send({type: "megolm_decrypt", ciphertext, sessionKey}); + } + + init() { + return this._send({type: "load_olm", path: "olm_legacy-3232457086.js"}); + // return this._send({type: "load_olm", path: "../lib/olm/olm_legacy.js"}); + } +} diff --git a/src/matrix/e2ee/megolm/decryption/SessionDecryption.js b/src/matrix/e2ee/megolm/decryption/SessionDecryption.js index 6abda029..5fc32f58 100644 --- a/src/matrix/e2ee/megolm/decryption/SessionDecryption.js +++ b/src/matrix/e2ee/megolm/decryption/SessionDecryption.js @@ -22,10 +22,11 @@ import {ReplayDetectionEntry} from "./ReplayDetectionEntry.js"; * Does the actual decryption of all events for a given megolm session in a batch */ export class SessionDecryption { - constructor(sessionInfo, events) { + constructor(sessionInfo, events, decryptor) { sessionInfo.retain(); this._sessionInfo = sessionInfo; this._events = events; + this._decryptor = decryptor; } async decryptAll() { @@ -38,7 +39,7 @@ export class SessionDecryption { try { const {session} = this._sessionInfo; const ciphertext = event.content.ciphertext; - const {plaintext, message_index: messageIndex} = await this._decrypt(session, ciphertext); + const {plaintext, message_index: messageIndex} = await this._decryptor.decrypt(session, ciphertext); let payload; try { payload = JSON.parse(plaintext); @@ -63,12 +64,6 @@ export class SessionDecryption { return {results, errors, replayEntries}; } - async _decrypt(session, ciphertext) { - // const sessionKey = session.export_session(session.first_known_index()); - // return this._worker.decrypt(sessionKey, ciphertext); - return session.decrypt(ciphertext); - } - dispose() { this._sessionInfo.release(); } diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 7245568d..53c26d82 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -42,7 +42,7 @@ export class Timeline { /** @package */ async load() { - const entries = await this._timelineReader.readFromEnd(50); + const entries = await this._timelineReader.readFromEnd(25); this._remoteEntries.setManySorted(entries); } diff --git a/src/worker-polyfill.js b/src/worker-polyfill.js new file mode 100644 index 00000000..08bbf652 --- /dev/null +++ b/src/worker-polyfill.js @@ -0,0 +1,19 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +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. +*/ + +// polyfills needed for IE11 +import "regenerator-runtime/runtime"; +import "core-js/modules/es.promise"; diff --git a/src/worker.js b/src/worker.js new file mode 100644 index 00000000..e470eaa7 --- /dev/null +++ b/src/worker.js @@ -0,0 +1,108 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +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. +*/ + +function asErrorMessage(err) { + return { + type: "error", + message: err.message, + stack: err.stack + }; +} + +function asSuccessMessage(payload) { + return { + type: "success", + payload + }; +} + +class MessageHandler { + constructor() { + this._olm = null; + } + + handleEvent(e) { + if (e.type === "message") { + this._handleMessage(e.data); + } + } + + _sendReply(refMessage, reply) { + reply.replyToId = refMessage.id; + self.postMessage(reply); + } + + _toMessage(fn) { + try { + let payload = fn(); + if (payload instanceof Promise) { + return payload.then( + payload => asSuccessMessage(payload), + err => asErrorMessage(err) + ); + } else { + return asSuccessMessage(payload); + } + } catch (err) { + return asErrorMessage(err); + } + } + + _loadOlm(path) { + return this._toMessage(async () => { + // might have some problems here with window vs self as global object? + if (self.msCrypto && !self.crypto) { + self.crypto = self.msCrypto; + } + self.importScripts(path); + const olm = self.olm_exports; + // mangle the globals enough to make olm load believe it is running in a browser + self.window = self; + self.document = {}; + await olm.init(); + delete self.document; + delete self.window; + this._olm = olm; + }); + } + + _megolmDecrypt(sessionKey, ciphertext) { + return this._toMessage(() => { + let session; + try { + session = new this._olm.InboundGroupSession(); + session.import_session(sessionKey); + // returns object with plaintext and message_index + return session.decrypt(ciphertext); + } finally { + session?.free(); + } + }); + } + + async _handleMessage(message) { + switch (message.type) { + case "load_olm": + this._sendReply(message, await this._loadOlm(message.path)); + break; + case "megolm_decrypt": + this._sendReply(message, this._megolmDecrypt(message.sessionKey, message.ciphertext)); + break; + } + } +} + +self.addEventListener("message", new MessageHandler());