From fb69688d47ce5fec14ef29a3e0be3ec9a2682cc0 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 14 Sep 2020 14:01:47 +0200 Subject: [PATCH 01/19] also update room list when encrypted events come in --- src/matrix/room/Room.js | 1 + src/matrix/room/RoomSummary.js | 30 ++++++++++++++++-------------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index 1ea18b4e..b6adb1fc 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -168,6 +168,7 @@ export class Room extends EventEmitter { } const summaryChanges = this._summary.writeSync( roomResponse, + entries, membership, isInitialSync, this._isTimelineOpen, txn); diff --git a/src/matrix/room/RoomSummary.js b/src/matrix/room/RoomSummary.js index 270fa690..711efbf4 100644 --- a/src/matrix/room/RoomSummary.js +++ b/src/matrix/room/RoomSummary.js @@ -16,7 +16,7 @@ limitations under the License. import {MEGOLM_ALGORITHM} from "../e2ee/common.js"; -function applySyncResponse(data, roomResponse, membership, isInitialSync, isTimelineOpen, ownUserId) { +function applySyncResponse(data, roomResponse, timelineEntries, membership, isInitialSync, isTimelineOpen, ownUserId) { if (roomResponse.summary) { data = updateSummary(data, roomResponse.summary); } @@ -31,13 +31,12 @@ function applySyncResponse(data, roomResponse, membership, isInitialSync, isTime if (roomResponse.state) { data = roomResponse.state.events.reduce(processStateEvent, data); } - const {timeline} = roomResponse; - if (timeline && Array.isArray(timeline.events)) { - data = timeline.events.reduce((data, event) => { - if (typeof event.state_key === "string") { - return processStateEvent(data, event); + if (timelineEntries.length) { + data = timelineEntries.reduce((data, entry) => { + if (typeof entry.stateKey === "string") { + return processStateEvent(data, entry.event); } else { - return processTimelineEvent(data, event, + return processTimelineEvent(data, entry, isInitialSync, isTimelineOpen, ownUserId); } }, data); @@ -91,17 +90,19 @@ function processStateEvent(data, event) { return data; } -function processTimelineEvent(data, event, isInitialSync, isTimelineOpen, ownUserId) { - if (event.type === "m.room.message") { +function processTimelineEvent(data, eventEntry, isInitialSync, isTimelineOpen, ownUserId) { + if (eventEntry.eventType === "m.room.message") { + console.log("new message", eventEntry.timestamp, eventEntry.content?.body); data = data.cloneIfNeeded(); - data.lastMessageTimestamp = event.origin_server_ts; - if (!isInitialSync && event.sender !== ownUserId && !isTimelineOpen) { + data.lastMessageTimestamp = eventEntry.timestamp; + if (!isInitialSync && eventEntry.sender !== ownUserId && !isTimelineOpen) { + console.log("also marking unread"); data.isUnread = true; } - const {content} = event; + const {content} = eventEntry; const body = content?.body; const msgtype = content?.msgtype; - if (msgtype === "m.text") { + if (msgtype === "m.text" && !eventEntry.isEncrypted) { data.lastMessageBody = body; } } @@ -267,12 +268,13 @@ export class RoomSummary { return data; } - writeSync(roomResponse, membership, isInitialSync, isTimelineOpen, txn) { + writeSync(roomResponse, timelineEntries, membership, isInitialSync, isTimelineOpen, txn) { // clear cloned flag, so cloneIfNeeded makes a copy and // this._data is not modified if any field is changed. this._data.cloned = false; const data = applySyncResponse( this._data, roomResponse, + timelineEntries, membership, isInitialSync, isTimelineOpen, this._ownUserId); From 0c3ea90ab44aeca6f05b80d914b9250b07b4b005 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 14 Sep 2020 14:01:47 +0200 Subject: [PATCH 02/19] also update room list when encrypted events come in --- src/matrix/room/RoomSummary.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/matrix/room/RoomSummary.js b/src/matrix/room/RoomSummary.js index 711efbf4..b779b13c 100644 --- a/src/matrix/room/RoomSummary.js +++ b/src/matrix/room/RoomSummary.js @@ -92,11 +92,9 @@ function processStateEvent(data, event) { function processTimelineEvent(data, eventEntry, isInitialSync, isTimelineOpen, ownUserId) { if (eventEntry.eventType === "m.room.message") { - console.log("new message", eventEntry.timestamp, eventEntry.content?.body); data = data.cloneIfNeeded(); data.lastMessageTimestamp = eventEntry.timestamp; if (!isInitialSync && eventEntry.sender !== ownUserId && !isTimelineOpen) { - console.log("also marking unread"); data.isUnread = true; } const {content} = eventEntry; From 9fad5b3b29fee4d791b9969ea546ed2377e18fb5 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 14 Sep 2020 14:07:20 +0200 Subject: [PATCH 03/19] only load olm once --- src/main.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main.js b/src/main.js index 8a7cbf0d..97c0b812 100644 --- a/src/main.js +++ b/src/main.js @@ -105,6 +105,7 @@ export async function main(container, paths) { const sessionInfoStorage = new SessionInfoStorage("brawl_sessions_v1"); const storageFactory = new StorageFactory(); + const olmPromise = loadOlm(paths.olm); // if wasm is not supported, we'll want // to run some olm operations in a worker (mainly for IE11) let workerPromise; @@ -121,7 +122,7 @@ export async function main(container, paths) { sessionInfoStorage, request, clock, - olmPromise: loadOlm(paths.olm), + olmPromise, workerPromise, }); }, From a2f8731a2300aace71b4a1014ff43e875e3897f0 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 14 Sep 2020 14:19:35 +0200 Subject: [PATCH 04/19] Keep room key with earliest index --- src/matrix/e2ee/megolm/Decryption.js | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/matrix/e2ee/megolm/Decryption.js b/src/matrix/e2ee/megolm/Decryption.js index b3f1ea71..4d756dcb 100644 --- a/src/matrix/e2ee/megolm/Decryption.js +++ b/src/matrix/e2ee/megolm/Decryption.js @@ -138,12 +138,23 @@ export class Decryption { return; } - // TODO: compare first_known_index to see which session to keep - const hasSession = await txn.inboundGroupSessions.has(roomId, senderKey, sessionId); - if (!hasSession) { - const session = new this._olm.InboundGroupSession(); - try { - session.create(sessionKey); + const session = new this._olm.InboundGroupSession(); + try { + session.create(sessionKey); + + let incomingSessionIsBetter = true; + const existingSessionEntry = await txn.inboundGroupSessions.get(roomId, senderKey, sessionId); + if (existingSessionEntry) { + const existingSession = new this._olm.InboundGroupSession(); + try { + existingSession.unpickle(this._pickleKey, existingSessionEntry.session); + incomingSessionIsBetter = session.first_known_index() < existingSession.first_known_index(); + } finally { + existingSession.free(); + } + } + + if (incomingSessionIsBetter) { const sessionEntry = { roomId, senderKey, @@ -153,9 +164,9 @@ export class Decryption { }; txn.inboundGroupSessions.set(sessionEntry); newSessions.push(sessionEntry); - } finally { - session.free(); } + } finally { + session.free(); } } From 8c4d68def94e238f9a3e9cc06fce484a1ea8243d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 14 Sep 2020 15:43:33 +0200 Subject: [PATCH 05/19] show decryption errors in timeline --- .../room/timeline/tiles/EncryptedEventTile.js | 12 +++++++++++- src/matrix/room/timeline/entries/EventEntry.js | 4 ++++ src/ui/web/css/themes/element/theme.css | 5 +++++ src/ui/web/session/room/TimelineList.js | 4 +++- src/ui/web/session/room/timeline/common.js | 1 + 5 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/domain/session/room/timeline/tiles/EncryptedEventTile.js b/src/domain/session/room/timeline/tiles/EncryptedEventTile.js index 99c8a291..bc4f8feb 100644 --- a/src/domain/session/room/timeline/tiles/EncryptedEventTile.js +++ b/src/domain/session/room/timeline/tiles/EncryptedEventTile.js @@ -28,7 +28,17 @@ export class EncryptedEventTile extends MessageTile { } } + get shape() { + return "message-status" + } + get text() { - return this.i18n`**Encrypted message**`; + const decryptionError = this._entry.decryptionError; + const code = decryptionError?.code; + if (code === "MEGOLM_NO_SESSION") { + return this.i18n`The sender hasn't sent us the key for this message yet.`; + } else { + return decryptionError?.message || this.i18n`"Could not decrypt message because of unknown reason."`; + } } } diff --git a/src/matrix/room/timeline/entries/EventEntry.js b/src/matrix/room/timeline/entries/EventEntry.js index 8c7029d4..81e31112 100644 --- a/src/matrix/room/timeline/entries/EventEntry.js +++ b/src/matrix/room/timeline/entries/EventEntry.js @@ -93,4 +93,8 @@ export class EventEntry extends BaseEntry { setDecryptionError(err) { this._decryptionError = err; } + + get decryptionError() { + return this._decryptionError; + } } diff --git a/src/ui/web/css/themes/element/theme.css b/src/ui/web/css/themes/element/theme.css index bdcd599f..a669d3f5 100644 --- a/src/ui/web/css/themes/element/theme.css +++ b/src/ui/web/css/themes/element/theme.css @@ -327,6 +327,11 @@ ul.Timeline > li.continuation time { display: none; } +ul.Timeline > li.messageStatus .message-container > p { + font-style: italic; + color: #777; +} + .message-container { padding: 1px 10px 0px 10px; margin: 5px 10px 0 10px; diff --git a/src/ui/web/session/room/TimelineList.js b/src/ui/web/session/room/TimelineList.js index b43fcc27..2072b453 100644 --- a/src/ui/web/session/room/TimelineList.js +++ b/src/ui/web/session/room/TimelineList.js @@ -30,7 +30,9 @@ export class TimelineList extends ListView { switch (entry.shape) { case "gap": return new GapView(entry); case "announcement": return new AnnouncementView(entry); - case "message": return new TextMessageView(entry); + case "message": + case "message-status": + return new TextMessageView(entry); case "image": return new ImageView(entry); } }); diff --git a/src/ui/web/session/room/timeline/common.js b/src/ui/web/session/room/timeline/common.js index 7869731f..50b6a0cd 100644 --- a/src/ui/web/session/room/timeline/common.js +++ b/src/ui/web/session/room/timeline/common.js @@ -24,6 +24,7 @@ export function renderMessage(t, vm, children) { pending: vm.isPending, unverified: vm.isUnverified, continuation: vm => vm.isContinuation, + messageStatus: vm => vm.shape === "message-status", }; const profile = t.div({className: "profile"}, [ From aa5d55bbf28a5b21ce8c1f9ff1f8a481c14e54ce Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 14 Sep 2020 15:44:07 +0200 Subject: [PATCH 06/19] show when e2ee is enabled in timeline --- .../timeline/tiles/EncryptionEnabledTile.js | 28 +++++++++++++++++++ .../session/room/timeline/tilesCreator.js | 3 ++ 2 files changed, 31 insertions(+) create mode 100644 src/domain/session/room/timeline/tiles/EncryptionEnabledTile.js diff --git a/src/domain/session/room/timeline/tiles/EncryptionEnabledTile.js b/src/domain/session/room/timeline/tiles/EncryptionEnabledTile.js new file mode 100644 index 00000000..00bc6737 --- /dev/null +++ b/src/domain/session/room/timeline/tiles/EncryptionEnabledTile.js @@ -0,0 +1,28 @@ +/* +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. +*/ + +import {SimpleTile} from "./SimpleTile.js"; + +export class EncryptionEnabledTile extends SimpleTile { + get shape() { + return "announcement"; + } + + get announcement() { + const senderName = this._entry.displayName || this._entry.sender; + return this.i18n`${senderName} has enabled end-to-end encryption`; + } +} diff --git a/src/domain/session/room/timeline/tilesCreator.js b/src/domain/session/room/timeline/tilesCreator.js index 567e9e7d..5f5593d5 100644 --- a/src/domain/session/room/timeline/tilesCreator.js +++ b/src/domain/session/room/timeline/tilesCreator.js @@ -21,6 +21,7 @@ import {LocationTile} from "./tiles/LocationTile.js"; import {RoomNameTile} from "./tiles/RoomNameTile.js"; import {RoomMemberTile} from "./tiles/RoomMemberTile.js"; import {EncryptedEventTile} from "./tiles/EncryptedEventTile.js"; +import {EncryptionEnabledTile} from "./tiles/EncryptionEnabledTile.js"; export function tilesCreator({room, ownUserId, clock}) { return function tilesCreator(entry, emitUpdate) { @@ -53,6 +54,8 @@ export function tilesCreator({room, ownUserId, clock}) { return new RoomMemberTile(options); case "m.room.encrypted": return new EncryptedEventTile(options); + case "m.room.encryption": + return new EncryptionEnabledTile(options); default: // unknown type not rendered return null; From 8555fd5f186934caebb66e8f929d67dd72ec9b68 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 14 Sep 2020 15:44:18 +0200 Subject: [PATCH 07/19] a little extra caution --- src/domain/session/room/timeline/tiles/RoomNameTile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/session/room/timeline/tiles/RoomNameTile.js b/src/domain/session/room/timeline/tiles/RoomNameTile.js index a7a785d0..d5694a62 100644 --- a/src/domain/session/room/timeline/tiles/RoomNameTile.js +++ b/src/domain/session/room/timeline/tiles/RoomNameTile.js @@ -24,6 +24,6 @@ export class RoomNameTile extends SimpleTile { get announcement() { const content = this._entry.content; - return `${this._entry.displayName || this._entry.sender} named the room "${content.name}"` + return `${this._entry.displayName || this._entry.sender} named the room "${content?.name}"` } } From 504371eaf3839f628ae2bf9deca33d985d834347 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 14 Sep 2020 15:44:34 +0200 Subject: [PATCH 08/19] this is outdated (and not really needed) --- src/matrix/storage/idb/Transaction.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/matrix/storage/idb/Transaction.js b/src/matrix/storage/idb/Transaction.js index af6d49ca..d28d802f 100644 --- a/src/matrix/storage/idb/Transaction.js +++ b/src/matrix/storage/idb/Transaction.js @@ -36,12 +36,7 @@ export class Transaction { constructor(txn, allowedStoreNames) { this._txn = txn; this._allowedStoreNames = allowedStoreNames; - this._stores = { - session: null, - roomSummary: null, - roomTimeline: null, - roomState: null, - }; + this._stores = {}; } _idbStore(name) { From 3325f1209265934e8061420ff99df6f861a928cd Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 14 Sep 2020 15:44:47 +0200 Subject: [PATCH 09/19] remove devices not present in /keys/query response --- src/matrix/e2ee/DeviceTracker.js | 67 ++++++++++++------- .../storage/idb/stores/DeviceIdentityStore.js | 24 +++++++ 2 files changed, 68 insertions(+), 23 deletions(-) diff --git a/src/matrix/e2ee/DeviceTracker.js b/src/matrix/e2ee/DeviceTracker.js index aef62e10..10eeb51b 100644 --- a/src/matrix/e2ee/DeviceTracker.js +++ b/src/matrix/e2ee/DeviceTracker.js @@ -149,36 +149,17 @@ export class DeviceTracker { }).response(); const verifiedKeysPerUser = this._filterVerifiedDeviceKeys(deviceKeyResponse["device_keys"]); - const flattenedVerifiedKeysPerUser = verifiedKeysPerUser.reduce((all, {verifiedKeys}) => all.concat(verifiedKeys), []); - const deviceIdentitiesWithPossibleChangedKeys = flattenedVerifiedKeysPerUser.map(deviceKeysAsDeviceIdentity); - const txn = await this._storage.readWriteTxn([ this._storage.storeNames.userIdentities, this._storage.storeNames.deviceIdentities, ]); let deviceIdentities; try { - // check ed25519 key has not changed if we've seen the device before - deviceIdentities = await Promise.all(deviceIdentitiesWithPossibleChangedKeys.map(async (deviceIdentity) => { - const existingDevice = await txn.deviceIdentities.get(deviceIdentity.userId, deviceIdentity.deviceId); - if (!existingDevice || existingDevice.ed25519Key === deviceIdentity.ed25519Key) { - return deviceIdentity; - } - // ignore devices where the keys have changed - return null; - })); - // filter out nulls - deviceIdentities = deviceIdentities.filter(di => !!di); - // store devices - for (const deviceIdentity of deviceIdentities) { - txn.deviceIdentities.set(deviceIdentity); - } - // mark user identities as up to date - await Promise.all(verifiedKeysPerUser.map(async ({userId}) => { - const identity = await txn.userIdentities.get(userId); - identity.deviceTrackingStatus = TRACKING_STATUS_UPTODATE; - txn.userIdentities.set(identity); + const devicesIdentitiesPerUser = await Promise.all(verifiedKeysPerUser.map(async ({userId, verifiedKeys}) => { + const deviceIdentities = verifiedKeys.map(deviceKeysAsDeviceIdentity); + return await this._storeQueriedDevicesForUserId(userId, deviceIdentities, txn); })); + deviceIdentities = devicesIdentitiesPerUser.reduce((all, devices) => all.concat(devices), []); } catch (err) { txn.abort(); throw err; @@ -187,6 +168,46 @@ export class DeviceTracker { return deviceIdentities; } + async _storeQueriedDevicesForUserId(userId, deviceIdentities, txn) { + const knownDeviceIds = await txn.deviceIdentities.getAllForUserId(userId); + // delete any devices that we know off but are not in the response anymore. + // important this happens before checking if the ed25519 key changed, + // otherwise we would end up deleting existing devices with changed keys. + for (const deviceId of knownDeviceIds) { + if (deviceIdentities.every(di => di.deviceId !== deviceId)) { + txn.deviceIdentities.remove(userId, deviceId); + } + } + + // all the device identities as we will have them in storage + const allDeviceIdentities = []; + const deviceIdentitiesToStore = []; + // filter out devices that have changed their ed25519 key since last time we queried them + deviceIdentities = await Promise.all(deviceIdentities.map(async deviceIdentity => { + if (knownDeviceIds.includes(deviceIdentity.deviceId)) { + const existingDevice = await txn.deviceIdentities.get(deviceIdentity.userId, deviceIdentity.deviceId); + if (existingDevice.ed25519Key !== deviceIdentity.ed25519Key) { + allDeviceIdentities.push(existingDevice); + } + } + allDeviceIdentities.push(deviceIdentity); + deviceIdentitiesToStore.push(deviceIdentity); + })); + // store devices + for (const deviceIdentity of deviceIdentitiesToStore) { + txn.deviceIdentities.set(deviceIdentity); + } + // mark user identities as up to date + const identity = await txn.userIdentities.get(userId); + identity.deviceTrackingStatus = TRACKING_STATUS_UPTODATE; + txn.userIdentities.set(identity); + + return allDeviceIdentities; + } + + /** + * @return {Array<{userId, verifiedKeys: Array>} + */ _filterVerifiedDeviceKeys(keyQueryDeviceKeysResponse) { const curve25519Keys = new Set(); const verifiedKeys = Object.entries(keyQueryDeviceKeysResponse).map(([userId, keysByDevice]) => { diff --git a/src/matrix/storage/idb/stores/DeviceIdentityStore.js b/src/matrix/storage/idb/stores/DeviceIdentityStore.js index d3aba963..4d209532 100644 --- a/src/matrix/storage/idb/stores/DeviceIdentityStore.js +++ b/src/matrix/storage/idb/stores/DeviceIdentityStore.js @@ -18,6 +18,11 @@ function encodeKey(userId, deviceId) { return `${userId}|${deviceId}`; } +function decodeKey(key) { + const [userId, deviceId] = key.split("|"); + return {userId, deviceId}; +} + export class DeviceIdentityStore { constructor(store) { this._store = store; @@ -30,6 +35,21 @@ export class DeviceIdentityStore { }); } + async getAllDeviceIds(userId) { + const deviceIds = []; + const range = IDBKeyRange.lowerBound(encodeKey(userId, "")); + await this._store.iterateKeys(range, key => { + const decodedKey = decodeKey(key); + // prevent running into the next room + if (decodedKey.userId === userId) { + deviceIds.push(decodedKey.deviceId); + return false; // fetch more + } + return true; // done + }); + return deviceIds; + } + get(userId, deviceId) { return this._store.get(encodeKey(userId, deviceId)); } @@ -42,4 +62,8 @@ export class DeviceIdentityStore { getByCurve25519Key(curve25519Key) { return this._store.index("byCurve25519Key").get(curve25519Key); } + + remove(userId, deviceId) { + this._store.delete(encodeKey(userId, deviceId)); + } } From bce46daa9c964e65a5104f4e89d41cfc81566d95 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 14 Sep 2020 16:33:30 +0200 Subject: [PATCH 10/19] this is shorter --- src/matrix/room/Room.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index b6adb1fc..3779f1c3 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -68,9 +68,8 @@ export class Room extends EventEmitter { } const decryptRequest = this._decryptEntries(DecryptionSource.Retry, retryEntries, txn); await decryptRequest.complete(); - if (this._timeline) { - // only adds if already present - this._timeline.replaceEntries(retryEntries); + + this._timeline?.replaceEntries(retryEntries); } // pass decryptedEntries to roomSummary } From f3f07a0672168ecd33b6c666a979f25346e4092d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 14 Sep 2020 16:33:43 +0200 Subject: [PATCH 11/19] centralize update emitting in room --- src/matrix/room/Room.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index 3779f1c3..3a8e7c5d 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -231,8 +231,7 @@ export class Room extends EventEmitter { } } if (emitChange) { - this.emit("change"); - this._emitCollectionChange(this); + this._emitUpdate(); } if (this._timeline) { this._timeline.appendLiveEntries(newTimelineEntries); @@ -442,6 +441,13 @@ export class Room extends EventEmitter { return !!this._timeline; } + _emitUpdate() { + // once for event emitter listeners + this.emit("change"); + // and once for collection listeners + this._emitCollectionChange(this); + } + async clearUnread() { if (this.isUnread || this.notificationCount) { const txn = await this._storage.readWriteTxn([ @@ -456,8 +462,7 @@ export class Room extends EventEmitter { } await txn.complete(); this._summary.applyChanges(data); - this.emit("change"); - this._emitCollectionChange(this); + this._emitUpdate(); try { const lastEventId = await this._getLastEventId(); From 49f330279bfc26370254fa7008edaad2452b55a4 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 14 Sep 2020 16:34:07 +0200 Subject: [PATCH 12/19] also pass timeline entries to summary after initial decryption failed --- src/matrix/room/Room.js | 9 +++- src/matrix/room/RoomSummary.js | 82 ++++++++++++++++++++++++++++------ 2 files changed, 77 insertions(+), 14 deletions(-) diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index 3a8e7c5d..69037c88 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -70,8 +70,15 @@ export class Room extends EventEmitter { await decryptRequest.complete(); this._timeline?.replaceEntries(retryEntries); + // we would ideally write the room summary in the same txn as the groupSessionDecryptions in the + // _decryptEntries entries and could even know which events have been decrypted for the first + // time from DecryptionChanges.write and only pass those to the summary. As timeline changes + // are not essential to the room summary, it's fine to write this in a separate txn for now. + const changes = this._summary.processTimelineEntries(retryEntries, false, this._isTimelineOpen); + if (changes) { + this._summary.writeAndApplyChanges(changes, this._storage); + this._emitUpdate(); } - // pass decryptedEntries to roomSummary } } } diff --git a/src/matrix/room/RoomSummary.js b/src/matrix/room/RoomSummary.js index b779b13c..b5220468 100644 --- a/src/matrix/room/RoomSummary.js +++ b/src/matrix/room/RoomSummary.js @@ -16,7 +16,19 @@ limitations under the License. import {MEGOLM_ALGORITHM} from "../e2ee/common.js"; -function applySyncResponse(data, roomResponse, timelineEntries, membership, isInitialSync, isTimelineOpen, ownUserId) { + +function applyTimelineEntries(data, timelineEntries, isInitialSync, isTimelineOpen, ownUserId) { + if (timelineEntries.length) { + data = timelineEntries.reduce((data, entry) => { + return processTimelineEvent(data, entry, + isInitialSync, isTimelineOpen, ownUserId); + }, data); + } + return data; +} + + +function applySyncResponse(data, roomResponse, membership) { if (roomResponse.summary) { data = updateSummary(data, roomResponse.summary); } @@ -31,13 +43,14 @@ function applySyncResponse(data, roomResponse, timelineEntries, membership, isIn if (roomResponse.state) { data = roomResponse.state.events.reduce(processStateEvent, data); } - if (timelineEntries.length) { - data = timelineEntries.reduce((data, entry) => { - if (typeof entry.stateKey === "string") { - return processStateEvent(data, entry.event); - } else { - return processTimelineEvent(data, entry, - isInitialSync, isTimelineOpen, ownUserId); + const {timeline} = roomResponse; + // process state events in timeline + // non-state events are handled by applyTimelineEntries + // so decryption is handled properly + if (timeline && Array.isArray(timeline.events)) { + data = timeline.events.reduce((data, event) => { + if (typeof event.state_key === "string") { + return processStateEvent(data, event); } }, data); } @@ -92,15 +105,19 @@ function processStateEvent(data, event) { function processTimelineEvent(data, eventEntry, isInitialSync, isTimelineOpen, ownUserId) { if (eventEntry.eventType === "m.room.message") { - data = data.cloneIfNeeded(); - data.lastMessageTimestamp = eventEntry.timestamp; + if (!data.lastMessageTimestamp || eventEntry.timestamp > data.lastMessageTimestamp) { + data = data.cloneIfNeeded(); + data.lastMessageTimestamp = eventEntry.timestamp; + } if (!isInitialSync && eventEntry.sender !== ownUserId && !isTimelineOpen) { + data = data.cloneIfNeeded(); data.isUnread = true; } const {content} = eventEntry; const body = content?.body; const msgtype = content?.msgtype; if (msgtype === "m.text" && !eventEntry.isEncrypted) { + data = data.cloneIfNeeded(); data.lastMessageBody = body; } } @@ -266,14 +283,34 @@ export class RoomSummary { return data; } + /** + * after retrying decryption + */ + processTimelineEntries(timelineEntries, isInitialSync, isTimelineOpen) { + // clear cloned flag, so cloneIfNeeded makes a copy and + // this._data is not modified if any field is changed. + + processTimelineEvent + + this._data.cloned = false; + const data = applyTimelineEntries( + this._data, + timelineEntries, + isInitialSync, isTimelineOpen, + this._ownUserId); + if (data !== this._data) { + return data; + } + } + writeSync(roomResponse, timelineEntries, membership, isInitialSync, isTimelineOpen, txn) { // clear cloned flag, so cloneIfNeeded makes a copy and // this._data is not modified if any field is changed. this._data.cloned = false; - const data = applySyncResponse( - this._data, roomResponse, + let data = applySyncResponse(this._data, roomResponse, membership); + data = applyTimelineEntries( + this._data, timelineEntries, - membership, isInitialSync, isTimelineOpen, this._ownUserId); if (data !== this._data) { @@ -282,6 +319,25 @@ export class RoomSummary { } } + /** + * Only to be used with processTimelineEntries, + * other methods like writeSync, writeHasFetchedMembers, + * writeIsTrackingMembers, ... take a txn directly. + */ + async writeAndApplyChanges(data, storage) { + const txn = await storage.readTxn([ + storage.storeNames.roomSummary, + ]); + try { + txn.roomSummary.set(data.serialize()); + } catch (err) { + txn.abort(); + throw err; + } + await txn.complete(); + this.applyChanges(data); + } + applyChanges(data) { this._data = data; } From 9212a1313e06e25fcb97b5286a3c476ca827a0c4 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 14 Sep 2020 17:01:45 +0200 Subject: [PATCH 13/19] add new icon --- icon.png | Bin 40823 -> 16599 bytes icon.svg | 6 ++++++ scripts/build.mjs | 35 +++++++++++++++++++++++++---------- 3 files changed, 31 insertions(+), 10 deletions(-) create mode 100644 icon.svg diff --git a/icon.png b/icon.png index 39f1ae92a31e7c1c545cf8655309de6c9afe11ac..3d96b2d8238c8d524f82533586cb9f6f22c89e1d 100644 GIT binary patch literal 16599 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 T?96B2zL1K7rhJX8Mfm>!lo{VW literal 40823 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 z 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 + + + + + diff --git a/scripts/build.mjs b/scripts/build.mjs index c6c37b96..da2c7b2f 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -64,6 +64,9 @@ const olmFiles = { wasmBundle: "olm-1421970081.js", }; +// IDEA: how about instead of assetPaths we maintain a mapping between the source file and the target file +// so throughout the build script we can refer to files by their source name + async function build() { // only used for CSS for now, using legacy for all targets for now const legacy = true; @@ -80,7 +83,7 @@ async function build() { await removeDirIfExists(targetDir); await createDirs(targetDir, themes); // copy assets - await copyFolder(path.join(projectDir, "lib/olm/"), targetDir, ); + await copyFolder(path.join(projectDir, "lib/olm/"), targetDir); // also creates the directories where the theme css bundles are placed in, // so do it first const themeAssets = await copyThemeAssets(themes, legacy); @@ -88,9 +91,19 @@ async function build() { 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, jsWorkerPath, cssBundlePaths, themeAssets); let manifestPath; + // copy icons + let iconPng = await fs.readFile(path.join(projectDir, "icon.png")); + let iconPngPath = resource("icon.png", iconPng); + await fs.writeFile(iconPngPath, iconPng); + let iconSvg = await fs.readFile(path.join(projectDir, "icon.svg")); + let iconSvgPath = resource("icon.svg", iconSvg); + await fs.writeFile(iconSvgPath, iconSvg); + + const assetPaths = createAssetPaths(jsBundlePath, jsLegacyBundlePath, jsWorkerPath, + iconPngPath, iconSvgPath, cssBundlePaths, themeAssets); + if (offline) { manifestPath = await buildOffline(version, assetPaths); } @@ -99,7 +112,7 @@ async function build() { console.log(`built ${PROJECT_ID} ${version} successfully`); } -function createAssetPaths(jsBundlePath, jsLegacyBundlePath, jsWorkerPath, cssBundlePaths, themeAssets) { +function createAssetPaths(jsBundlePath, jsLegacyBundlePath, jsWorkerPath, iconPngPath, iconSvgPath, cssBundlePaths, themeAssets) { function trim(path) { if (!path.startsWith(targetDir)) { throw new Error("invalid target path: " + targetDir); @@ -113,7 +126,9 @@ function createAssetPaths(jsBundlePath, jsLegacyBundlePath, jsWorkerPath, cssBun cssMainBundle: () => trim(cssBundlePaths.main), cssThemeBundle: themeName => trim(cssBundlePaths.themes[themeName]), cssThemeBundles: () => Object.values(cssBundlePaths.themes).map(a => trim(a)), - otherAssets: () => Object.values(themeAssets).map(a => trim(a)) + otherAssets: () => Object.values(themeAssets).map(a => trim(a)), + iconSvgPath: () => trim(iconSvgPath), + iconPngPath: () => trim(iconPngPath), }; } @@ -248,7 +263,8 @@ async function buildOffline(version, assetPaths) { const offlineFiles = [ assetPaths.cssMainBundle(), "index.html", - "icon-192.png", + assetPaths.iconPngPath(), + assetPaths.iconSvgPath(), ].concat(assetPaths.cssThemeBundles()); // write appcache manifest @@ -275,15 +291,14 @@ async function buildOffline(version, assetPaths) { short_name: PROJECT_SHORT_NAME, display: "fullscreen", start_url: "index.html", - icons: [{"src": "icon-192.png", "sizes": "192x192", "type": "image/png"}], + icons: [ + {"src": assetPaths.iconPngPath(), "sizes": "384x384", "type": "image/png"}, + {"src": assetPaths.iconSvgPath(), "type": "image/svg+xml"}, + ], }; const manifestJson = JSON.stringify(webManifest); const manifestPath = resource("manifest.json", manifestJson); await fs.writeFile(manifestPath, manifestJson, "utf8"); - // copy icon - // should this icon have a content hash as well? - let icon = await fs.readFile(path.join(projectDir, "icon.png")); - await fs.writeFile(path.join(targetDir, "icon-192.png"), icon); return manifestPath; } From c91290fac28eaa4b5773d2c9e73709cee45c832c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 14 Sep 2020 17:10:52 +0200 Subject: [PATCH 14/19] set theme color as well for pwa --- scripts/build.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/build.mjs b/scripts/build.mjs index da2c7b2f..7084ebc0 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -295,6 +295,7 @@ async function buildOffline(version, assetPaths) { {"src": assetPaths.iconPngPath(), "sizes": "384x384", "type": "image/png"}, {"src": assetPaths.iconSvgPath(), "type": "image/svg+xml"}, ], + theme_color: "#0DBD8B" }; const manifestJson = JSON.stringify(webManifest); const manifestPath = resource("manifest.json", manifestJson); From 9ea961ae532a240fc44294734a900b8d9496aeca Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 14 Sep 2020 17:43:06 +0200 Subject: [PATCH 15/19] don't crash when we don't have a subscription anymore --- src/domain/ViewModel.js | 6 ++++++ src/domain/session/room/timeline/TimelineViewModel.js | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/src/domain/ViewModel.js b/src/domain/ViewModel.js index 15812f8c..7f973ad7 100644 --- a/src/domain/ViewModel.js +++ b/src/domain/ViewModel.js @@ -25,6 +25,7 @@ export class ViewModel extends EventEmitter { constructor({clock, emitChange} = {}) { super(); this.disposables = null; + this._isDisposed = false; this._options = {clock, emitChange}; } @@ -43,6 +44,11 @@ export class ViewModel extends EventEmitter { if (this.disposables) { this.disposables.dispose(); } + this._isDisposed = true; + } + + get isDisposed() { + return this._isDisposed; } disposeTracked(disposable) { diff --git a/src/domain/session/room/timeline/TimelineViewModel.js b/src/domain/session/room/timeline/TimelineViewModel.js index 16d529cb..a4f662f6 100644 --- a/src/domain/session/room/timeline/TimelineViewModel.js +++ b/src/domain/session/room/timeline/TimelineViewModel.js @@ -50,6 +50,10 @@ export class TimelineViewModel extends ViewModel { * @return {bool} startReached if the start of the timeline was reached */ async loadAtTop() { + if (this.isDisposed) { + // stop loading more, we switched room + return true; + } const firstTile = this._tiles.getFirst(); if (firstTile.shape === "gap") { return firstTile.fill(); From b2e6e8687ef5e804cf37c2b7ad94892ba4123122 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 14 Sep 2020 17:44:22 +0200 Subject: [PATCH 16/19] dispose tiles also add more defence against emitting event when disposed --- src/domain/session/room/timeline/TilesCollection.js | 9 ++++++--- .../session/room/timeline/tiles/SimpleTile.js | 13 ++++++++++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/domain/session/room/timeline/TilesCollection.js b/src/domain/session/room/timeline/TilesCollection.js index 6854bd5e..c2d9df5d 100644 --- a/src/domain/session/room/timeline/TilesCollection.js +++ b/src/domain/session/room/timeline/TilesCollection.js @@ -145,7 +145,7 @@ export class TilesCollection extends BaseObservableList { if (tile) { const action = tile.updateEntry(entry, params); if (action.shouldReplace) { - this._replaceTile(tileIdx, this._tileCreator(entry)); + this._replaceTile(tileIdx, tile, this._tileCreator(entry)); } if (action.shouldRemove) { this._removeTile(tileIdx, tile); @@ -167,7 +167,8 @@ export class TilesCollection extends BaseObservableList { // merge with neighbours? ... hard to imagine use case for this ... } - _replaceTile(tileIdx, newTile) { + _replaceTile(tileIdx, existingTile, newTile) { + existingTile.dispose(); const prevTile = this._getTileAtIdx(tileIdx - 1); const nextTile = this._getTileAtIdx(tileIdx + 1); this._tiles[tileIdx] = newTile; @@ -184,7 +185,7 @@ export class TilesCollection extends BaseObservableList { this._tiles.splice(tileIdx, 1); prevTile && prevTile.updateNextSibling(nextTile); nextTile && nextTile.updatePreviousSibling(prevTile); - tile.setUpdateEmit(null); + tile.dispose(); this.emitRemove(tileIdx, tile); } @@ -254,6 +255,8 @@ export function tests() { updateEntry() { return UpdateAction.Nothing; } + + dispose() {} } return { diff --git a/src/domain/session/room/timeline/tiles/SimpleTile.js b/src/domain/session/room/timeline/tiles/SimpleTile.js index 7b018b37..12acd4c5 100644 --- a/src/domain/session/room/timeline/tiles/SimpleTile.js +++ b/src/domain/session/room/timeline/tiles/SimpleTile.js @@ -48,7 +48,13 @@ export class SimpleTile extends ViewModel { } // TilesCollection contract below setUpdateEmit(emitUpdate) { - this.updateOptions({emitChange: paramName => emitUpdate(this, paramName)}); + this.updateOptions({emitChange: paramName => { + if (emitUpdate) { + emitUpdate(this, paramName); + } else { + console.trace("Tile is emitting event after being disposed"); + } + }}); } get upperEntry() { @@ -88,5 +94,10 @@ export class SimpleTile extends ViewModel { updateNextSibling(next) { } + + dispose() { + this.setUpdateEmit(null); + super.dispose(); + } // TilesCollection contract above } From a11b612640e2aa0186324e951986ae3adc67a301 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 14 Sep 2020 17:45:02 +0200 Subject: [PATCH 17/19] await this --- src/domain/session/room/timeline/TimelineViewModel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/session/room/timeline/TimelineViewModel.js b/src/domain/session/room/timeline/TimelineViewModel.js index a4f662f6..0f7464f8 100644 --- a/src/domain/session/room/timeline/TimelineViewModel.js +++ b/src/domain/session/room/timeline/TimelineViewModel.js @@ -56,7 +56,7 @@ export class TimelineViewModel extends ViewModel { } const firstTile = this._tiles.getFirst(); if (firstTile.shape === "gap") { - return firstTile.fill(); + return await firstTile.fill(); } else { await this._timeline.loadAtTop(10); return false; From 75bff228ec6964d4300ef0679809f06903a81f34 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 14 Sep 2020 17:45:13 +0200 Subject: [PATCH 18/19] fix data not being passed on, caused crash on initial sync --- src/matrix/room/Room.js | 2 +- src/matrix/room/RoomSummary.js | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index 69037c88..5976242d 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -181,7 +181,7 @@ export class Room extends EventEmitter { // fetch new members while we have txn open, // but don't make any in-memory changes yet let heroChanges; - if (needsHeroes(summaryChanges)) { + if (summaryChanges && needsHeroes(summaryChanges)) { // room name disappeared, open heroes if (!this._heroes) { this._heroes = new Heroes(this._roomId); diff --git a/src/matrix/room/RoomSummary.js b/src/matrix/room/RoomSummary.js index b5220468..741b8764 100644 --- a/src/matrix/room/RoomSummary.js +++ b/src/matrix/room/RoomSummary.js @@ -52,6 +52,7 @@ function applySyncResponse(data, roomResponse, membership) { if (typeof event.state_key === "string") { return processStateEvent(data, event); } + return data; }, data); } const unreadNotifications = roomResponse.unread_notifications; @@ -307,9 +308,9 @@ export class RoomSummary { // clear cloned flag, so cloneIfNeeded makes a copy and // this._data is not modified if any field is changed. this._data.cloned = false; - let data = applySyncResponse(this._data, roomResponse, membership); + let data = applySyncResponse(this._data, roomResponse, membership); data = applyTimelineEntries( - this._data, + data, timelineEntries, isInitialSync, isTimelineOpen, this._ownUserId); From 12262f28249addf320bb9c93e4ba3e6b26e0f6c5 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 14 Sep 2020 18:31:54 +0200 Subject: [PATCH 19/19] actually use correct method to get all device ids for a user --- src/matrix/e2ee/DeviceTracker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/e2ee/DeviceTracker.js b/src/matrix/e2ee/DeviceTracker.js index 10eeb51b..cc3a4215 100644 --- a/src/matrix/e2ee/DeviceTracker.js +++ b/src/matrix/e2ee/DeviceTracker.js @@ -169,7 +169,7 @@ export class DeviceTracker { } async _storeQueriedDevicesForUserId(userId, deviceIdentities, txn) { - const knownDeviceIds = await txn.deviceIdentities.getAllForUserId(userId); + const knownDeviceIds = await txn.deviceIdentities.getAllDeviceIds(userId); // delete any devices that we know off but are not in the response anymore. // important this happens before checking if the ed25519 key changed, // otherwise we would end up deleting existing devices with changed keys.