diff --git a/src/matrix/net/HomeServerApi.js b/src/matrix/net/HomeServerApi.js index 02d4dc2d..a9b63f8e 100644 --- a/src/matrix/net/HomeServerApi.js +++ b/src/matrix/net/HomeServerApi.js @@ -130,6 +130,10 @@ export class HomeServerApi { {}, {}, options); } + state(roomId, eventType, stateKey, options = null) { + return this._get(`/rooms/${encodeURIComponent(roomId)}/state/${encodeURIComponent(eventType)}/${encodeURIComponent(stateKey)}`, {}, null, options); + } + passwordLogin(username, password, initialDeviceDisplayName, options = null) { return this._unauthedRequest("POST", this._url("/login"), null, { "type": "m.login.password", diff --git a/src/matrix/room/BaseRoom.js b/src/matrix/room/BaseRoom.js index 7ee04761..b24ef706 100644 --- a/src/matrix/room/BaseRoom.js +++ b/src/matrix/room/BaseRoom.js @@ -21,7 +21,7 @@ import {RelationWriter} from "./timeline/persistence/RelationWriter.js"; import {Timeline} from "./timeline/Timeline.js"; import {FragmentIdComparer} from "./timeline/FragmentIdComparer.js"; import {WrappedError} from "../error.js" -import {fetchOrLoadMembers} from "./members/load.js"; +import {fetchOrLoadMembers, fetchOrLoadMember} from "./members/load.js"; import {MemberList} from "./members/MemberList.js"; import {Heroes} from "./members/Heroes.js"; import {EventEntry} from "./timeline/entries/EventEntry.js"; @@ -54,6 +54,7 @@ export class BaseRoom extends EventEmitter { this._observedEvents = null; this._powerLevels = null; this._powerLevelLoading = null; + this._observedMembers = null; } async _eventIdsToEntries(eventIds, txn) { @@ -214,6 +215,32 @@ export class BaseRoom extends EventEmitter { } } + async observeMember(userId) { + if (!this._observedMembers) { + this._observedMembers = new Map(); + } + const mapMember = this._observedMembers.get(userId); + if (mapMember) { + // Hit, we're already observing this member + return mapMember; + } + // Miss, load from storage/hs and set in map + const member = await fetchOrLoadMember({ + summary: this._summary, + roomId: this._roomId, + userId, + storage: this._storage, + hsApi: this._hsApi + }, this._platform.logger); + if (!member) { + return null; + } + const observableMember = new RetainedObservableValue(member, () => this._observedMembers.delete(userId)); + this._observedMembers.set(userId, observableMember); + return observableMember; + } + + /** @public */ async loadMemberList(log = null) { if (this._memberList) { diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index a8e94326..c49d6a4e 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -209,6 +209,9 @@ export class Room extends BaseRoom { if (this._memberList) { this._memberList.afterSync(memberChanges); } + if (this._observedMembers) { + this._updateObservedMembers(memberChanges); + } if (this._timeline) { for (const [userId, memberChange] of memberChanges.entries()) { if (userId === this._user.id) { @@ -250,6 +253,15 @@ export class Room extends BaseRoom { } } + _updateObservedMembers(memberChanges) { + for (const [userId, memberChange] of memberChanges) { + const observableMember = this._observedMembers.get(userId); + if (observableMember) { + observableMember.set(memberChange.member); + } + } + } + needsAfterSyncCompleted({shouldFlushKeyShares}) { return shouldFlushKeyShares; } diff --git a/src/matrix/room/members/load.js b/src/matrix/room/members/load.js index 52ae58c4..5077d793 100644 --- a/src/matrix/room/members/load.js +++ b/src/matrix/room/members/load.js @@ -90,3 +90,49 @@ export async function fetchOrLoadMembers(options, logger) { return loadMembers(options); } } + +export async function fetchOrLoadMember(options, logger) { + const member = await loadMember(options); + const {summary} = options; + if (!summary.data.hasFetchedMembers && !member) { + // We haven't fetched the memberlist yet; so ping the hs to see if this member does exist + return logger.wrapOrRun(options.log, "fetchMember", log => fetchMember(options, log)); + } + return member; +} + +async function loadMember({roomId, userId, storage}) { + const txn = await storage.readTxn([storage.storeNames.roomMembers,]); + const member = await txn.roomMembers.get(roomId, userId); + return member? new RoomMember(member) : null; +} + +async function fetchMember({roomId, userId, hsApi, storage}, log) { + let memberData; + try { + memberData = await hsApi.state(roomId, "m.room.member", userId, { log }).response(); + } + catch (error) { + if (error.name === "HomeServerError" && error.errcode === "M_NOT_FOUND") { + return null; + } + throw error; + } + const member = new RoomMember({ + roomId, + userId, + membership: memberData.membership, + avatarUrl: memberData.avatar_url, + displayName: memberData.displayname, + }); + const txn = await storage.readWriteTxn([storage.storeNames.roomMembers]); + try { + txn.roomMembers.set(member.serialize()); + } + catch(e) { + txn.abort(); + throw e; + } + await txn.complete(); + return member; +}