From 0c87a67caf645fb86e55dd7dca4649411b56ff5b Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 6 May 2021 11:46:25 +0100 Subject: [PATCH 01/10] Lazily decrypt events on room view --- src/components/structures/MatrixChat.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 078b296295..41cacd2569 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -906,6 +906,7 @@ export default class MatrixChat extends React.PureComponent { let presentedId = roomInfo.room_alias || roomInfo.room_id; const room = MatrixClientPeg.get().getRoom(roomInfo.room_id); if (room) { + room.lazyDecryptEvents(); const theAlias = Rooms.getDisplayAliasForRoom(room); if (theAlias) { presentedId = theAlias; From 17099c656bdcc6997170659905068d93d7ad34a5 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Fri, 7 May 2021 11:25:25 +0100 Subject: [PATCH 02/10] Call renamed room::decryptAllEvents method --- src/components/structures/MatrixChat.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 41cacd2569..f691b7ab0b 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -906,7 +906,7 @@ export default class MatrixChat extends React.PureComponent { let presentedId = roomInfo.room_alias || roomInfo.room_id; const room = MatrixClientPeg.get().getRoom(roomInfo.room_id); if (room) { - room.lazyDecryptEvents(); + room.decryptAllEvents(); const theAlias = Rooms.getDisplayAliasForRoom(room); if (theAlias) { presentedId = theAlias; From 5bd41209200798e93594776a29d6c789098f42e9 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Fri, 7 May 2021 12:58:37 +0100 Subject: [PATCH 03/10] Decrypt breadcrumb events for better UX --- src/stores/BreadcrumbsStore.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/stores/BreadcrumbsStore.ts b/src/stores/BreadcrumbsStore.ts index 393f4f27a1..5c49fef148 100644 --- a/src/stores/BreadcrumbsStore.ts +++ b/src/stores/BreadcrumbsStore.ts @@ -22,6 +22,7 @@ import defaultDispatcher from "../dispatcher/dispatcher"; import { arrayHasDiff } from "../utils/arrays"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; import { SettingLevel } from "../settings/SettingLevel"; +import {MatrixClientPeg} from '../MatrixClientPeg'; const MAX_ROOMS = 20; // arbitrary const AUTOJOIN_WAIT_THRESHOLD_MS = 90000; // 90s, the time we wait for an autojoined room to show up @@ -87,6 +88,23 @@ export class BreadcrumbsStore extends AsyncStoreWithClient { this.matrixClient.on("Room.myMembership", this.onMyMembership); this.matrixClient.on("Room", this.onRoom); + + const client = MatrixClientPeg.get(); + const breadcrumbs = client.store.getAccountData("im.vector.setting.breadcrumbs"); + const breadcrumbsRooms: string[] = breadcrumbs?.getContent().recent_rooms || []; + + breadcrumbsRooms.map(async roomId => { + const room = client.getRoom(roomId); + if (room) { + const [cryptoEvent] = room.currentState.getStateEvents("m.room.encryption"); + if (cryptoEvent) { + if (!client.isRoomEncrypted(roomId)) { + await client._crypto.onCryptoEvent(cryptoEvent); + } + return room?.decryptAllEvents(); + } + } + }); } protected async onNotReady() { From fa30285c6bbda4f1328c4a71e66b290d38a375f1 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Fri, 7 May 2021 15:16:54 +0100 Subject: [PATCH 04/10] Decrypt messages on when used on a timeline --- src/components/structures/TimelinePanel.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 8cc344f66b..6622482efc 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -1141,6 +1141,14 @@ class TimelinePanel extends React.Component { // get the list of events from the timeline window and the pending event list _getEvents() { const events = this._timelineWindow.getEvents(); + + events + .forEach(event => { + if (event.shouldAttemptDecryption()) { + event.attemptDecryption(MatrixClientPeg.get()._crypto); + } + }); + const firstVisibleEventIndex = this._checkForPreJoinUISI(events); // Hold onto the live events separately. The read receipt and read marker From 6e3f8d6a0a0a1b6915901b0780b376b6b0aad42f Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Fri, 7 May 2021 15:26:16 +0100 Subject: [PATCH 05/10] Decrypt last events first to avoid shifts when scrolling up --- src/components/structures/TimelinePanel.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 6622482efc..63a52f7807 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -1142,7 +1142,12 @@ class TimelinePanel extends React.Component { _getEvents() { const events = this._timelineWindow.getEvents(); + + // `slice` performs a shallow copy of the array + // we want the last event to be decrypted first but displayed last + // `reverse` is destructive and unfortunately mutates the "events" array events + .slice().reverse() .forEach(event => { if (event.shouldAttemptDecryption()) { event.attemptDecryption(MatrixClientPeg.get()._crypto); From d0d2907a07782e1596171bee24b82ae609056efa Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 10 May 2021 15:19:46 +0100 Subject: [PATCH 06/10] Decrypt events ahead of storing them in the index --- src/indexing/EventIndex.js | 37 +++++++++++++++---------------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/src/indexing/EventIndex.js b/src/indexing/EventIndex.js index 1cb44f240d..e27687e784 100644 --- a/src/indexing/EventIndex.js +++ b/src/indexing/EventIndex.js @@ -38,7 +38,6 @@ export default class EventIndex extends EventEmitter { this._eventsPerCrawl = 100; this._crawler = null; this._currentCheckpoint = null; - this.liveEventsForIndex = new Set(); } async init() { @@ -188,16 +187,11 @@ export default class EventIndex extends EventEmitter { return; } - // If the event is not yet decrypted mark it for the - // Event.decrypted callback. if (ev.isBeingDecrypted()) { - const eventId = ev.getId(); - this.liveEventsForIndex.add(eventId); - } else { - // If the event is decrypted or is unencrypted add it to the - // index now. - await this.addLiveEventToIndex(ev); + await ev._decryptionPromise; } + + await this.addLiveEventToIndex(ev); } onRoomStateEvent = async (ev, state) => { @@ -219,7 +213,6 @@ export default class EventIndex extends EventEmitter { const eventId = ev.getId(); // If the event isn't in our live event set, ignore it. - if (!this.liveEventsForIndex.delete(eventId)) return; if (err) return; await this.addLiveEventToIndex(ev); } @@ -523,18 +516,18 @@ export default class EventIndex extends EventEmitter { } }); - const decryptionPromises = []; - - matrixEvents.forEach(ev => { - if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) { - // TODO the decryption promise is a private property, this - // should either be made public or we should convert the - // event that gets fired when decryption is done into a - // promise using the once event emitter method: - // https://nodejs.org/api/events.html#events_events_once_emitter_name - decryptionPromises.push(ev._decryptionPromise); - } - }); + const decryptionPromises = matrixEvents + .filter(event => event.isEncrypted()) + .map(event => { + if (event.shouldAttemptDecryption()) { + return event.attemptDecryption(client._crypto, { + isRetry: true, + emit: false, + }); + } else { + return event._decryptionPromise; + } + }); // Let us wait for all the events to get decrypted. await Promise.all(decryptionPromises); From f1a6f6fd7f015531d7ae3b03c001e2f18de596d6 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 10 May 2021 15:36:59 +0100 Subject: [PATCH 07/10] make breadcrumb room events decryption more idiomatic --- src/components/structures/TimelinePanel.js | 1 - src/stores/BreadcrumbsStore.ts | 44 +++++++++++++--------- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 63a52f7807..a3c1c56276 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -1142,7 +1142,6 @@ class TimelinePanel extends React.Component { _getEvents() { const events = this._timelineWindow.getEvents(); - // `slice` performs a shallow copy of the array // we want the last event to be decrypted first but displayed last // `reverse` is destructive and unfortunately mutates the "events" array diff --git a/src/stores/BreadcrumbsStore.ts b/src/stores/BreadcrumbsStore.ts index 5c49fef148..28f3151434 100644 --- a/src/stores/BreadcrumbsStore.ts +++ b/src/stores/BreadcrumbsStore.ts @@ -60,6 +60,33 @@ export class BreadcrumbsStore extends AsyncStoreWithClient { return this.matrixClient && this.matrixClient.getVisibleRooms().length >= 20; } + componentDidUpdate(prevProps, prevState) { + const prevRoomCount = (prevState.rooms?.length || 0); + const currentRoomCount = (this.state.rooms?.length || 0) + + /** + * Only decrypting the breadcrumb rooms events on app initialisation + * when room count transitions from 0 to the number of rooms it contains + */ + if (prevRoomCount === 0 && currentRoomCount > prevRoomCount) { + const client = MatrixClientPeg.get(); + /** + * Rooms in the breadcrumb have a good chance to be interacted with + * again by a user. Decrypting the messages ahead of time will help + * reduce content shift on first render + */ + this.state.rooms?.forEach(async room => { + const [cryptoEvent] = room.currentState.getStateEvents("m.room.encryption"); + if (cryptoEvent) { + if (!client.isRoomEncrypted(room.roomId)) { + await client._crypto.onCryptoEvent(cryptoEvent); + } + room?.decryptAllEvents(); + } + }); + } + } + protected async onAction(payload: ActionPayload) { if (!this.matrixClient) return; @@ -88,23 +115,6 @@ export class BreadcrumbsStore extends AsyncStoreWithClient { this.matrixClient.on("Room.myMembership", this.onMyMembership); this.matrixClient.on("Room", this.onRoom); - - const client = MatrixClientPeg.get(); - const breadcrumbs = client.store.getAccountData("im.vector.setting.breadcrumbs"); - const breadcrumbsRooms: string[] = breadcrumbs?.getContent().recent_rooms || []; - - breadcrumbsRooms.map(async roomId => { - const room = client.getRoom(roomId); - if (room) { - const [cryptoEvent] = room.currentState.getStateEvents("m.room.encryption"); - if (cryptoEvent) { - if (!client.isRoomEncrypted(roomId)) { - await client._crypto.onCryptoEvent(cryptoEvent); - } - return room?.decryptAllEvents(); - } - } - }); } protected async onNotReady() { From c96f11db7d92daf76b7d8fed5551207fecf76eb7 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 10 May 2021 17:22:33 +0100 Subject: [PATCH 08/10] appease linter --- src/indexing/EventIndex.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/indexing/EventIndex.js b/src/indexing/EventIndex.js index e27687e784..0193be3375 100644 --- a/src/indexing/EventIndex.js +++ b/src/indexing/EventIndex.js @@ -210,8 +210,6 @@ export default class EventIndex extends EventEmitter { * listener, if so queues it up to be added to the index. */ onEventDecrypted = async (ev, err) => { - const eventId = ev.getId(); - // If the event isn't in our live event set, ignore it. if (err) return; await this.addLiveEventToIndex(ev); From be236309c52fb6f33bb8bf9441c1ee0405b36685 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 11 May 2021 10:08:57 +0100 Subject: [PATCH 09/10] use arrayFastClone instead of slice --- src/components/structures/TimelinePanel.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index a3c1c56276..5012d91a5f 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -38,6 +38,7 @@ import {haveTileForEvent} from "../views/rooms/EventTile"; import {UIFeature} from "../../settings/UIFeature"; import {objectHasDiff} from "../../utils/objects"; import {replaceableComponent} from "../../utils/replaceableComponent"; +import { arrayFastClone } from "../../utils/arrays"; const PAGINATE_SIZE = 20; const INITIAL_SIZE = 20; @@ -1142,11 +1143,11 @@ class TimelinePanel extends React.Component { _getEvents() { const events = this._timelineWindow.getEvents(); - // `slice` performs a shallow copy of the array + // `arrayFastClone` performs a shallow copy of the array // we want the last event to be decrypted first but displayed last // `reverse` is destructive and unfortunately mutates the "events" array - events - .slice().reverse() + arrayFastClone(events) + .reverse() .forEach(event => { if (event.shouldAttemptDecryption()) { event.attemptDecryption(MatrixClientPeg.get()._crypto); From da1df705576a35ac5655e7a70c7f5278cdecdf8f Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 11 May 2021 10:18:53 +0100 Subject: [PATCH 10/10] Improve comments and explainer for new decryption approach --- src/components/structures/MatrixChat.tsx | 4 ++++ src/indexing/EventIndex.js | 6 ++++++ src/stores/BreadcrumbsStore.ts | 20 +++++++++----------- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index fa2ea1546a..691c2bd08c 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -906,6 +906,10 @@ export default class MatrixChat extends React.PureComponent { let presentedId = roomInfo.room_alias || roomInfo.room_id; const room = MatrixClientPeg.get().getRoom(roomInfo.room_id); if (room) { + // Not all timeline events are decrypted ahead of time anymore + // Only the critical ones for a typical UI are + // This will start the decryption process for all events when a + // user views a room room.decryptAllEvents(); const theAlias = Rooms.getDisplayAliasForRoom(room); if (theAlias) { diff --git a/src/indexing/EventIndex.js b/src/indexing/EventIndex.js index 0193be3375..857dc5b248 100644 --- a/src/indexing/EventIndex.js +++ b/src/indexing/EventIndex.js @@ -188,6 +188,7 @@ export default class EventIndex extends EventEmitter { } if (ev.isBeingDecrypted()) { + // XXX: Private member access await ev._decryptionPromise; } @@ -523,6 +524,11 @@ export default class EventIndex extends EventEmitter { emit: false, }); } else { + // TODO the decryption promise is a private property, this + // should either be made public or we should convert the + // event that gets fired when decryption is done into a + // promise using the once event emitter method: + // https://nodejs.org/api/events.html#events_events_once_emitter_name return event._decryptionPromise; } }); diff --git a/src/stores/BreadcrumbsStore.ts b/src/stores/BreadcrumbsStore.ts index edb8fc8e29..2d59bc7d02 100644 --- a/src/stores/BreadcrumbsStore.ts +++ b/src/stores/BreadcrumbsStore.ts @@ -16,13 +16,14 @@ limitations under the License. import SettingsStore from "../settings/SettingsStore"; import { Room } from "matrix-js-sdk/src/models/room"; +import { EventType } from "matrix-js-sdk/src/@types/event"; import { ActionPayload } from "../dispatcher/payloads"; import { AsyncStoreWithClient } from "./AsyncStoreWithClient"; import defaultDispatcher from "../dispatcher/dispatcher"; import { arrayHasDiff } from "../utils/arrays"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; import { SettingLevel } from "../settings/SettingLevel"; -import {MatrixClientPeg} from '../MatrixClientPeg'; +import { MatrixClientPeg } from '../MatrixClientPeg'; const MAX_ROOMS = 20; // arbitrary const AUTOJOIN_WAIT_THRESHOLD_MS = 90000; // 90s, the time we wait for an autojoined room to show up @@ -64,21 +65,18 @@ export class BreadcrumbsStore extends AsyncStoreWithClient { const prevRoomCount = (prevState.rooms?.length || 0); const currentRoomCount = (this.state.rooms?.length || 0) - /** - * Only decrypting the breadcrumb rooms events on app initialisation - * when room count transitions from 0 to the number of rooms it contains - */ + // Only decrypting the breadcrumb rooms events on app initialisation + // when room count transitions from 0 to the number of rooms it contains if (prevRoomCount === 0 && currentRoomCount > prevRoomCount) { const client = MatrixClientPeg.get(); - /** - * Rooms in the breadcrumb have a good chance to be interacted with - * again by a user. Decrypting the messages ahead of time will help - * reduce content shift on first render - */ + // Rooms in the breadcrumb have a good chance to be interacted with + // again by a user. Decrypting the messages ahead of time will help + // reduce content shift on first render this.state.rooms?.forEach(async room => { - const [cryptoEvent] = room.currentState.getStateEvents("m.room.encryption"); + const [cryptoEvent] = room.currentState.getStateEvents(EventType.RoomEncryption); if (cryptoEvent) { if (!client.isRoomEncrypted(room.roomId)) { + // XXX: Private member access await client._crypto.onCryptoEvent(cryptoEvent); } room?.decryptAllEvents();