diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index f01c1ea960..288acc108a 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -908,6 +908,11 @@ 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) { presentedId = theAlias; diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 8cc344f66b..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; @@ -1141,6 +1142,18 @@ 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(); + + // `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 + arrayFastClone(events) + .reverse() + .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 diff --git a/src/indexing/EventIndex.js b/src/indexing/EventIndex.js index 1cb44f240d..857dc5b248 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,12 @@ 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); + // XXX: Private member access + await ev._decryptionPromise; } + + await this.addLiveEventToIndex(ev); } onRoomStateEvent = async (ev, state) => { @@ -216,10 +211,7 @@ 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 (!this.liveEventsForIndex.delete(eventId)) return; if (err) return; await this.addLiveEventToIndex(ev); } @@ -523,18 +515,23 @@ 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 { + // 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; + } + }); // Let us wait for all the events to get decrypted. await Promise.all(decryptionPromises); diff --git a/src/stores/BreadcrumbsStore.ts b/src/stores/BreadcrumbsStore.ts index 1a78a1b485..2d59bc7d02 100644 --- a/src/stores/BreadcrumbsStore.ts +++ b/src/stores/BreadcrumbsStore.ts @@ -16,12 +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'; const MAX_ROOMS = 20; // arbitrary const AUTOJOIN_WAIT_THRESHOLD_MS = 90000; // 90s, the time we wait for an autojoined room to show up @@ -59,6 +61,30 @@ 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(EventType.RoomEncryption); + if (cryptoEvent) { + if (!client.isRoomEncrypted(room.roomId)) { + // XXX: Private member access + await client._crypto.onCryptoEvent(cryptoEvent); + } + room?.decryptAllEvents(); + } + }); + } + } + protected async onAction(payload: ActionPayload) { if (!this.matrixClient) return;