diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index 86ab6c35..f50b221b 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -51,7 +51,7 @@ export class Room extends EventEmitter { membership, isInitialSync, isTimelineOpen, txn); - const {entries, newLiveKey, changedMembers} = await this._syncWriter.writeSync(roomResponse, txn); + const {entries, newLiveKey, memberChanges} = await this._syncWriter.writeSync(roomResponse, txn); // fetch new members while we have txn open, // but don't make any in-memory changes yet let heroChanges; @@ -60,7 +60,7 @@ export class Room extends EventEmitter { if (!this._heroes) { this._heroes = new Heroes(this._roomId); } - heroChanges = await this._heroes.calculateChanges(summaryChanges.heroes, changedMembers, txn); + heroChanges = await this._heroes.calculateChanges(summaryChanges.heroes, memberChanges, txn); } let removedPendingEvents; if (roomResponse.timeline && roomResponse.timeline.events) { @@ -71,22 +71,22 @@ export class Room extends EventEmitter { newTimelineEntries: entries, newLiveKey, removedPendingEvents, - changedMembers, + memberChanges, heroChanges }; } /** @package */ - afterSync({summaryChanges, newTimelineEntries, newLiveKey, removedPendingEvents, changedMembers, heroChanges}) { + afterSync({summaryChanges, newTimelineEntries, newLiveKey, removedPendingEvents, memberChanges, heroChanges}) { this._syncWriter.afterSync(newLiveKey); - if (changedMembers.length) { + if (memberChanges.size) { if (this._changedMembersDuringSync) { - for (const member of changedMembers) { - this._changedMembersDuringSync.set(member.userId, member); + for (const [userId, memberChange] of memberChanges.entries()) { + this._changedMembersDuringSync.set(userId, memberChange.member); } } if (this._memberList) { - this._memberList.afterSync(changedMembers); + this._memberList.afterSync(memberChanges); } } let emitChange = false; diff --git a/src/matrix/room/members/Heroes.js b/src/matrix/room/members/Heroes.js index f7ccb2df..809b61c2 100644 --- a/src/matrix/room/members/Heroes.js +++ b/src/matrix/room/members/Heroes.js @@ -42,11 +42,11 @@ export class Heroes { /** * @param {string[]} newHeroes array of user ids - * @param {RoomMember[]} changedMembers array of changed members in this sync + * @param {Map} memberChanges map of changed memberships * @param {Transaction} txn * @return {Promise} */ - async calculateChanges(newHeroes, changedMembers, txn) { + async calculateChanges(newHeroes, memberChanges, txn) { const updatedHeroMembers = new Map(); const removedUserIds = []; // remove non-present members @@ -56,9 +56,9 @@ export class Heroes { } } // update heroes with synced member changes - for (const member of changedMembers) { - if (this._members.has(member.userId) || newHeroes.indexOf(member.userId) !== -1) { - updatedHeroMembers.set(member.userId, member); + for (const [userId, memberChange] of memberChanges.entries()) { + if (this._members.has(userId) || newHeroes.indexOf(userId) !== -1) { + updatedHeroMembers.set(userId, memberChange.member); } } // load member for new heroes from storage diff --git a/src/matrix/room/members/MemberList.js b/src/matrix/room/members/MemberList.js index f428ed6c..734887fd 100644 --- a/src/matrix/room/members/MemberList.js +++ b/src/matrix/room/members/MemberList.js @@ -26,9 +26,9 @@ export class MemberList { this._retentionCount = 1; } - afterSync(updatedMembers) { - for (const member of updatedMembers) { - this._members.add(member.userId, member); + afterSync(memberChanges) { + for (const [userId, memberChange] of memberChanges.entries()) { + this._members.add(userId, memberChange.member); } } diff --git a/src/matrix/room/members/RoomMember.js b/src/matrix/room/members/RoomMember.js index 02c3c292..27a2e59f 100644 --- a/src/matrix/room/members/RoomMember.js +++ b/src/matrix/room/members/RoomMember.js @@ -99,3 +99,30 @@ export class RoomMember { return this._data; } } + +export class MemberChange { + constructor(roomId, memberEvent) { + this._roomId = roomId; + this._memberEvent = memberEvent; + this._member = null; + } + + get member() { + if (!this._member) { + this._member = RoomMember.fromMemberEvent(this._roomId, this._memberEvent); + } + return this._member; + } + + userId() { + return this._memberEvent.state_key; + } + + previousMembership() { + return this._memberEvent.unsigned?.prev_content?.membership; + } + + membership() { + return this._memberEvent.content?.membership; + } +} diff --git a/src/matrix/room/timeline/persistence/SyncWriter.js b/src/matrix/room/timeline/persistence/SyncWriter.js index 84e8a18f..fdc4035b 100644 --- a/src/matrix/room/timeline/persistence/SyncWriter.js +++ b/src/matrix/room/timeline/persistence/SyncWriter.js @@ -18,7 +18,7 @@ import {EventKey} from "../EventKey.js"; import {EventEntry} from "../entries/EventEntry.js"; import {FragmentBoundaryEntry} from "../entries/FragmentBoundaryEntry.js"; import {createEventEntry} from "./common.js"; -import {RoomMember, EVENT_TYPE as MEMBER_EVENT_TYPE} from "../../members/RoomMember.js"; +import {MemberChange, RoomMember, EVENT_TYPE as MEMBER_EVENT_TYPE} from "../../members/RoomMember.js"; // Synapse bug? where the m.room.create event appears twice in sync response // when first syncing the room @@ -102,13 +102,13 @@ export class SyncWriter { if (event.type === MEMBER_EVENT_TYPE) { const userId = event.state_key; if (userId) { - const member = RoomMember.fromMemberEvent(this._roomId, event); - if (member) { + const memberChange = new MemberChange(this._roomId, event); + if (memberChange.member) { // as this is sync, we can just replace the member // if it is there already - txn.roomMembers.set(member.serialize()); + txn.roomMembers.set(memberChange.member.serialize()); + return memberChange; } - return member; } } else { txn.roomState.set(this._roomId, event); @@ -116,22 +116,22 @@ export class SyncWriter { } _writeStateEvents(roomResponse, txn) { - const changedMembers = []; + const memberChanges = new Map(); // persist state const {state} = roomResponse; if (Array.isArray(state?.events)) { for (const event of state.events) { - const member = this._writeStateEvent(event, txn); - if (member) { - changedMembers.push(member); + const memberChange = this._writeStateEvent(event, txn); + if (memberChange) { + memberChanges.set(memberChange.userId, memberChange); } } } - return changedMembers; + return memberChanges; } async _writeTimeline(entries, timeline, currentKey, txn) { - const changedMembers = []; + const memberChanges = new Map(); if (timeline.events) { const events = deduplicateEvents(timeline.events); for(const event of events) { @@ -148,14 +148,14 @@ export class SyncWriter { // process live state events first, so new member info is available if (typeof event.state_key === "string") { - const member = this._writeStateEvent(event, txn); - if (member) { - changedMembers.push(member); + const memberChange = this._writeStateEvent(event, txn); + if (memberChange) { + memberChanges.set(memberChange.userId, memberChange); } } } } - return {currentKey, changedMembers}; + return {currentKey, memberChanges}; } async _findMemberData(userId, events, txn) { @@ -198,12 +198,14 @@ export class SyncWriter { } // important this happens before _writeTimeline so // members are available in the transaction - const changedMembers = this._writeStateEvents(roomResponse, txn); + const memberChanges = this._writeStateEvents(roomResponse, txn); const timelineResult = await this._writeTimeline(entries, timeline, currentKey, txn); currentKey = timelineResult.currentKey; - changedMembers.push(...timelineResult.changedMembers); - - return {entries, newLiveKey: currentKey, changedMembers}; + // merge member changes from state and timeline, giving precedence to the latter + for (const [userId, memberChange] of timelineResult.memberChanges.entries()) { + memberChanges.set(userId, memberChange); + } + return {entries, newLiveKey: currentKey, memberChanges}; } afterSync(newLiveKey) {