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; }