diff --git a/src/matrix/net/HomeServerApi.js b/src/matrix/net/HomeServerApi.js index f8dfa517..5110ba44 100644 --- a/src/matrix/net/HomeServerApi.js +++ b/src/matrix/net/HomeServerApi.js @@ -189,6 +189,10 @@ export class HomeServerApi { return this._get(`/rooms/${encodeURIComponent(roomId)}/messages`, params, null, options); } + roomState(roomId, eventType, stateKey, options = null) { + return this._get(`/rooms/${encodeURIComponent(roomId)}/state/${encodeURIComponent(eventType)}/${encodeURIComponent(stateKey)}`, null, null, options); + } + // params is at, membership and not_membership members(roomId, params, options = null) { return this._get(`/rooms/${encodeURIComponent(roomId)}/members`, params, null, options); diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index 7326e32f..a8bdb818 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -425,8 +425,13 @@ export class Room extends EventEmitter { /** @public */ sendEvent(eventType, content, attachments, log = null) { - this._platform.logger.wrapOrRun(log, "send", log => { + this._platform.logger.wrapOrRun(log, "send", async log => { log.set("id", this.id); + if (this._timeline) { + // ensure we have our own member loaded for the local echo + await this._timeline.ensureOwnMember( + this._user, this._summary.data.membership, this._hsApi, log); + } return this._sendQueue.enqueueEvent(eventType, content, attachments, log); }); } @@ -678,7 +683,12 @@ export class Room extends EventEmitter { if (this._roomEncryption) { this._timeline.enableEncryption(this._decryptEntries.bind(this, DecryptionSource.Timeline)); } - await this._timeline.load(this._user, log); + if (this._sendQueue.pendingEvents.length !== 0) { + // ensure we have our own member loaded for the local echo + await this._timeline.ensureOwnMember( + this._user, this._summary.data.membership, this._hsApi, log); + } + await this._timeline.load(log); return this._timeline; }); } diff --git a/src/matrix/room/members/RoomMember.js b/src/matrix/room/members/RoomMember.js index 4013f556..c5db675a 100644 --- a/src/matrix/room/members/RoomMember.js +++ b/src/matrix/room/members/RoomMember.js @@ -23,6 +23,15 @@ export class RoomMember { this._data = data; } + static async fromFetch(roomId, userId, hsApi, log) { + const memberEvent = await hsApi.roomState(this._roomId, EVENT_TYPE, userId, {log}).response(); + return RoomMember.fromMemberEvent(memberEvent); + } + + static fromUserId(roomId, userId, membership) { + return new RoomMember({roomId, userId, membership}); + } + static fromMemberEvent(roomId, memberEvent) { const userId = memberEvent?.state_key; if (typeof userId !== "string") { diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 2cccdf83..682fb131 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -45,14 +45,8 @@ export class Timeline { } /** @package */ - async load(user, log) { - const txn = await this._storage.readTxn(this._timelineReader.readTxnStores.concat(this._storage.storeNames.roomMembers)); - const memberData = await txn.roomMembers.get(this._roomId, user.id); - this._ownMember = new RoomMember(memberData); - // it should be fine to not update the local entries, - // as they should only populate once the view subscribes to it - // if they are populated already, the sender profile would be empty - + async load(log) { + const txn = await this._storage.readTxn(this._timelineReader.readTxnStores); // 30 seems to be a good amount to fill the entire screen const readerRequest = this._disposables.track(this._timelineReader.readFromEnd(30, txn, log)); try { @@ -63,6 +57,41 @@ export class Timeline { } } + async ensureOwnMember(user, myMembership, hsApi, log) { + if (this._ownMember) { + return; + } + const txn = await this._storage.readTxn(this._storage.storeNames.roomMembers); + const memberData = await txn.roomMembers.get(this._roomId, user.id); + if (memberData) { + this._ownMember = new RoomMember(memberData); + } else { + await log.wrap("own member missing", async log => { + // important this *never* throws, not even when offline, + // as it would prevent enqueueing a message, or opening the timeline + try { + this._ownMember = await RoomMember.fromFetch(this._roomId, user.id, hsApi, log); + const writeTxn = await this._storage.readWriteTxn(this._storage.storeNames.roomMembers); + // check we still don't have the member after the fetch (e.g. from sync) + // just to make sure we don't overwrite it + const memberData = await writeTxn.roomMembers.get(this._roomId, user.id); + if(!memberData) { + writeTxn.roomMembers.set(this._roomId, this._ownMember.serialize()); + await writeTxn.complete(); + } else { + this._ownMember = new RoomMember(memberData); + } + } catch (err) { + log.error = err; + if (!this._ownMember) { + log.set("fallback", true); + this._ownMember = RoomMember.fromUserId(this._roomId, user.id, myMembership); + } + } + }); + } + } + updateOwnMember(member) { this._ownMember = member; }