From b753507b8d25df68c9e8e836ff7a4fedafaa6137 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 6 Dec 2021 11:36:45 +0530 Subject: [PATCH 01/82] WIP --- .../room/timeline/ReactionsViewModel.js | 12 ++++++-- src/matrix/net/HomeServerApi.ts | 4 +++ src/matrix/room/BaseRoom.js | 10 ++++++- src/matrix/room/timeline/Timeline.js | 30 +++++++++++++++++-- .../room/timeline/entries/EventEntry.js | 9 ++++++ 5 files changed, 60 insertions(+), 5 deletions(-) diff --git a/src/domain/session/room/timeline/ReactionsViewModel.js b/src/domain/session/room/timeline/ReactionsViewModel.js index ad29c01a..bb5511e6 100644 --- a/src/domain/session/room/timeline/ReactionsViewModel.js +++ b/src/domain/session/room/timeline/ReactionsViewModel.js @@ -254,8 +254,16 @@ export function tests() { // 2. setup queue & timeline const queue = new SendQueue({roomId, storage, hsApi: new MockHomeServer().api}); const powerLevelsObservable = new ObservableValue(new PowerLevels({ ownUserId: alice, membership: "join" })); - const timeline = new Timeline({roomId, storage, fragmentIdComparer, - clock: new MockClock(), pendingEvents: queue.pendingEvents, powerLevelsObservable}); + const timeline = new Timeline({ + roomId, + storage, + fragmentIdComparer, + clock: new MockClock(), + pendingEvents: queue.pendingEvents, + powerLevelsObservable, + fetchEventFromHomeserver: () => {}, + fetchEventFromStorage: () => {} + }); // 3. load the timeline, which will load the message with the reaction await timeline.load(new User(alice), "join", new NullLogItem()); const tiles = mapMessageEntriesToBaseMessageTile(timeline, queue); diff --git a/src/matrix/net/HomeServerApi.ts b/src/matrix/net/HomeServerApi.ts index bacf26b0..db5e281b 100644 --- a/src/matrix/net/HomeServerApi.ts +++ b/src/matrix/net/HomeServerApi.ts @@ -128,6 +128,10 @@ export class HomeServerApi { return this._get("/sync", {since, timeout, filter}, undefined, options); } + event(roomId, eventId) { + return this._get(`/rooms/${encodeURIComponent(roomId)}/event/${encodeURIComponent(eventId)}`); + } + // params is from, dir and optionally to, limit, filter. messages(roomId: string, params: Record, options?: IRequestOptions): IHomeServerRequest { return this._get(`/rooms/${encodeURIComponent(roomId)}/messages`, params, undefined, options); diff --git a/src/matrix/room/BaseRoom.js b/src/matrix/room/BaseRoom.js index 93973b71..b483d65b 100644 --- a/src/matrix/room/BaseRoom.js +++ b/src/matrix/room/BaseRoom.js @@ -501,7 +501,9 @@ export class BaseRoom extends EventEmitter { }, clock: this._platform.clock, logger: this._platform.logger, - powerLevelsObservable: await this.observePowerLevels() + powerLevelsObservable: await this.observePowerLevels(), + fetchEventFromStorage: eventId => this._readEventById(eventId), + fetchEventFromHomeserver: eventId => this._getEventFromHomeserver(eventId) }); try { if (this._roomEncryption) { @@ -559,6 +561,12 @@ export class BaseRoom extends EventEmitter { } } + async _getEventFromHomeserver(eventId) { + const response = await this._hsApi.event(this._roomId, eventId).response(); + const entry = new EventEntry({ event: response }, this._fragmentIdComparer); + return entry; + } + dispose() { this._roomEncryption?.dispose(); diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 04adde0d..bd3f9e8f 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -25,8 +25,10 @@ import {getRelation, ANNOTATION_RELATION_TYPE} from "./relations.js"; import {REDACTION_TYPE} from "../common.js"; export class Timeline { - constructor({roomId, storage, closeCallback, fragmentIdComparer, pendingEvents, clock, powerLevelsObservable}) { + constructor({roomId, storage, closeCallback, fragmentIdComparer, pendingEvents, clock, powerLevelsObservable, fetchEventFromStorage, fetchEventFromHomeserver}) { this._roomId = roomId; + this._fetchEventFromStorage = fetchEventFromStorage; + this._fetchEventFromHomeserver = fetchEventFromHomeserver; this._storage = storage; this._closeCallback = closeCallback; this._fragmentIdComparer = fragmentIdComparer; @@ -78,6 +80,7 @@ export class Timeline { const readerRequest = this._disposables.track(this._timelineReader.readFromEnd(20, txn, log)); try { const entries = await readerRequest.complete(); + await this._loadRelatedEvents(entries); this._setupEntries(entries); } finally { this._disposables.disposeTracked(readerRequest); @@ -211,8 +214,9 @@ export class Timeline { } /** @package */ - replaceEntries(entries) { + replaceEntries(entries) { this._addLocalRelationsToNewRemoteEntries(entries); + this._loadRelatedEvents(entries); for (const entry of entries) { try { this._remoteEntries.getAndUpdate(entry, Timeline._entryUpdater); @@ -236,8 +240,30 @@ export class Timeline { /** @package */ addEntries(newEntries) { this._addLocalRelationsToNewRemoteEntries(newEntries); + this._loadRelatedEvents(newEntries); this._remoteEntries.setManySorted(newEntries); } + + async _loadRelatedEvents(entries) { + const filteredEntries = entries.filter(e => !!e.relation); + for (const entry of filteredEntries) { + const id = entry.relatedEventId; + let relatedEvent; + // find in remote events + relatedEvent = this.getByEventId(id); + // find in storage + if (!relatedEvent) { + relatedEvent = await this._fetchEventFromStorage(id); + } + // fetch from hs + if (!relatedEvent) { + relatedEvent = await this._fetchEventFromHomeserver(id); + } + if (relatedEvent) { + entry.setRelatedEntry(relatedEvent); + } + } + } // tries to prepend `amount` entries to the `entries` list. /** diff --git a/src/matrix/room/timeline/entries/EventEntry.js b/src/matrix/room/timeline/entries/EventEntry.js index 89d3f379..e626b02f 100644 --- a/src/matrix/room/timeline/entries/EventEntry.js +++ b/src/matrix/room/timeline/entries/EventEntry.js @@ -24,6 +24,7 @@ export class EventEntry extends BaseEventEntry { this._eventEntry = eventEntry; this._decryptionError = null; this._decryptionResult = null; + this._relatedEntry = null; } clone() { @@ -41,6 +42,10 @@ export class EventEntry extends BaseEventEntry { } } + setRelatedEntry(entry) { + this._relatedEntry = entry; + } + get event() { return this._eventEntry.event; } @@ -122,6 +127,10 @@ export class EventEntry extends BaseEventEntry { return getRelatedEventId(this.event); } + get relatedEntry() { + return this._relatedEntry; + } + get isRedacted() { return super.isRedacted || isRedacted(this._eventEntry.event); } From 2265d198a6d8f9d099bbd1c869516d2715a9bded Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 6 Dec 2021 11:39:06 +0530 Subject: [PATCH 02/82] Formatting fix --- src/matrix/room/timeline/Timeline.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index bd3f9e8f..6ccb998d 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -214,7 +214,7 @@ export class Timeline { } /** @package */ - replaceEntries(entries) { + replaceEntries(entries) { this._addLocalRelationsToNewRemoteEntries(entries); this._loadRelatedEvents(entries); for (const entry of entries) { From e9011426612f824a3732747b1713b8f65c3fcfa7 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 6 Dec 2021 11:49:00 +0530 Subject: [PATCH 03/82] await on loading related events --- src/matrix/room/timeline/Timeline.js | 29 ++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 6ccb998d..352ba91d 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -214,9 +214,9 @@ export class Timeline { } /** @package */ - replaceEntries(entries) { + async replaceEntries(entries) { this._addLocalRelationsToNewRemoteEntries(entries); - this._loadRelatedEvents(entries); + await this._loadRelatedEvents(entries); for (const entry of entries) { try { this._remoteEntries.getAndUpdate(entry, Timeline._entryUpdater); @@ -238,9 +238,9 @@ export class Timeline { } /** @package */ - addEntries(newEntries) { + async addEntries(newEntries) { this._addLocalRelationsToNewRemoteEntries(newEntries); - this._loadRelatedEvents(newEntries); + await this._loadRelatedEvents(newEntries); this._remoteEntries.setManySorted(newEntries); } @@ -405,10 +405,10 @@ export function tests() { // 2. test replaceEntries and addEntries don't fail const event1 = withTextBody("hi!", withSender(bob, createEvent("m.room.message", "!abc"))); const entry1 = new EventEntry({event: event1, fragmentId: 1, eventIndex: 1}, fragmentIdComparer); - timeline.replaceEntries([entry1]); + await timeline.replaceEntries([entry1]); const event2 = withTextBody("hi bob!", withSender(alice, createEvent("m.room.message", "!def"))); const entry2 = new EventEntry({event: event2, fragmentId: 1, eventIndex: 2}, fragmentIdComparer); - timeline.addEntries([entry2]); + await timeline.addEntries([entry2]); // 3. add local relation (redaction) pendingEvents.append(new PendingEvent({data: { roomId, @@ -432,7 +432,7 @@ export function tests() { await timeline.load(new User(bob), "join", new NullLogItem()); timeline.entries.subscribe(new ListObserver()); const event = withTextBody("hi bob!", withSender(alice, createEvent("m.room.message", "!abc"))); - timeline.addEntries([new EventEntry({event, fragmentId: 1, eventIndex: 2}, fragmentIdComparer)]); + await timeline.addEntries([new EventEntry({event, fragmentId: 1, eventIndex: 2}, fragmentIdComparer)]); let entry = getIndexFromIterable(timeline.entries, 0); // 2. add local reaction pendingEvents.append(new PendingEvent({data: { @@ -503,7 +503,7 @@ export function tests() { await timeline.load(new User(bob), "join", new NullLogItem()); timeline.entries.subscribe(new ListObserver()); // 3. add message to timeline - timeline.addEntries([messageEntry]); + await timeline.addEntries([messageEntry]); const entry = getIndexFromIterable(timeline.entries, 0); assert.equal(entry, messageEntry); assert.equal(entry.annotations["👋"].count, 1); @@ -512,7 +512,8 @@ export function tests() { // 1. setup timeline const pendingEvents = new ObservableArray(); const timeline = new Timeline({roomId, storage: await createMockStorage(), - closeCallback: () => {}, fragmentIdComparer, pendingEvents, clock: new MockClock()}); + closeCallback: () => { }, fragmentIdComparer, pendingEvents, clock: new MockClock(), + fetchEventFromStorage: () => undefined, fetchEventFromHomeserver: () => undefined}); await timeline.load(new User(bob), "join", new NullLogItem()); timeline.entries.subscribe(new ListObserver()); // 2. add message and reaction to timeline @@ -524,7 +525,7 @@ export function tests() { event: withContent(createAnnotation(messageEntry.id, "👋"), createEvent("m.reaction", "!def", bob)), fragmentId: 1, eventIndex: 3, roomId }, fragmentIdComparer); - timeline.addEntries([messageEntry, reactionEntry]); + await timeline.addEntries([messageEntry, reactionEntry]); // 3. redact reaction pendingEvents.append(new PendingEvent({data: { roomId, @@ -557,7 +558,7 @@ export function tests() { }})); await poll(() => timeline.entries.length === 1); // 3. add remote reaction target - timeline.addEntries([messageEntry]); + await timeline.addEntries([messageEntry]); await poll(() => timeline.entries.length === 2); const entry = getIndexFromIterable(timeline.entries, 0); assert.equal(entry, messageEntry); @@ -591,7 +592,7 @@ export function tests() { }})); await poll(() => timeline.entries.length === 1); // 4. add reaction target - timeline.addEntries([new EventEntry({ + await timeline.addEntries([new EventEntry({ event: withTextBody("hi bob!", createEvent("m.room.message", messageId, alice)), fragmentId: 1, eventIndex: 2}, fragmentIdComparer) ]); @@ -614,14 +615,14 @@ export function tests() { const timeline = new Timeline({roomId, storage: await createMockStorage(), closeCallback: () => {}, fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock()}); await timeline.load(new User(alice), "join", new NullLogItem()); - timeline.addEntries([decryptedEntry]); + await timeline.addEntries([decryptedEntry]); const observer = new ListObserver(); timeline.entries.subscribe(observer); // 3. replace the entry with one that is not decrypted // (as would happen when receiving a reaction, // as it does not rerun the decryption) // and check that the decrypted content is preserved - timeline.replaceEntries([encryptedEntry]); + await timeline.replaceEntries([encryptedEntry]); const {value, type} = await observer.next(); assert.equal(type, "update"); assert.equal(value.eventType, "m.room.message"); From 0c42f53a2f63e307bf7d62baa6b2bc63e3b5f2d9 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 7 Dec 2021 12:18:51 +0530 Subject: [PATCH 04/82] Implement context endpoint --- src/matrix/net/HomeServerApi.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/matrix/net/HomeServerApi.ts b/src/matrix/net/HomeServerApi.ts index db5e281b..3a6517cd 100644 --- a/src/matrix/net/HomeServerApi.ts +++ b/src/matrix/net/HomeServerApi.ts @@ -128,8 +128,8 @@ export class HomeServerApi { return this._get("/sync", {since, timeout, filter}, undefined, options); } - event(roomId, eventId) { - return this._get(`/rooms/${encodeURIComponent(roomId)}/event/${encodeURIComponent(eventId)}`); + context(roomId: string, eventId: string, limit: number, filter: string): IHomeServerRequest { + return this._get(`/rooms/${encodeURIComponent(roomId)}/context/${encodeURIComponent(eventId)}`, {filter, limit}); } // params is from, dir and optionally to, limit, filter. From 696980aca479428ed674b89cf1bfae28eeae4758 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 7 Dec 2021 12:19:11 +0530 Subject: [PATCH 05/82] Parse display name and avatar of event --- src/matrix/room/BaseRoom.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/matrix/room/BaseRoom.js b/src/matrix/room/BaseRoom.js index b483d65b..1c1e3720 100644 --- a/src/matrix/room/BaseRoom.js +++ b/src/matrix/room/BaseRoom.js @@ -562,9 +562,15 @@ export class BaseRoom extends EventEmitter { } async _getEventFromHomeserver(eventId) { - const response = await this._hsApi.event(this._roomId, eventId).response(); - const entry = new EventEntry({ event: response }, this._fragmentIdComparer); - return entry; + const response = await this._hsApi.context(this._roomId, eventId, 0).response(); + const sender = response.event.sender; + const member = response.state.find(e => e.type === "m.room.member" && e.user_id === sender); + const entry = { + event: response.event, + displayName: member.content.displayname, + avatarUrl: member.content.avatar_url + }; + return new EventEntry(entry, this._fragmentIdComparer); } From 764e38f8c9ff23562e011571c53991a889dd9109 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 7 Dec 2021 12:25:56 +0530 Subject: [PATCH 06/82] Use 'context' instead of 'related' --- src/matrix/room/timeline/Timeline.js | 16 ++++++++-------- src/matrix/room/timeline/entries/EventEntry.js | 10 +++++----- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 352ba91d..2baa0c80 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -248,19 +248,19 @@ export class Timeline { const filteredEntries = entries.filter(e => !!e.relation); for (const entry of filteredEntries) { const id = entry.relatedEventId; - let relatedEvent; + let contextEvent; // find in remote events - relatedEvent = this.getByEventId(id); + contextEvent = this.getByEventId(id); // find in storage - if (!relatedEvent) { - relatedEvent = await this._fetchEventFromStorage(id); + if (!contextEvent) { + contextEvent = await this._fetchEventFromStorage(id); } // fetch from hs - if (!relatedEvent) { - relatedEvent = await this._fetchEventFromHomeserver(id); + if (!contextEvent) { + contextEvent = await this._fetchEventFromHomeserver(id); } - if (relatedEvent) { - entry.setRelatedEntry(relatedEvent); + if (contextEvent) { + entry.setContextEntry(contextEvent); } } } diff --git a/src/matrix/room/timeline/entries/EventEntry.js b/src/matrix/room/timeline/entries/EventEntry.js index e626b02f..229c6940 100644 --- a/src/matrix/room/timeline/entries/EventEntry.js +++ b/src/matrix/room/timeline/entries/EventEntry.js @@ -24,7 +24,7 @@ export class EventEntry extends BaseEventEntry { this._eventEntry = eventEntry; this._decryptionError = null; this._decryptionResult = null; - this._relatedEntry = null; + this._contextEntry = null; } clone() { @@ -42,8 +42,8 @@ export class EventEntry extends BaseEventEntry { } } - setRelatedEntry(entry) { - this._relatedEntry = entry; + setContextEntry(entry) { + this._contextEntry = entry; } get event() { @@ -127,8 +127,8 @@ export class EventEntry extends BaseEventEntry { return getRelatedEventId(this.event); } - get relatedEntry() { - return this._relatedEntry; + get contextEntry() { + return this._contextEntry; } get isRedacted() { From 06864a65b76d33a77c91ceb1f3d1673bf20a77bb Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 7 Dec 2021 13:00:10 +0530 Subject: [PATCH 07/82] Add contextEventId --- src/matrix/room/timeline/Timeline.js | 4 ++-- src/matrix/room/timeline/entries/EventEntry.js | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 2baa0c80..1488a307 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -245,9 +245,9 @@ export class Timeline { } async _loadRelatedEvents(entries) { - const filteredEntries = entries.filter(e => !!e.relation); + const filteredEntries = entries.filter(e => !!e.contextEventId); for (const entry of filteredEntries) { - const id = entry.relatedEventId; + const id = entry.contextEventId; let contextEvent; // find in remote events contextEvent = this.getByEventId(id); diff --git a/src/matrix/room/timeline/entries/EventEntry.js b/src/matrix/room/timeline/entries/EventEntry.js index 229c6940..906bd108 100644 --- a/src/matrix/room/timeline/entries/EventEntry.js +++ b/src/matrix/room/timeline/entries/EventEntry.js @@ -127,6 +127,15 @@ export class EventEntry extends BaseEventEntry { return getRelatedEventId(this.event); } + // return a related event-id only if this entry is a reply + // excludes relations like redaction + get contextEventId() { + if (this.isReply) { + return this.relatedEventId; + } + return null; + } + get contextEntry() { return this._contextEntry; } From d191b327c63bdcd5fddfecbee98515652759de42 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 7 Dec 2021 13:04:01 +0530 Subject: [PATCH 08/82] Change comment --- src/matrix/room/timeline/entries/EventEntry.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/matrix/room/timeline/entries/EventEntry.js b/src/matrix/room/timeline/entries/EventEntry.js index 906bd108..064d7f01 100644 --- a/src/matrix/room/timeline/entries/EventEntry.js +++ b/src/matrix/room/timeline/entries/EventEntry.js @@ -127,8 +127,7 @@ export class EventEntry extends BaseEventEntry { return getRelatedEventId(this.event); } - // return a related event-id only if this entry is a reply - // excludes relations like redaction + // similar to relatedEventID but excludes relations like redaction get contextEventId() { if (this.isReply) { return this.relatedEventId; From 053dcf39a5fb4891cd235336c4a4a87b10025e17 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 7 Dec 2021 16:01:30 +0530 Subject: [PATCH 09/82] Use NonPersistedEventEntry --- src/matrix/room/BaseRoom.js | 3 +- .../entries/NonPersistedEventEntry.js | 28 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 src/matrix/room/timeline/entries/NonPersistedEventEntry.js diff --git a/src/matrix/room/BaseRoom.js b/src/matrix/room/BaseRoom.js index 1c1e3720..9e47a7d8 100644 --- a/src/matrix/room/BaseRoom.js +++ b/src/matrix/room/BaseRoom.js @@ -30,6 +30,7 @@ import {DecryptionSource} from "../e2ee/common.js"; import {ensureLogItem} from "../../logging/utils"; import {PowerLevels} from "./PowerLevels.js"; import {RetainedObservableValue} from "../../observable/ObservableValue"; +import {NonPersistedEventEntry} from "./timeline/entries/NonPersistedEventEntry"; const EVENT_ENCRYPTED_TYPE = "m.room.encrypted"; @@ -570,7 +571,7 @@ export class BaseRoom extends EventEmitter { displayName: member.content.displayname, avatarUrl: member.content.avatar_url }; - return new EventEntry(entry, this._fragmentIdComparer); + return new NonPersistedEventEntry(entry, this._fragmentIdComparer); } diff --git a/src/matrix/room/timeline/entries/NonPersistedEventEntry.js b/src/matrix/room/timeline/entries/NonPersistedEventEntry.js new file mode 100644 index 00000000..f3bae9d2 --- /dev/null +++ b/src/matrix/room/timeline/entries/NonPersistedEventEntry.js @@ -0,0 +1,28 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {EventEntry} from "./EventEntry.js"; + +// EventEntry but without the two properties that are populated via SyncWriter +export class NonPersistedEventEntry extends EventEntry { + get fragmentId() { + throw new Error("Cannot access fragmentId for non-persisted EventEntry"); + } + + get entryIndex() { + throw new Error("Cannot access entryIndex for non-persisted EventEntry"); + } +} From 7cc3d4b91a5667e64e66f5946074803906639c58 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 7 Dec 2021 20:26:54 +0530 Subject: [PATCH 10/82] Emit updated entries --- src/matrix/room/timeline/Timeline.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 1488a307..bc81008d 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -80,7 +80,7 @@ export class Timeline { const readerRequest = this._disposables.track(this._timelineReader.readFromEnd(20, txn, log)); try { const entries = await readerRequest.complete(); - await this._loadRelatedEvents(entries); + this._loadRelatedEvents(entries); this._setupEntries(entries); } finally { this._disposables.disposeTracked(readerRequest); @@ -214,9 +214,9 @@ export class Timeline { } /** @package */ - async replaceEntries(entries) { + replaceEntries(entries) { this._addLocalRelationsToNewRemoteEntries(entries); - await this._loadRelatedEvents(entries); + this._loadRelatedEvents(entries); for (const entry of entries) { try { this._remoteEntries.getAndUpdate(entry, Timeline._entryUpdater); @@ -238,9 +238,9 @@ export class Timeline { } /** @package */ - async addEntries(newEntries) { + addEntries(newEntries) { this._addLocalRelationsToNewRemoteEntries(newEntries); - await this._loadRelatedEvents(newEntries); + this._loadRelatedEvents(newEntries); this._remoteEntries.setManySorted(newEntries); } @@ -261,6 +261,8 @@ export class Timeline { } if (contextEvent) { entry.setContextEntry(contextEvent); + // emit this change + this._remoteEntries.update(entry); } } } From c690de9f7b0b44f11a9b77b1ee2c1ee697774f0d Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 7 Dec 2021 23:07:10 +0530 Subject: [PATCH 11/82] Support decryption on entries fetched from hs --- src/matrix/room/BaseRoom.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/matrix/room/BaseRoom.js b/src/matrix/room/BaseRoom.js index 9e47a7d8..d5cf70b9 100644 --- a/src/matrix/room/BaseRoom.js +++ b/src/matrix/room/BaseRoom.js @@ -571,7 +571,12 @@ export class BaseRoom extends EventEmitter { displayName: member.content.displayname, avatarUrl: member.content.avatar_url }; - return new NonPersistedEventEntry(entry, this._fragmentIdComparer); + const eventEntry = new NonPersistedEventEntry(entry, this._fragmentIdComparer); + if (eventEntry.eventType === EVENT_ENCRYPTED_TYPE) { + const request = this._decryptEntries(DecryptionSource.Timeline, [eventEntry]); + await request.complete(); + } + return eventEntry; } From ea89c272b9183f62212f842347e6ccd9d093fcce Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 8 Dec 2021 17:14:17 +0530 Subject: [PATCH 12/82] Support redaction changes in remoteEntries --- src/matrix/room/timeline/Timeline.js | 5 +++++ src/matrix/room/timeline/entries/EventEntry.js | 13 +++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index bc81008d..6d4f0398 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -209,6 +209,8 @@ export class Timeline { // used in replaceEntries static _entryUpdater(existingEntry, entry) { + // ensure dependents point to the new entry instead of existingEntry + existingEntry.dependents?.forEach(event => event.setContextEntry(entry)); entry.updateFrom(existingEntry); return entry; } @@ -220,6 +222,8 @@ export class Timeline { for (const entry of entries) { try { this._remoteEntries.getAndUpdate(entry, Timeline._entryUpdater); + // Since this entry changed, all dependent entries should be updated + entry.dependents?.forEach(e => this._findAndUpdateRelatedEntry(null, e.id, () => true)); } catch (err) { if (err.name === "CompareError") { // see FragmentIdComparer, if the replacing entry is on a fragment @@ -251,6 +255,7 @@ export class Timeline { let contextEvent; // find in remote events contextEvent = this.getByEventId(id); + contextEvent?.addDependent(entry); // find in storage if (!contextEvent) { contextEvent = await this._fetchEventFromStorage(id); diff --git a/src/matrix/room/timeline/entries/EventEntry.js b/src/matrix/room/timeline/entries/EventEntry.js index 064d7f01..c656dfb5 100644 --- a/src/matrix/room/timeline/entries/EventEntry.js +++ b/src/matrix/room/timeline/entries/EventEntry.js @@ -25,6 +25,7 @@ export class EventEntry extends BaseEventEntry { this._decryptionError = null; this._decryptionResult = null; this._contextEntry = null; + this._dependents = null; } clone() { @@ -40,12 +41,24 @@ export class EventEntry extends BaseEventEntry { if (other._decryptionError && !this._decryptionError) { this._decryptionError = other._decryptionError; } + this._dependents = other._dependents; } setContextEntry(entry) { this._contextEntry = entry; } + addDependent(entry) { + if (!this._dependents) { + this._dependents = []; + } + this._dependents.push(entry); + } + + get dependents() { + return this._dependents; + } + get event() { return this._eventEntry.event; } From 4a81e06e96c5b826d0e449759823d26cdd4d0af0 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 9 Dec 2021 13:48:55 +0530 Subject: [PATCH 13/82] Track fetched entries for redactions --- src/matrix/room/timeline/Timeline.js | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 6d4f0398..6a9a9473 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -45,6 +45,8 @@ export class Timeline { }); this._readerRequest = null; this._allEntries = null; + // Stores event entries that we fetch for reply previews + this._fetchedEventEntries = []; this.initializePowerLevels(powerLevelsObservable); } @@ -218,6 +220,7 @@ export class Timeline { /** @package */ replaceEntries(entries) { this._addLocalRelationsToNewRemoteEntries(entries); + this._updateFetchedEntries(entries); this._loadRelatedEvents(entries); for (const entry of entries) { try { @@ -244,20 +247,31 @@ export class Timeline { /** @package */ addEntries(newEntries) { this._addLocalRelationsToNewRemoteEntries(newEntries); + this._updateFetchedEntries(newEntries); this._loadRelatedEvents(newEntries); this._remoteEntries.setManySorted(newEntries); } + _updateFetchedEntries(entries) { + for (const entry of entries) { + const relatedEntry = this._fetchedEventEntries.find(e => e.id === entry.relatedEventId); + if (relatedEntry?.addLocalRelation(entry)) { + relatedEntry.dependents.forEach(e => this._findAndUpdateRelatedEntry(null, e.id, () => true)); + } + } + } + async _loadRelatedEvents(entries) { const filteredEntries = entries.filter(e => !!e.contextEventId); for (const entry of filteredEntries) { const id = entry.contextEventId; + let needToTrack = false; let contextEvent; // find in remote events contextEvent = this.getByEventId(id); - contextEvent?.addDependent(entry); // find in storage if (!contextEvent) { + needToTrack = true; contextEvent = await this._fetchEventFromStorage(id); } // fetch from hs @@ -265,6 +279,12 @@ export class Timeline { contextEvent = await this._fetchEventFromHomeserver(id); } if (contextEvent) { + if (needToTrack) { + // this entry was created from storage/hs, so it's not tracked by remoteEntries + // we track them here for redactions + this._fetchedEventEntries.push(contextEvent); + } + contextEvent.addDependent(entry); entry.setContextEntry(contextEvent); // emit this change this._remoteEntries.update(entry); From 7a91dd95953b2ea889db485456454e1e36b1b922 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 9 Dec 2021 14:56:59 +0530 Subject: [PATCH 14/82] Improve comment --- src/matrix/room/timeline/Timeline.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 6a9a9473..5044989c 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -281,7 +281,7 @@ export class Timeline { if (contextEvent) { if (needToTrack) { // this entry was created from storage/hs, so it's not tracked by remoteEntries - // we track them here for redactions + // we track them here so that we can update reply preview of dependents on redaction this._fetchedEventEntries.push(contextEvent); } contextEvent.addDependent(entry); From 287212956b8f1efb494b0fd37035afb06aa76e9d Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 10 Dec 2021 12:47:52 +0530 Subject: [PATCH 15/82] findAndUpdate instead of update --- src/matrix/room/timeline/Timeline.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 5044989c..3c843d26 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -287,7 +287,7 @@ export class Timeline { contextEvent.addDependent(entry); entry.setContextEntry(contextEvent); // emit this change - this._remoteEntries.update(entry); + this._findAndUpdateRelatedEntry(null, entry.id, () => true); } } } From 4a6293dcdc2beb4b7b959cd3b2617240575814d9 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 12 Dec 2021 18:34:02 +0530 Subject: [PATCH 16/82] Made code more readable --- src/matrix/room/timeline/Timeline.js | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 3c843d26..0da418ce 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -262,28 +262,17 @@ export class Timeline { } async _loadRelatedEvents(entries) { - const filteredEntries = entries.filter(e => !!e.contextEventId); - for (const entry of filteredEntries) { + const entriesNeedingContext = entries.filter(e => !!e.contextEventId); + for (const entry of entriesNeedingContext) { const id = entry.contextEventId; - let needToTrack = false; - let contextEvent; - // find in remote events - contextEvent = this.getByEventId(id); - // find in storage + let contextEvent = this.getByEventId(id); if (!contextEvent) { - needToTrack = true; - contextEvent = await this._fetchEventFromStorage(id); - } - // fetch from hs - if (!contextEvent) { - contextEvent = await this._fetchEventFromHomeserver(id); + contextEvent = await this._fetchEventFromStorage(id) ?? await this._fetchEventFromHomeserver(id); + // this entry was created from storage/hs, so it's not tracked by remoteEntries + // we track them here so that we can update reply preview of dependents on redaction + this._fetchedEventEntries.push(contextEvent); } if (contextEvent) { - if (needToTrack) { - // this entry was created from storage/hs, so it's not tracked by remoteEntries - // we track them here so that we can update reply preview of dependents on redaction - this._fetchedEventEntries.push(contextEvent); - } contextEvent.addDependent(entry); entry.setContextEntry(contextEvent); // emit this change From 0da94e51e0e96c8e606e409fa2e7b6d72f5f11cb Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 12 Dec 2021 18:52:45 +0530 Subject: [PATCH 17/82] Use map and fetch from Map if available --- src/matrix/room/timeline/Timeline.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 0da418ce..0551d223 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -46,7 +46,7 @@ export class Timeline { this._readerRequest = null; this._allEntries = null; // Stores event entries that we fetch for reply previews - this._fetchedEventEntries = []; + this._fetchedEventEntries = new Map(); this.initializePowerLevels(powerLevelsObservable); } @@ -254,7 +254,7 @@ export class Timeline { _updateFetchedEntries(entries) { for (const entry of entries) { - const relatedEntry = this._fetchedEventEntries.find(e => e.id === entry.relatedEventId); + const relatedEntry = this._fetchedEventEntries.get(entry.relatedEventId); if (relatedEntry?.addLocalRelation(entry)) { relatedEntry.dependents.forEach(e => this._findAndUpdateRelatedEntry(null, e.id, () => true)); } @@ -265,12 +265,12 @@ export class Timeline { const entriesNeedingContext = entries.filter(e => !!e.contextEventId); for (const entry of entriesNeedingContext) { const id = entry.contextEventId; - let contextEvent = this.getByEventId(id); + let contextEvent = this._getTrackedEvent(id); if (!contextEvent) { contextEvent = await this._fetchEventFromStorage(id) ?? await this._fetchEventFromHomeserver(id); // this entry was created from storage/hs, so it's not tracked by remoteEntries // we track them here so that we can update reply preview of dependents on redaction - this._fetchedEventEntries.push(contextEvent); + this._fetchedEventEntries.set(id, contextEvent); } if (contextEvent) { contextEvent.addDependent(entry); @@ -280,6 +280,10 @@ export class Timeline { } } } + + _getTrackedEvent(id) { + return this.getByEventId(id) ?? this._fetchedEventEntries.get(id); + } // tries to prepend `amount` entries to the `entries` list. /** From 51b7b2108268dfaa8b2c8395649fd1a7c3e87ac0 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 12 Dec 2021 20:57:41 +0530 Subject: [PATCH 18/82] Implement readById() in TimelineReader --- src/matrix/room/timeline/Timeline.js | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 0551d223..3f48e5a8 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -23,12 +23,12 @@ import {PendingEventEntry} from "./entries/PendingEventEntry.js"; import {RoomMember} from "../members/RoomMember.js"; import {getRelation, ANNOTATION_RELATION_TYPE} from "./relations.js"; import {REDACTION_TYPE} from "../common.js"; +import {NonPersistedEventEntry} from "./entries/NonPersistedEventEntry.js"; +import {DecryptionSource} from "../../e2ee/common.js"; export class Timeline { - constructor({roomId, storage, closeCallback, fragmentIdComparer, pendingEvents, clock, powerLevelsObservable, fetchEventFromStorage, fetchEventFromHomeserver}) { + constructor({roomId, storage, closeCallback, fragmentIdComparer, pendingEvents, clock, powerLevelsObservable, hsApi}) { this._roomId = roomId; - this._fetchEventFromStorage = fetchEventFromStorage; - this._fetchEventFromHomeserver = fetchEventFromHomeserver; this._storage = storage; this._closeCallback = closeCallback; this._fragmentIdComparer = fragmentIdComparer; @@ -47,6 +47,8 @@ export class Timeline { this._allEntries = null; // Stores event entries that we fetch for reply previews this._fetchedEventEntries = new Map(); + this._decryptEntries = null; + this._hsApi = hsApi; this.initializePowerLevels(powerLevelsObservable); } @@ -267,7 +269,7 @@ export class Timeline { const id = entry.contextEventId; let contextEvent = this._getTrackedEvent(id); if (!contextEvent) { - contextEvent = await this._fetchEventFromStorage(id) ?? await this._fetchEventFromHomeserver(id); + contextEvent = await this._timelineReader.readById(id) ?? await this._getEventFromHomeserver(id); // this entry was created from storage/hs, so it's not tracked by remoteEntries // we track them here so that we can update reply preview of dependents on redaction this._fetchedEventEntries.set(id, contextEvent); @@ -284,6 +286,23 @@ export class Timeline { _getTrackedEvent(id) { return this.getByEventId(id) ?? this._fetchedEventEntries.get(id); } + + async _getEventFromHomeserver(eventId) { + const response = await this._hsApi.context(this._roomId, eventId, 0).response(); + const sender = response.event.sender; + const member = response.state.find(e => e.type === "m.room.member" && e.user_id === sender); + const entry = { + event: response.event, + displayName: member.content.displayname, + avatarUrl: member.content.avatar_url + }; + const eventEntry = new NonPersistedEventEntry(entry, this._fragmentIdComparer); + if (this._decryptEntries) { + const request = this._decryptEntries(DecryptionSource.Timeline, [eventEntry]); + await request.complete(); + } + return eventEntry; + } // tries to prepend `amount` entries to the `entries` list. /** @@ -374,6 +393,7 @@ export class Timeline { /** @internal */ enableEncryption(decryptEntries) { + this._decryptEntries = decryptEntries; this._timelineReader.enableEncryption(decryptEntries); } From 5c0bbdd4c822cea72c3db89dc312b04e0005ff69 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 12 Dec 2021 20:58:03 +0530 Subject: [PATCH 19/82] Move methods into Timeline --- src/matrix/room/BaseRoom.js | 40 +++---------------- .../timeline/persistence/TimelineReader.js | 17 ++++++++ 2 files changed, 22 insertions(+), 35 deletions(-) diff --git a/src/matrix/room/BaseRoom.js b/src/matrix/room/BaseRoom.js index d5cf70b9..39232ae5 100644 --- a/src/matrix/room/BaseRoom.js +++ b/src/matrix/room/BaseRoom.js @@ -30,7 +30,7 @@ import {DecryptionSource} from "../e2ee/common.js"; import {ensureLogItem} from "../../logging/utils"; import {PowerLevels} from "./PowerLevels.js"; import {RetainedObservableValue} from "../../observable/ObservableValue"; -import {NonPersistedEventEntry} from "./timeline/entries/NonPersistedEventEntry"; +import {TimelineReader} from "./timeline/persistence/TimelineReader"; const EVENT_ENCRYPTED_TYPE = "m.room.encrypted"; @@ -503,8 +503,7 @@ export class BaseRoom extends EventEmitter { clock: this._platform.clock, logger: this._platform.logger, powerLevelsObservable: await this.observePowerLevels(), - fetchEventFromStorage: eventId => this._readEventById(eventId), - fetchEventFromHomeserver: eventId => this._getEventFromHomeserver(eventId) + hsApi: this._hsApi }); try { if (this._roomEncryption) { @@ -546,40 +545,11 @@ export class BaseRoom extends EventEmitter { } async _readEventById(eventId) { - let stores = [this._storage.storeNames.timelineEvents]; - if (this.isEncrypted) { - stores.push(this._storage.storeNames.inboundGroupSessions); - } - const txn = await this._storage.readTxn(stores); - const storageEntry = await txn.timelineEvents.getByEventId(this._roomId, eventId); - if (storageEntry) { - const entry = new EventEntry(storageEntry, this._fragmentIdComparer); - if (entry.eventType === EVENT_ENCRYPTED_TYPE) { - const request = this._decryptEntries(DecryptionSource.Timeline, [entry], txn); - await request.complete(); - } - return entry; - } + const reader = new TimelineReader({ roomId: this._roomId, storage: this._storage, fragmentIdComparer: this._fragmentIdComparer }); + const entry = await reader.readById(eventId); + return entry; } - async _getEventFromHomeserver(eventId) { - const response = await this._hsApi.context(this._roomId, eventId, 0).response(); - const sender = response.event.sender; - const member = response.state.find(e => e.type === "m.room.member" && e.user_id === sender); - const entry = { - event: response.event, - displayName: member.content.displayname, - avatarUrl: member.content.avatar_url - }; - const eventEntry = new NonPersistedEventEntry(entry, this._fragmentIdComparer); - if (eventEntry.eventType === EVENT_ENCRYPTED_TYPE) { - const request = this._decryptEntries(DecryptionSource.Timeline, [eventEntry]); - await request.complete(); - } - return eventEntry; - } - - dispose() { this._roomEncryption?.dispose(); this._timeline?.dispose(); diff --git a/src/matrix/room/timeline/persistence/TimelineReader.js b/src/matrix/room/timeline/persistence/TimelineReader.js index ffad80ec..dd7474e7 100644 --- a/src/matrix/room/timeline/persistence/TimelineReader.js +++ b/src/matrix/room/timeline/persistence/TimelineReader.js @@ -132,6 +132,23 @@ export class TimelineReader { }, log); } + async readById(id, log) { + let stores = [this._storage.storeNames.timelineEvents]; + if (this.isEncrypted) { + stores.push(this._storage.storeNames.inboundGroupSessions); + } + const txn = await this._storage.readTxn(stores); // todo: can we just use this.readTxnStores here? probably + const storageEntry = await txn.timelineEvents.getByEventId(this._roomId, id); + if (storageEntry) { + const entry = new EventEntry(storageEntry, this._fragmentIdComparer); + if (this._decryptEntries) { + const request = this._decryptEntries([entry], txn, log); + await request.complete(); + } + return entry; + } + } + async _readFrom(eventKey, direction, amount, r, txn, log) { const entries = await readRawTimelineEntriesWithTxn(this._roomId, eventKey, direction, amount, this._fragmentIdComparer, txn); if (this._decryptEntries) { From 39f68e8c2f4947399ab634b5970d81cad22ca007 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 12 Dec 2021 21:01:56 +0530 Subject: [PATCH 20/82] Refactor out magic string --- src/matrix/room/timeline/Timeline.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 3f48e5a8..e7418d30 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -25,6 +25,7 @@ import {getRelation, ANNOTATION_RELATION_TYPE} from "./relations.js"; import {REDACTION_TYPE} from "../common.js"; import {NonPersistedEventEntry} from "./entries/NonPersistedEventEntry.js"; import {DecryptionSource} from "../../e2ee/common.js"; +import {EVENT_TYPE as MEMBER_EVENT_TYPE} from "../members/RoomMember.js"; export class Timeline { constructor({roomId, storage, closeCallback, fragmentIdComparer, pendingEvents, clock, powerLevelsObservable, hsApi}) { @@ -290,7 +291,7 @@ export class Timeline { async _getEventFromHomeserver(eventId) { const response = await this._hsApi.context(this._roomId, eventId, 0).response(); const sender = response.event.sender; - const member = response.state.find(e => e.type === "m.room.member" && e.user_id === sender); + const member = response.state.find(e => e.type === MEMBER_EVENT_TYPE && e.user_id === sender); const entry = { event: response.event, displayName: member.content.displayname, From 544dca3b18d59580a08c80449e7adffc84f9e33b Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 12 Dec 2021 21:12:50 +0530 Subject: [PATCH 21/82] Use _updateEntry --- src/matrix/room/timeline/Timeline.js | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index e7418d30..71f532bb 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -133,28 +133,28 @@ export class Timeline { const params = updater(e); return params ? params : false; }; - this._findAndUpdateRelatedEntry(pee.pendingEvent.relatedTxnId, pee.relatedEventId, updateOrFalse); + this._findAndUpdateEntryById(pee.pendingEvent.relatedTxnId, pee.relatedEventId, updateOrFalse); // also look for a relation target to update with this redaction if (pee.redactingEntry) { // redactingEntry might be a PendingEventEntry or an EventEntry, so don't assume pendingEvent const relatedTxnId = pee.redactingEntry.pendingEvent?.relatedTxnId; - this._findAndUpdateRelatedEntry(relatedTxnId, pee.redactingEntry.relatedEventId, updateOrFalse); + this._findAndUpdateEntryById(relatedTxnId, pee.redactingEntry.relatedEventId, updateOrFalse); } } - _findAndUpdateRelatedEntry(relatedTxnId, relatedEventId, updateOrFalse) { + _findAndUpdateEntryById(txnId, eventId, updateOrFalse) { let found = false; // first, look in local entries based on txn id - if (relatedTxnId) { + if (txnId) { found = this._localEntries.findAndUpdate( - e => e.id === relatedTxnId, + e => e.id === txnId, updateOrFalse, ); } // if not found here, look in remote entries based on event id - if (!found && relatedEventId) { + if (!found && eventId) { this._remoteEntries.findAndUpdate( - e => e.id === relatedEventId, + e => e.id === eventId, updateOrFalse ); } @@ -229,7 +229,7 @@ export class Timeline { try { this._remoteEntries.getAndUpdate(entry, Timeline._entryUpdater); // Since this entry changed, all dependent entries should be updated - entry.dependents?.forEach(e => this._findAndUpdateRelatedEntry(null, e.id, () => true)); + entry.dependents?.forEach(e => this._updateEntry(e)); } catch (err) { if (err.name === "CompareError") { // see FragmentIdComparer, if the replacing entry is on a fragment @@ -259,11 +259,17 @@ export class Timeline { for (const entry of entries) { const relatedEntry = this._fetchedEventEntries.get(entry.relatedEventId); if (relatedEntry?.addLocalRelation(entry)) { - relatedEntry.dependents.forEach(e => this._findAndUpdateRelatedEntry(null, e.id, () => true)); + relatedEntry.dependents.forEach(e => this._updateEntry(e)); } } } + _updateEntry(entry) { + const txnId = entry.isPending ? entry.id : null; + const eventId = entry.isPending ? null : entry.id; + this._findAndUpdateEntryById(txnId, eventId, () => true); + } + async _loadRelatedEvents(entries) { const entriesNeedingContext = entries.filter(e => !!e.contextEventId); for (const entry of entriesNeedingContext) { @@ -279,7 +285,7 @@ export class Timeline { contextEvent.addDependent(entry); entry.setContextEntry(contextEvent); // emit this change - this._findAndUpdateRelatedEntry(null, entry.id, () => true); + this._updateEntry(entry); } } } From d924dbb7235c7ff4e30f0d6cc73750ce5bad9f3c Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 13 Dec 2021 12:12:58 +0530 Subject: [PATCH 22/82] Add explaining comment --- src/matrix/room/timeline/Timeline.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 71f532bb..f58e7b83 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -256,8 +256,10 @@ export class Timeline { } _updateFetchedEntries(entries) { + // Update fetchedEntries based on incoming event, eg: a fetched event being redacted for (const entry of entries) { const relatedEntry = this._fetchedEventEntries.get(entry.relatedEventId); + // todo: can this be called .addRelation instead? if (relatedEntry?.addLocalRelation(entry)) { relatedEntry.dependents.forEach(e => this._updateEntry(e)); } From f5fadf700eddba791e9ef54989a6b0e847c3b776 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 13 Dec 2021 12:45:17 +0530 Subject: [PATCH 23/82] Move event to remoteEntries if needed --- src/matrix/room/timeline/Timeline.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index f58e7b83..1b980fee 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -251,6 +251,7 @@ export class Timeline { addEntries(newEntries) { this._addLocalRelationsToNewRemoteEntries(newEntries); this._updateFetchedEntries(newEntries); + this._moveFetchedEntryToRemoteEntries(newEntries); this._loadRelatedEvents(newEntries); this._remoteEntries.setManySorted(newEntries); } @@ -266,6 +267,17 @@ export class Timeline { } } + _moveFetchedEntryToRemoteEntries(entries) { + // if some entry in entries is also in fetchedEntries, we need to remove it from fetchedEntries + for (const entry of entries) { + const fetchedEntry = this._fetchedEventEntries.get(entry.id); + if (fetchedEntry) { + fetchedEntry.dependents.forEach(e => e.setContextEntry(entry)); + entry.updateFrom(fetchedEntry); + } + } + } + _updateEntry(entry) { const txnId = entry.isPending ? entry.id : null; const eventId = entry.isPending ? null : entry.id; From d1818d2a570ab6640901b8b911a8187229f6e4ed Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 13 Dec 2021 12:52:03 +0530 Subject: [PATCH 24/82] Reuse code in getOrLoadEntry --- src/matrix/room/timeline/Timeline.js | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 1b980fee..55facd29 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -290,7 +290,7 @@ export class Timeline { const id = entry.contextEventId; let contextEvent = this._getTrackedEvent(id); if (!contextEvent) { - contextEvent = await this._timelineReader.readById(id) ?? await this._getEventFromHomeserver(id); + contextEvent = await this._getEventFromStorage(id) ?? await this._getEventFromHomeserver(id); // this entry was created from storage/hs, so it's not tracked by remoteEntries // we track them here so that we can update reply preview of dependents on redaction this._fetchedEventEntries.set(id, contextEvent); @@ -308,6 +308,11 @@ export class Timeline { return this.getByEventId(id) ?? this._fetchedEventEntries.get(id); } + async _getEventFromStorage(eventId) { + const entry = await this._timelineReader.readById(eventId); + return entry; + } + async _getEventFromHomeserver(eventId) { const response = await this._hsApi.context(this._roomId, eventId, 0).response(); const sender = response.event.sender; @@ -364,18 +369,7 @@ export class Timeline { } } if (eventId) { - const loadedEntry = this.getByEventId(eventId); - if (loadedEntry) { - return loadedEntry; - } else { - const txn = await this._storage.readWriteTxn([ - this._storage.storeNames.timelineEvents, - ]); - const redactionTargetEntry = await txn.timelineEvents.getByEventId(this._roomId, eventId); - if (redactionTargetEntry) { - return new EventEntry(redactionTargetEntry, this._fragmentIdComparer); - } - } + return this.getByEventId(eventId) ?? await this._getEventFromStorage(eventId); } return null; } From c3bef6d4d2b04b7cd29fc37fede80c7cb4d14222 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 13 Dec 2021 14:38:10 +0530 Subject: [PATCH 25/82] Rename dependents --> contextForEntries --- src/matrix/room/timeline/Timeline.js | 12 ++++++------ src/matrix/room/timeline/entries/EventEntry.js | 16 ++++++++-------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 55facd29..c0739f53 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -214,8 +214,8 @@ export class Timeline { // used in replaceEntries static _entryUpdater(existingEntry, entry) { - // ensure dependents point to the new entry instead of existingEntry - existingEntry.dependents?.forEach(event => event.setContextEntry(entry)); + // ensure other entries for which this existingEntry is a context point to the new entry instead of existingEntry + existingEntry.contextForEntries?.forEach(event => event.setContextEntry(entry)); entry.updateFrom(existingEntry); return entry; } @@ -229,7 +229,7 @@ export class Timeline { try { this._remoteEntries.getAndUpdate(entry, Timeline._entryUpdater); // Since this entry changed, all dependent entries should be updated - entry.dependents?.forEach(e => this._updateEntry(e)); + entry.contextForEntries?.forEach(e => this._updateEntry(e)); } catch (err) { if (err.name === "CompareError") { // see FragmentIdComparer, if the replacing entry is on a fragment @@ -262,7 +262,7 @@ export class Timeline { const relatedEntry = this._fetchedEventEntries.get(entry.relatedEventId); // todo: can this be called .addRelation instead? if (relatedEntry?.addLocalRelation(entry)) { - relatedEntry.dependents.forEach(e => this._updateEntry(e)); + relatedEntry.contextForEntries.forEach(e => this._updateEntry(e)); } } } @@ -272,7 +272,7 @@ export class Timeline { for (const entry of entries) { const fetchedEntry = this._fetchedEventEntries.get(entry.id); if (fetchedEntry) { - fetchedEntry.dependents.forEach(e => e.setContextEntry(entry)); + fetchedEntry.contextForEntries.forEach(e => e.setContextEntry(entry)); entry.updateFrom(fetchedEntry); } } @@ -296,7 +296,7 @@ export class Timeline { this._fetchedEventEntries.set(id, contextEvent); } if (contextEvent) { - contextEvent.addDependent(entry); + contextEvent.setAsContextOf(entry); entry.setContextEntry(contextEvent); // emit this change this._updateEntry(entry); diff --git a/src/matrix/room/timeline/entries/EventEntry.js b/src/matrix/room/timeline/entries/EventEntry.js index c656dfb5..d1fd0f4f 100644 --- a/src/matrix/room/timeline/entries/EventEntry.js +++ b/src/matrix/room/timeline/entries/EventEntry.js @@ -25,7 +25,7 @@ export class EventEntry extends BaseEventEntry { this._decryptionError = null; this._decryptionResult = null; this._contextEntry = null; - this._dependents = null; + this._contextForEntries = null; } clone() { @@ -41,22 +41,22 @@ export class EventEntry extends BaseEventEntry { if (other._decryptionError && !this._decryptionError) { this._decryptionError = other._decryptionError; } - this._dependents = other._dependents; + this._contextForEntries = other._contextForEntries; } setContextEntry(entry) { this._contextEntry = entry; } - addDependent(entry) { - if (!this._dependents) { - this._dependents = []; + setAsContextOf(entry) { + if (!this._contextForEntries) { + this._contextForEntries = []; } - this._dependents.push(entry); + this._contextForEntries.push(entry); } - get dependents() { - return this._dependents; + get contextForEntries() { + return this._contextForEntries; } get event() { From 05d2defa2d7dffbe6bd7909bab31b07931224317 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 13 Dec 2021 15:04:55 +0530 Subject: [PATCH 26/82] Rename fetchedEntries --> contextEntriesNotInTimeline --- src/matrix/room/timeline/Timeline.js | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index c0739f53..2f970975 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -46,8 +46,8 @@ export class Timeline { }); this._readerRequest = null; this._allEntries = null; - // Stores event entries that we fetch for reply previews - this._fetchedEventEntries = new Map(); + /** Stores event entries that we had to fetch from hs/storage for reply previews (because they were not in timeline) */ + this._contextEntriesNotInTimeline = new Map(); this._decryptEntries = null; this._hsApi = hsApi; this.initializePowerLevels(powerLevelsObservable); @@ -256,10 +256,13 @@ export class Timeline { this._remoteEntries.setManySorted(newEntries); } + /** + * Update entries in contextEntriesNotInTimeline based on newly received events. + * eg: a newly received redacted event may mark an existing event in contextEntriesNotInTimeline as being redacted + */ _updateFetchedEntries(entries) { - // Update fetchedEntries based on incoming event, eg: a fetched event being redacted for (const entry of entries) { - const relatedEntry = this._fetchedEventEntries.get(entry.relatedEventId); + const relatedEntry = this._contextEntriesNotInTimeline.get(entry.relatedEventId); // todo: can this be called .addRelation instead? if (relatedEntry?.addLocalRelation(entry)) { relatedEntry.contextForEntries.forEach(e => this._updateEntry(e)); @@ -267,10 +270,13 @@ export class Timeline { } } + /** + * If an event we had to fetch from hs/storage is now in the timeline (for eg, due to gap fill), + * remove the event from _contextEntriesNotInTimeline since it is now in remoteEntries + */ _moveFetchedEntryToRemoteEntries(entries) { - // if some entry in entries is also in fetchedEntries, we need to remove it from fetchedEntries for (const entry of entries) { - const fetchedEntry = this._fetchedEventEntries.get(entry.id); + const fetchedEntry = this._contextEntriesNotInTimeline.get(entry.id); if (fetchedEntry) { fetchedEntry.contextForEntries.forEach(e => e.setContextEntry(entry)); entry.updateFrom(fetchedEntry); @@ -292,20 +298,19 @@ export class Timeline { if (!contextEvent) { contextEvent = await this._getEventFromStorage(id) ?? await this._getEventFromHomeserver(id); // this entry was created from storage/hs, so it's not tracked by remoteEntries - // we track them here so that we can update reply preview of dependents on redaction - this._fetchedEventEntries.set(id, contextEvent); + // we track them here so that we can update reply previews later + this._contextEntriesNotInTimeline.set(id, contextEvent); } if (contextEvent) { contextEvent.setAsContextOf(entry); entry.setContextEntry(contextEvent); - // emit this change this._updateEntry(entry); } } } _getTrackedEvent(id) { - return this.getByEventId(id) ?? this._fetchedEventEntries.get(id); + return this.getByEventId(id) ?? this._contextEntriesNotInTimeline.get(id); } async _getEventFromStorage(eventId) { From 640a3fb9fa2dc9097323fb44aeaaf43c1ace94e5 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 13 Dec 2021 15:13:45 +0530 Subject: [PATCH 27/82] Check if contextEvent was found --- src/matrix/room/timeline/Timeline.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 2f970975..9c689c5a 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -297,9 +297,11 @@ export class Timeline { let contextEvent = this._getTrackedEvent(id); if (!contextEvent) { contextEvent = await this._getEventFromStorage(id) ?? await this._getEventFromHomeserver(id); - // this entry was created from storage/hs, so it's not tracked by remoteEntries - // we track them here so that we can update reply previews later - this._contextEntriesNotInTimeline.set(id, contextEvent); + if (contextEvent) { + // this entry was created from storage/hs, so it's not tracked by remoteEntries + // we track them here so that we can update reply previews later + this._contextEntriesNotInTimeline.set(id, contextEvent); + } } if (contextEvent) { contextEvent.setAsContextOf(entry); From 6f8001bd826e2c195abf635087bd663ce98d4b05 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 13 Dec 2021 21:27:17 +0530 Subject: [PATCH 28/82] Add tests --- src/matrix/room/timeline/Timeline.js | 149 +++++++++++++++++++++++++-- 1 file changed, 142 insertions(+), 7 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 9c689c5a..dcbeae5f 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -252,20 +252,20 @@ export class Timeline { this._addLocalRelationsToNewRemoteEntries(newEntries); this._updateFetchedEntries(newEntries); this._moveFetchedEntryToRemoteEntries(newEntries); - this._loadRelatedEvents(newEntries); this._remoteEntries.setManySorted(newEntries); + this._loadRelatedEvents(newEntries); } /** - * Update entries in contextEntriesNotInTimeline based on newly received events. + * Update entries based on newly received events. * eg: a newly received redacted event may mark an existing event in contextEntriesNotInTimeline as being redacted */ _updateFetchedEntries(entries) { for (const entry of entries) { - const relatedEntry = this._contextEntriesNotInTimeline.get(entry.relatedEventId); + const relatedEntry = this._getTrackedEntry(entry.relatedEventId); // todo: can this be called .addRelation instead? if (relatedEntry?.addLocalRelation(entry)) { - relatedEntry.contextForEntries.forEach(e => this._updateEntry(e)); + relatedEntry.contextForEntries?.forEach(e => this._updateEntry(e)); } } } @@ -294,7 +294,7 @@ export class Timeline { const entriesNeedingContext = entries.filter(e => !!e.contextEventId); for (const entry of entriesNeedingContext) { const id = entry.contextEventId; - let contextEvent = this._getTrackedEvent(id); + let contextEvent = this._getTrackedEntry(id); if (!contextEvent) { contextEvent = await this._getEventFromStorage(id) ?? await this._getEventFromHomeserver(id); if (contextEvent) { @@ -311,7 +311,7 @@ export class Timeline { } } - _getTrackedEvent(id) { + _getTrackedEntry(id) { return this.getByEventId(id) ?? this._contextEntriesNotInTimeline.get(id); } @@ -433,7 +433,7 @@ import {poll} from "../../../mocks/poll.js"; import {Clock as MockClock} from "../../../mocks/Clock.js"; import {createMockStorage} from "../../../mocks/Storage"; import {ListObserver} from "../../../mocks/ListObserver.js"; -import {createEvent, withTextBody, withContent, withSender} from "../../../mocks/event.js"; +import {createEvent, withTextBody, withContent, withSender, withRedacts} from "../../../mocks/event.js"; import {NullLogItem} from "../../../logging/NullLogger"; import {EventEntry} from "./entries/EventEntry.js"; import {User} from "../../User.js"; @@ -689,6 +689,141 @@ export function tests() { assert.equal(type, "update"); assert.equal(value.eventType, "m.room.message"); assert.equal(value.content.body, "hi bob!"); + }, + + "context entry is fetched from remoteEntries": async assert => { + const timeline = new Timeline({roomId, storage: await createMockStorage(), closeCallback: () => {}, + fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock()}); + const entryA = new EventEntry({ event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)) }); + let event = withContent({ + body: "bar", + msgtype: "m.text", + "m.relates_to": { + "m.in_reply_to": { + "event_id": "event_id_1" + } + } + }, createEvent("m.room.message", "event_id_2", bob)); + const entryB = new EventEntry({ event }); + await timeline.load(new User(alice), "join", new NullLogItem()); + timeline._remoteEntries.setManyUnsorted([entryA, entryB]); + await timeline._loadRelatedEvents([entryA, entryB]); + assert.deepEqual(entryB.contextEntry, entryA); + }, + + "context entry is fetched from storage": async assert => { + const timeline = new Timeline({roomId, storage: await createMockStorage(), closeCallback: () => {}, + fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock()}); + const entryA = new EventEntry({ event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)) }); + let event = withContent({ + body: "bar", + msgtype: "m.text", + "m.relates_to": { + "m.in_reply_to": { + "event_id": "event_id_1" + } + } + }, createEvent("m.room.message", "event_id_2", bob)); + const entryB = new EventEntry({ event }); + timeline._getEventFromStorage = () => entryA + await timeline.load(new User(alice), "join", new NullLogItem()); + await timeline._loadRelatedEvents([entryB]); + assert.deepEqual(entryB.contextEntry, entryA); + }, + + "context entry is fetched from hs": async assert => { + const timeline = new Timeline({roomId, storage: await createMockStorage(), closeCallback: () => {}, + fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock()}); + const entryA = new EventEntry({ event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)) }); + let event = withContent({ + body: "bar", + msgtype: "m.text", + "m.relates_to": { + "m.in_reply_to": { + "event_id": "event_id_1" + } + } + }, createEvent("m.room.message", "event_id_2", bob)); + const entryB = new EventEntry({ event }); + timeline._getEventFromHomeserver = () => entryA + await timeline.load(new User(alice), "join", new NullLogItem()); + await timeline._loadRelatedEvents([entryB]); + assert.deepEqual(entryB.contextEntry, entryA); + }, + + "context entry has a list of entries to which it forms the context": async assert => { + const timeline = new Timeline({roomId, storage: await createMockStorage(), closeCallback: () => {}, + fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock()}); + const entryA = new EventEntry({ event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)) }); + const content = { + body: "bar", + msgtype: "m.text", + "m.relates_to": { + "m.in_reply_to": { + "event_id": "event_id_1" + } + } + }; + const entryB = new EventEntry({ event: withContent(content, createEvent("m.room.message", "event_id_2", bob)) }); + const entryC = new EventEntry({ event: withContent(content, createEvent("m.room.message", "event_id_3", bob)) }); + await timeline.load(new User(alice), "join", new NullLogItem()); + timeline._remoteEntries.setManyUnsorted([entryA, entryB, entryC]); + await timeline._loadRelatedEvents([entryA, entryB, entryC]); + assert.deepEqual(entryA.contextForEntries, [entryB, entryC]); + }, + + "context entry in contextEntryNotInTimeline gets updated based on incoming redaction": async assert => { + const timeline = new Timeline({roomId, storage: await createMockStorage(), closeCallback: () => {}, + fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock()}); + const entryA = new EventEntry({ event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)) }); + let event = withContent({ + body: "bar", + msgtype: "m.text", + "m.relates_to": { + "m.in_reply_to": { + "event_id": "event_id_1" + } + } + }, createEvent("m.room.message", "event_id_2", bob)); + const entryB = new EventEntry({ event }); + timeline._getEventFromStorage = () => entryA + await timeline.load(new User(alice), "join", new NullLogItem()); + await timeline._loadRelatedEvents([entryB]); + const redactingEntry = new EventEntry({ event: withRedacts(entryA.id, "foo", createEvent("m.room.redaction", "event_id_3", alice)) }); + await timeline.addEntries([redactingEntry]); + const contextEntry = timeline._contextEntriesNotInTimeline.get(entryA.id); + assert.strictEqual(contextEntry.isRedacted, true); + }, + + "redaction of context entry triggers updates in other entries": async assert => { + const timeline = new Timeline({roomId, storage: await createMockStorage(), closeCallback: () => {}, + fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock()}); + const entryA = new EventEntry({ event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)) }); + const content = { + body: "bar", + msgtype: "m.text", + "m.relates_to": { + "m.in_reply_to": { + "event_id": "event_id_1" + } + } + }; + const entryB = new EventEntry({ event: withContent(content, createEvent("m.room.message", "event_id_2", bob)) }); + const entryC = new EventEntry({ event: withContent(content, createEvent("m.room.message", "event_id_3", bob)) }); + await timeline.load(new User(alice), "join", new NullLogItem()); + // timeline._getEventFromStorage = () => entryA; + await timeline.addEntries([entryA, entryB, entryC]); + const bin = [entryB, entryC]; + timeline.entries.subscribe({ + onUpdate: (index) => { + const i = bin.findIndex(e => e.id === index); + bin.splice(i, 1); + }, + onAdd: () => { } + }); + const redactingEntry = new EventEntry({ event: withRedacts(entryA.id, "foo", createEvent("m.room.redaction", "event_id_3", alice)) }); + await timeline.addEntries([redactingEntry]); + assert.strictEqual(bin.length, 0); } }; } From 2d5bb82077fdfb97c7e22b481ab8b987b3b55a73 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 14 Dec 2021 14:36:57 +0530 Subject: [PATCH 29/82] Fix bug --- src/matrix/room/timeline/persistence/TimelineReader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/room/timeline/persistence/TimelineReader.js b/src/matrix/room/timeline/persistence/TimelineReader.js index dd7474e7..23ec4ed8 100644 --- a/src/matrix/room/timeline/persistence/TimelineReader.js +++ b/src/matrix/room/timeline/persistence/TimelineReader.js @@ -134,7 +134,7 @@ export class TimelineReader { async readById(id, log) { let stores = [this._storage.storeNames.timelineEvents]; - if (this.isEncrypted) { + if (this._decryptEntries) { stores.push(this._storage.storeNames.inboundGroupSessions); } const txn = await this._storage.readTxn(stores); // todo: can we just use this.readTxnStores here? probably From 7ef79c92f51091214b8e480b1e065e55e85db78f Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 15 Dec 2021 11:42:37 +0530 Subject: [PATCH 30/82] Remove entry from map --- src/matrix/room/timeline/Timeline.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index dcbeae5f..81c176f9 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -280,6 +280,7 @@ export class Timeline { if (fetchedEntry) { fetchedEntry.contextForEntries.forEach(e => e.setContextEntry(entry)); entry.updateFrom(fetchedEntry); + this._contextEntriesNotInTimeline.delete(entry.id); } } } From 3fe824dbd161e8f987b469ee518734a819e43f2e Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 15 Dec 2021 17:47:14 +0530 Subject: [PATCH 31/82] Propagate updates --- src/matrix/room/timeline/Timeline.js | 24 +++++++++++++++---- .../room/timeline/entries/EventEntry.js | 7 +++++- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 81c176f9..6d05db64 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -259,14 +259,30 @@ export class Timeline { /** * Update entries based on newly received events. * eg: a newly received redacted event may mark an existing event in contextEntriesNotInTimeline as being redacted + * This is only for the events that are not in the timeline but had to fetched from elsewhere to render reply previews. */ _updateFetchedEntries(entries) { for (const entry of entries) { - const relatedEntry = this._getTrackedEntry(entry.relatedEventId); - // todo: can this be called .addRelation instead? - if (relatedEntry?.addLocalRelation(entry)) { - relatedEntry.contextForEntries?.forEach(e => this._updateEntry(e)); + const relatedEntry = this._contextEntriesNotInTimeline.get(entry.relatedEventId); + if (!relatedEntry) { + continue; } + // update dependents with this new entry indicating that this is an update to contextEntry + const newEntry = this._createEntryFromRelatedEntries(entry, relatedEntry); + if (newEntry) { + Timeline._entryUpdater(relatedEntry, newEntry); + relatedEntry.contextForEntries?.forEach(e => { + this._remoteEntries.findAndUpdate(te => te.id === e.id, () => { return { reply: newEntry }; }); + }); + } + } + } + + _createEntryFromRelatedEntries(entry, relatedEntry) { + if (entry.isRedaction) { + const newEntry = relatedEntry.clone(); + newEntry.setAsRedacted(); + return newEntry; } } diff --git a/src/matrix/room/timeline/entries/EventEntry.js b/src/matrix/room/timeline/entries/EventEntry.js index d1fd0f4f..93700842 100644 --- a/src/matrix/room/timeline/entries/EventEntry.js +++ b/src/matrix/room/timeline/entries/EventEntry.js @@ -26,6 +26,7 @@ export class EventEntry extends BaseEventEntry { this._decryptionResult = null; this._contextEntry = null; this._contextForEntries = null; + this._markedAsRedacted = false; } clone() { @@ -55,6 +56,10 @@ export class EventEntry extends BaseEventEntry { this._contextForEntries.push(entry); } + setAsRedacted() { + this._markedAsRedacted = true; + } + get contextForEntries() { return this._contextForEntries; } @@ -153,7 +158,7 @@ export class EventEntry extends BaseEventEntry { } get isRedacted() { - return super.isRedacted || isRedacted(this._eventEntry.event); + return this._markedAsRedacted || super.isRedacted || isRedacted(this._eventEntry.event); } get redactionReason() { From a060d54468fd23c23fd68553e22374038e64fc5b Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 15 Dec 2021 19:23:51 +0530 Subject: [PATCH 32/82] Make tests pass --- src/matrix/room/timeline/Timeline.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 6d05db64..c5f6d505 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -271,6 +271,8 @@ export class Timeline { const newEntry = this._createEntryFromRelatedEntries(entry, relatedEntry); if (newEntry) { Timeline._entryUpdater(relatedEntry, newEntry); + this._contextEntriesNotInTimeline.delete(relatedEntry.id); + this._contextEntriesNotInTimeline.set(newEntry.id, newEntry); relatedEntry.contextForEntries?.forEach(e => { this._remoteEntries.findAndUpdate(te => te.id === e.id, () => { return { reply: newEntry }; }); }); @@ -828,8 +830,8 @@ export function tests() { const entryB = new EventEntry({ event: withContent(content, createEvent("m.room.message", "event_id_2", bob)) }); const entryC = new EventEntry({ event: withContent(content, createEvent("m.room.message", "event_id_3", bob)) }); await timeline.load(new User(alice), "join", new NullLogItem()); - // timeline._getEventFromStorage = () => entryA; - await timeline.addEntries([entryA, entryB, entryC]); + timeline._getEventFromStorage = () => entryA; + await timeline.addEntries([entryB, entryC]); const bin = [entryB, entryC]; timeline.entries.subscribe({ onUpdate: (index) => { From 8ec75ce4bb2be2964d604dd97df11351b05eb949 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 15 Dec 2021 20:42:38 +0530 Subject: [PATCH 33/82] Rename methods --- src/matrix/room/timeline/Timeline.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index c5f6d505..08f3c68b 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -223,7 +223,7 @@ export class Timeline { /** @package */ replaceEntries(entries) { this._addLocalRelationsToNewRemoteEntries(entries); - this._updateFetchedEntries(entries); + this._updateEntriesNotInTimeline(entries); this._loadRelatedEvents(entries); for (const entry of entries) { try { @@ -250,18 +250,18 @@ export class Timeline { /** @package */ addEntries(newEntries) { this._addLocalRelationsToNewRemoteEntries(newEntries); - this._updateFetchedEntries(newEntries); - this._moveFetchedEntryToRemoteEntries(newEntries); + this._updateEntriesNotInTimeline(newEntries); + this._moveEntryToRemoteEntries(newEntries); this._remoteEntries.setManySorted(newEntries); this._loadRelatedEvents(newEntries); } /** * Update entries based on newly received events. - * eg: a newly received redacted event may mark an existing event in contextEntriesNotInTimeline as being redacted + * eg: a newly received redaction event may mark an existing event in contextEntriesNotInTimeline as being redacted * This is only for the events that are not in the timeline but had to fetched from elsewhere to render reply previews. */ - _updateFetchedEntries(entries) { + _updateEntriesNotInTimeline(entries) { for (const entry of entries) { const relatedEntry = this._contextEntriesNotInTimeline.get(entry.relatedEventId); if (!relatedEntry) { @@ -292,7 +292,7 @@ export class Timeline { * If an event we had to fetch from hs/storage is now in the timeline (for eg, due to gap fill), * remove the event from _contextEntriesNotInTimeline since it is now in remoteEntries */ - _moveFetchedEntryToRemoteEntries(entries) { + _moveEntryToRemoteEntries(entries) { for (const entry of entries) { const fetchedEntry = this._contextEntriesNotInTimeline.get(entry.id); if (fetchedEntry) { From 41cf6460d0fff2ee560dcedd3daf0efc84149888 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 21 Dec 2021 11:32:07 +0530 Subject: [PATCH 34/82] Remove dead code --- src/domain/session/room/timeline/ReactionsViewModel.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/domain/session/room/timeline/ReactionsViewModel.js b/src/domain/session/room/timeline/ReactionsViewModel.js index bb5511e6..de5ea2c3 100644 --- a/src/domain/session/room/timeline/ReactionsViewModel.js +++ b/src/domain/session/room/timeline/ReactionsViewModel.js @@ -260,9 +260,7 @@ export function tests() { fragmentIdComparer, clock: new MockClock(), pendingEvents: queue.pendingEvents, - powerLevelsObservable, - fetchEventFromHomeserver: () => {}, - fetchEventFromStorage: () => {} + powerLevelsObservable }); // 3. load the timeline, which will load the message with the reaction await timeline.load(new User(alice), "join", new NullLogItem()); From d2c7eec8e04268ea96d2456da952a02dc1b87659 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 21 Dec 2021 12:10:16 +0530 Subject: [PATCH 35/82] No need to delete before update on map --- src/matrix/room/timeline/Timeline.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 08f3c68b..62a2a7b9 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -271,7 +271,6 @@ export class Timeline { const newEntry = this._createEntryFromRelatedEntries(entry, relatedEntry); if (newEntry) { Timeline._entryUpdater(relatedEntry, newEntry); - this._contextEntriesNotInTimeline.delete(relatedEntry.id); this._contextEntriesNotInTimeline.set(newEntry.id, newEntry); relatedEntry.contextForEntries?.forEach(e => { this._remoteEntries.findAndUpdate(te => te.id === e.id, () => { return { reply: newEntry }; }); From 44187005895724028313cb950958068a1abc3bef Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 21 Dec 2021 12:39:52 +0530 Subject: [PATCH 36/82] Add test for move code --- src/matrix/room/timeline/Timeline.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 62a2a7b9..8956e809 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -842,6 +842,33 @@ export function tests() { const redactingEntry = new EventEntry({ event: withRedacts(entryA.id, "foo", createEvent("m.room.redaction", "event_id_3", alice)) }); await timeline.addEntries([redactingEntry]); assert.strictEqual(bin.length, 0); + }, + + "context entries fetched from storage/hs are moved to remoteEntries": async assert => { + const timeline = new Timeline({roomId, storage: await createMockStorage(), closeCallback: () => {}, + fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock()}); + const entryA = new EventEntry({ event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)) }); + let event = withContent({ + body: "bar", + msgtype: "m.text", + "m.relates_to": { + "m.in_reply_to": { + "event_id": "event_id_1" + } + } + }, createEvent("m.room.message", "event_id_2", bob)); + const entryB = new EventEntry({ event }); + timeline._getEventFromHomeserver = () => entryA + await timeline.load(new User(alice), "join", new NullLogItem()); + await timeline.addEntries([entryB]); + await timeline._loadRelatedEvents([entryB]); + assert.strictEqual(timeline._contextEntriesNotInTimeline.has(entryA.id), true); + await timeline.addEntries([entryA]); + assert.strictEqual(timeline._contextEntriesNotInTimeline.has(entryA.id), false); + const movedEntry = timeline.remoteEntries[0]; + assert.deepEqual(movedEntry, entryA); + assert.deepEqual(movedEntry.contextForEntries.pop(), entryB); + assert.deepEqual(entryB.contextEntry, movedEntry); } }; } From 9f1764c32591dfca410ffcb9b787626ca26d69f8 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 21 Dec 2021 13:25:30 +0530 Subject: [PATCH 37/82] Update comment --- src/matrix/room/timeline/Timeline.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 8956e809..f45f99a3 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -267,11 +267,11 @@ export class Timeline { if (!relatedEntry) { continue; } - // update dependents with this new entry indicating that this is an update to contextEntry const newEntry = this._createEntryFromRelatedEntries(entry, relatedEntry); if (newEntry) { Timeline._entryUpdater(relatedEntry, newEntry); this._contextEntriesNotInTimeline.set(newEntry.id, newEntry); + // update other entries for which this entry is a context entry relatedEntry.contextForEntries?.forEach(e => { this._remoteEntries.findAndUpdate(te => te.id === e.id, () => { return { reply: newEntry }; }); }); From 78f97c6532e92d0d4b69391531bd1bf8a050ba41 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 21 Dec 2021 13:41:14 +0530 Subject: [PATCH 38/82] Remove await from tests --- src/matrix/room/timeline/Timeline.js | 29 ++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index f45f99a3..196dac03 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -485,10 +485,10 @@ export function tests() { // 2. test replaceEntries and addEntries don't fail const event1 = withTextBody("hi!", withSender(bob, createEvent("m.room.message", "!abc"))); const entry1 = new EventEntry({event: event1, fragmentId: 1, eventIndex: 1}, fragmentIdComparer); - await timeline.replaceEntries([entry1]); + timeline.replaceEntries([entry1]); const event2 = withTextBody("hi bob!", withSender(alice, createEvent("m.room.message", "!def"))); const entry2 = new EventEntry({event: event2, fragmentId: 1, eventIndex: 2}, fragmentIdComparer); - await timeline.addEntries([entry2]); + timeline.addEntries([entry2]); // 3. add local relation (redaction) pendingEvents.append(new PendingEvent({data: { roomId, @@ -512,7 +512,7 @@ export function tests() { await timeline.load(new User(bob), "join", new NullLogItem()); timeline.entries.subscribe(new ListObserver()); const event = withTextBody("hi bob!", withSender(alice, createEvent("m.room.message", "!abc"))); - await timeline.addEntries([new EventEntry({event, fragmentId: 1, eventIndex: 2}, fragmentIdComparer)]); + timeline.addEntries([new EventEntry({event, fragmentId: 1, eventIndex: 2}, fragmentIdComparer)]); let entry = getIndexFromIterable(timeline.entries, 0); // 2. add local reaction pendingEvents.append(new PendingEvent({data: { @@ -583,7 +583,7 @@ export function tests() { await timeline.load(new User(bob), "join", new NullLogItem()); timeline.entries.subscribe(new ListObserver()); // 3. add message to timeline - await timeline.addEntries([messageEntry]); + timeline.addEntries([messageEntry]); const entry = getIndexFromIterable(timeline.entries, 0); assert.equal(entry, messageEntry); assert.equal(entry.annotations["👋"].count, 1); @@ -605,7 +605,7 @@ export function tests() { event: withContent(createAnnotation(messageEntry.id, "👋"), createEvent("m.reaction", "!def", bob)), fragmentId: 1, eventIndex: 3, roomId }, fragmentIdComparer); - await timeline.addEntries([messageEntry, reactionEntry]); + timeline.addEntries([messageEntry, reactionEntry]); // 3. redact reaction pendingEvents.append(new PendingEvent({data: { roomId, @@ -638,7 +638,7 @@ export function tests() { }})); await poll(() => timeline.entries.length === 1); // 3. add remote reaction target - await timeline.addEntries([messageEntry]); + timeline.addEntries([messageEntry]); await poll(() => timeline.entries.length === 2); const entry = getIndexFromIterable(timeline.entries, 0); assert.equal(entry, messageEntry); @@ -672,7 +672,7 @@ export function tests() { }})); await poll(() => timeline.entries.length === 1); // 4. add reaction target - await timeline.addEntries([new EventEntry({ + timeline.addEntries([new EventEntry({ event: withTextBody("hi bob!", createEvent("m.room.message", messageId, alice)), fragmentId: 1, eventIndex: 2}, fragmentIdComparer) ]); @@ -695,14 +695,14 @@ export function tests() { const timeline = new Timeline({roomId, storage: await createMockStorage(), closeCallback: () => {}, fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock()}); await timeline.load(new User(alice), "join", new NullLogItem()); - await timeline.addEntries([decryptedEntry]); + timeline.addEntries([decryptedEntry]); const observer = new ListObserver(); timeline.entries.subscribe(observer); // 3. replace the entry with one that is not decrypted // (as would happen when receiving a reaction, // as it does not rerun the decryption) // and check that the decrypted content is preserved - await timeline.replaceEntries([encryptedEntry]); + timeline.replaceEntries([encryptedEntry]); const {value, type} = await observer.next(); assert.equal(type, "update"); assert.equal(value.eventType, "m.room.message"); @@ -808,7 +808,7 @@ export function tests() { await timeline.load(new User(alice), "join", new NullLogItem()); await timeline._loadRelatedEvents([entryB]); const redactingEntry = new EventEntry({ event: withRedacts(entryA.id, "foo", createEvent("m.room.redaction", "event_id_3", alice)) }); - await timeline.addEntries([redactingEntry]); + timeline.addEntries([redactingEntry]); const contextEntry = timeline._contextEntriesNotInTimeline.get(entryA.id); assert.strictEqual(contextEntry.isRedacted, true); }, @@ -830,7 +830,8 @@ export function tests() { const entryC = new EventEntry({ event: withContent(content, createEvent("m.room.message", "event_id_3", bob)) }); await timeline.load(new User(alice), "join", new NullLogItem()); timeline._getEventFromStorage = () => entryA; - await timeline.addEntries([entryB, entryC]); + timeline.addEntries([entryB, entryC]); + await poll(() => timeline._remoteEntries.array.length === 2); const bin = [entryB, entryC]; timeline.entries.subscribe({ onUpdate: (index) => { @@ -840,7 +841,7 @@ export function tests() { onAdd: () => { } }); const redactingEntry = new EventEntry({ event: withRedacts(entryA.id, "foo", createEvent("m.room.redaction", "event_id_3", alice)) }); - await timeline.addEntries([redactingEntry]); + timeline.addEntries([redactingEntry]); assert.strictEqual(bin.length, 0); }, @@ -860,10 +861,10 @@ export function tests() { const entryB = new EventEntry({ event }); timeline._getEventFromHomeserver = () => entryA await timeline.load(new User(alice), "join", new NullLogItem()); - await timeline.addEntries([entryB]); + timeline.addEntries([entryB]); await timeline._loadRelatedEvents([entryB]); assert.strictEqual(timeline._contextEntriesNotInTimeline.has(entryA.id), true); - await timeline.addEntries([entryA]); + timeline.addEntries([entryA]); assert.strictEqual(timeline._contextEntriesNotInTimeline.has(entryA.id), false); const movedEntry = timeline.remoteEntries[0]; assert.deepEqual(movedEntry, entryA); From 595deb3a3de26f5018353a50b88d477914c8135e Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 21 Dec 2021 13:46:21 +0530 Subject: [PATCH 39/82] Also copy over contextEntry from otherEntry --- src/matrix/room/timeline/entries/EventEntry.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/matrix/room/timeline/entries/EventEntry.js b/src/matrix/room/timeline/entries/EventEntry.js index 93700842..4d19cf56 100644 --- a/src/matrix/room/timeline/entries/EventEntry.js +++ b/src/matrix/room/timeline/entries/EventEntry.js @@ -42,7 +42,8 @@ export class EventEntry extends BaseEventEntry { if (other._decryptionError && !this._decryptionError) { this._decryptionError = other._decryptionError; } - this._contextForEntries = other._contextForEntries; + this._contextForEntries = other.contextForEntries; + this._contextEntry = other.contextEntry; } setContextEntry(entry) { From 90c9018aa4efe598766a09a10475c05f7de58b3a Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 21 Dec 2021 13:47:29 +0530 Subject: [PATCH 40/82] Update comment --- src/matrix/room/timeline/entries/EventEntry.js | 2 +- src/matrix/room/timeline/entries/NonPersistedEventEntry.js | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/matrix/room/timeline/entries/EventEntry.js b/src/matrix/room/timeline/entries/EventEntry.js index 4d19cf56..bdabfea4 100644 --- a/src/matrix/room/timeline/entries/EventEntry.js +++ b/src/matrix/room/timeline/entries/EventEntry.js @@ -146,7 +146,7 @@ export class EventEntry extends BaseEventEntry { return getRelatedEventId(this.event); } - // similar to relatedEventID but excludes relations like redaction + // similar to relatedEventID but only for replies get contextEventId() { if (this.isReply) { return this.relatedEventId; diff --git a/src/matrix/room/timeline/entries/NonPersistedEventEntry.js b/src/matrix/room/timeline/entries/NonPersistedEventEntry.js index f3bae9d2..8ec9f72e 100644 --- a/src/matrix/room/timeline/entries/NonPersistedEventEntry.js +++ b/src/matrix/room/timeline/entries/NonPersistedEventEntry.js @@ -17,6 +17,8 @@ limitations under the License. import {EventEntry} from "./EventEntry.js"; // EventEntry but without the two properties that are populated via SyncWriter +// Useful if you want to create an EventEntry that is ephemeral + export class NonPersistedEventEntry extends EventEntry { get fragmentId() { throw new Error("Cannot access fragmentId for non-persisted EventEntry"); From a2ab36480f9c76f1a716a97485dbc1427b6f85db Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 6 Jan 2022 15:02:44 +0530 Subject: [PATCH 41/82] Add jsdoc comment --- src/matrix/room/timeline/Timeline.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 196dac03..e4014340 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -329,8 +329,13 @@ export class Timeline { } } - _getTrackedEntry(id) { - return this.getByEventId(id) ?? this._contextEntriesNotInTimeline.get(id); + /** + * Fetches an entry with the given event-id from remoteEntries or contextEntriesNotInTimeline. + * @param {string} eventId event-id of the entry + * @returns entry if found, undefined otherwise + */ + _getTrackedEntry(eventId) { + return this.getByEventId(eventId) ?? this._contextEntriesNotInTimeline.get(eventId); } async _getEventFromStorage(eventId) { From f76217dcced6a62952196831e9b0ad1385268853 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 6 Jan 2022 15:14:13 +0530 Subject: [PATCH 42/82] Change method name --- src/matrix/room/timeline/Timeline.js | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index e4014340..614fa247 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -85,7 +85,7 @@ export class Timeline { const readerRequest = this._disposables.track(this._timelineReader.readFromEnd(20, txn, log)); try { const entries = await readerRequest.complete(); - this._loadRelatedEvents(entries); + this._loadContextEntriesWhereNeeded(entries); this._setupEntries(entries); } finally { this._disposables.disposeTracked(readerRequest); @@ -224,7 +224,7 @@ export class Timeline { replaceEntries(entries) { this._addLocalRelationsToNewRemoteEntries(entries); this._updateEntriesNotInTimeline(entries); - this._loadRelatedEvents(entries); + this._loadContextEntriesWhereNeeded(entries); for (const entry of entries) { try { this._remoteEntries.getAndUpdate(entry, Timeline._entryUpdater); @@ -253,7 +253,7 @@ export class Timeline { this._updateEntriesNotInTimeline(newEntries); this._moveEntryToRemoteEntries(newEntries); this._remoteEntries.setManySorted(newEntries); - this._loadRelatedEvents(newEntries); + this._loadContextEntriesWhereNeeded(newEntries); } /** @@ -308,7 +308,15 @@ export class Timeline { this._findAndUpdateEntryById(txnId, eventId, () => true); } - async _loadRelatedEvents(entries) { + /** + * For each entry in entries, this method associates a context-entry (if needed) to it. + * The context-entry is fetched using the following strategies (in the same order as given): + * - timeline + * - storage + * - homeserver + * @param {EventEntry[]} entries + */ + async _loadContextEntriesWhereNeeded(entries) { const entriesNeedingContext = entries.filter(e => !!e.contextEventId); for (const entry of entriesNeedingContext) { const id = entry.contextEventId; @@ -730,7 +738,7 @@ export function tests() { const entryB = new EventEntry({ event }); await timeline.load(new User(alice), "join", new NullLogItem()); timeline._remoteEntries.setManyUnsorted([entryA, entryB]); - await timeline._loadRelatedEvents([entryA, entryB]); + await timeline._loadContextEntriesWhereNeeded([entryA, entryB]); assert.deepEqual(entryB.contextEntry, entryA); }, @@ -750,7 +758,7 @@ export function tests() { const entryB = new EventEntry({ event }); timeline._getEventFromStorage = () => entryA await timeline.load(new User(alice), "join", new NullLogItem()); - await timeline._loadRelatedEvents([entryB]); + await timeline._loadContextEntriesWhereNeeded([entryB]); assert.deepEqual(entryB.contextEntry, entryA); }, @@ -770,7 +778,7 @@ export function tests() { const entryB = new EventEntry({ event }); timeline._getEventFromHomeserver = () => entryA await timeline.load(new User(alice), "join", new NullLogItem()); - await timeline._loadRelatedEvents([entryB]); + await timeline._loadContextEntriesWhereNeeded([entryB]); assert.deepEqual(entryB.contextEntry, entryA); }, @@ -791,7 +799,7 @@ export function tests() { const entryC = new EventEntry({ event: withContent(content, createEvent("m.room.message", "event_id_3", bob)) }); await timeline.load(new User(alice), "join", new NullLogItem()); timeline._remoteEntries.setManyUnsorted([entryA, entryB, entryC]); - await timeline._loadRelatedEvents([entryA, entryB, entryC]); + await timeline._loadContextEntriesWhereNeeded([entryA, entryB, entryC]); assert.deepEqual(entryA.contextForEntries, [entryB, entryC]); }, @@ -811,7 +819,7 @@ export function tests() { const entryB = new EventEntry({ event }); timeline._getEventFromStorage = () => entryA await timeline.load(new User(alice), "join", new NullLogItem()); - await timeline._loadRelatedEvents([entryB]); + await timeline._loadContextEntriesWhereNeeded([entryB]); const redactingEntry = new EventEntry({ event: withRedacts(entryA.id, "foo", createEvent("m.room.redaction", "event_id_3", alice)) }); timeline.addEntries([redactingEntry]); const contextEntry = timeline._contextEntriesNotInTimeline.get(entryA.id); @@ -867,7 +875,7 @@ export function tests() { timeline._getEventFromHomeserver = () => entryA await timeline.load(new User(alice), "join", new NullLogItem()); timeline.addEntries([entryB]); - await timeline._loadRelatedEvents([entryB]); + await timeline._loadContextEntriesWhereNeeded([entryB]); assert.strictEqual(timeline._contextEntriesNotInTimeline.has(entryA.id), true); timeline.addEntries([entryA]); assert.strictEqual(timeline._contextEntriesNotInTimeline.has(entryA.id), false); From 7adce08eee008fe8befb32a6efaa6c92ea9de551 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 6 Jan 2022 15:33:00 +0530 Subject: [PATCH 43/82] add more jsdoc comments --- src/matrix/room/timeline/Timeline.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 614fa247..9a533c1c 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -279,6 +279,12 @@ export class Timeline { } } + /** + * Creates a new entry based on two related entries + * @param {EventEntry} entry an entry + * @param {EventEntry} relatedEntry `entry` specifies something about this entry (eg: this entry is redacted) + * @returns a new entry or undefined + */ _createEntryFromRelatedEntries(entry, relatedEntry) { if (entry.isRedaction) { const newEntry = relatedEntry.clone(); From cfbb6d42509b341cbfa5b274cc3da4e421bbdcc6 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 6 Jan 2022 15:37:58 +0530 Subject: [PATCH 44/82] Add explaining comment --- src/matrix/room/timeline/Timeline.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 9a533c1c..528acd6c 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -48,6 +48,7 @@ export class Timeline { this._allEntries = null; /** Stores event entries that we had to fetch from hs/storage for reply previews (because they were not in timeline) */ this._contextEntriesNotInTimeline = new Map(); + /** Only used to decrypt non-persisted context entries fetched from the homeserver */ this._decryptEntries = null; this._hsApi = hsApi; this.initializePowerLevels(powerLevelsObservable); From c6484f1eac5bd43d2a7d9aad16e96802d73ec0a6 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 7 Jan 2022 17:11:42 +0530 Subject: [PATCH 45/82] Replace entry in contextEntryNotInTimeline --- src/matrix/room/timeline/Timeline.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 528acd6c..f74d6acf 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -224,11 +224,14 @@ export class Timeline { /** @package */ replaceEntries(entries) { this._addLocalRelationsToNewRemoteEntries(entries); - this._updateEntriesNotInTimeline(entries); - this._loadContextEntriesWhereNeeded(entries); for (const entry of entries) { try { this._remoteEntries.getAndUpdate(entry, Timeline._entryUpdater); + const oldEntry = this._contextEntriesNotInTimeline.get(entry.id) + if (oldEntry) { + Timeline._entryUpdater(oldEntry, entry); + } + this._contextEntriesNotInTimeline.set(entry.id, entry); // Since this entry changed, all dependent entries should be updated entry.contextForEntries?.forEach(e => this._updateEntry(e)); } catch (err) { From 0a09a50ab91251d8107c84bc76938097be7ad964 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 7 Jan 2022 17:29:17 +0530 Subject: [PATCH 46/82] Move line into if --- src/matrix/room/timeline/Timeline.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index f74d6acf..2e793be1 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -230,8 +230,8 @@ export class Timeline { const oldEntry = this._contextEntriesNotInTimeline.get(entry.id) if (oldEntry) { Timeline._entryUpdater(oldEntry, entry); + this._contextEntriesNotInTimeline.set(entry.id, entry); } - this._contextEntriesNotInTimeline.set(entry.id, entry); // Since this entry changed, all dependent entries should be updated entry.contextForEntries?.forEach(e => this._updateEntry(e)); } catch (err) { From 8cc04e4c255c6fb8513f583e86e407fc8b46ca59 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 7 Jan 2022 17:50:36 +0530 Subject: [PATCH 47/82] Keep calls internal to class --- src/matrix/room/timeline/Timeline.js | 1 - src/matrix/room/timeline/entries/EventEntry.js | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 2e793be1..bb6c9ac5 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -340,7 +340,6 @@ export class Timeline { } } if (contextEvent) { - contextEvent.setAsContextOf(entry); entry.setContextEntry(contextEvent); this._updateEntry(entry); } diff --git a/src/matrix/room/timeline/entries/EventEntry.js b/src/matrix/room/timeline/entries/EventEntry.js index bdabfea4..513d427a 100644 --- a/src/matrix/room/timeline/entries/EventEntry.js +++ b/src/matrix/room/timeline/entries/EventEntry.js @@ -48,6 +48,7 @@ export class EventEntry extends BaseEventEntry { setContextEntry(entry) { this._contextEntry = entry; + entry.setAsContextOf(this); } setAsContextOf(entry) { From 9d161a0bcf649335c7af22ffdd66ca5c6ca76ab3 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 7 Jan 2022 19:38:57 +0530 Subject: [PATCH 48/82] Refactor + put redaction in NonPersistedEventEntry --- src/matrix/room/timeline/Timeline.js | 16 ++++--- src/matrix/room/timeline/common.js | 47 +++++++++++++++++++ .../room/timeline/entries/EventEntry.js | 7 +-- .../entries/NonPersistedEventEntry.js | 18 +++++++ .../timeline/persistence/RelationWriter.js | 46 +----------------- 5 files changed, 78 insertions(+), 56 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index bb6c9ac5..eec848c7 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -254,7 +254,7 @@ export class Timeline { /** @package */ addEntries(newEntries) { this._addLocalRelationsToNewRemoteEntries(newEntries); - this._updateEntriesNotInTimeline(newEntries); + this._updateEntriesFetchedFromHomeserver(newEntries); this._moveEntryToRemoteEntries(newEntries); this._remoteEntries.setManySorted(newEntries); this._loadContextEntriesWhereNeeded(newEntries); @@ -262,13 +262,17 @@ export class Timeline { /** * Update entries based on newly received events. - * eg: a newly received redaction event may mark an existing event in contextEntriesNotInTimeline as being redacted - * This is only for the events that are not in the timeline but had to fetched from elsewhere to render reply previews. + * This is specific to events that are not in the timeline but had to be fetched from the homeserver. */ - _updateEntriesNotInTimeline(entries) { + _updateEntriesFetchedFromHomeserver(entries) { for (const entry of entries) { const relatedEntry = this._contextEntriesNotInTimeline.get(entry.relatedEventId); - if (!relatedEntry) { + if (!relatedEntry || !relatedEntry.isNonPersisted) { + /** + * Updates for entries in timeline is handled by remoteEntries observable collection + * Updates for entries not in timeline but fetched from storage is handled in this.replaceEntries() + * This code is specific to entries fetched from HomeServer i.e NonPersistedEventEntry + */ continue; } const newEntry = this._createEntryFromRelatedEntries(entry, relatedEntry); @@ -292,7 +296,7 @@ export class Timeline { _createEntryFromRelatedEntries(entry, relatedEntry) { if (entry.isRedaction) { const newEntry = relatedEntry.clone(); - newEntry.setAsRedacted(); + newEntry.redact(entry); return newEntry; } } diff --git a/src/matrix/room/timeline/common.js b/src/matrix/room/timeline/common.js index ec1ec499..7958f754 100644 --- a/src/matrix/room/timeline/common.js +++ b/src/matrix/room/timeline/common.js @@ -17,3 +17,50 @@ limitations under the License. export function isValidFragmentId(id) { return typeof id === "number"; } + +// copied over from matrix-js-sdk, copyright 2016 OpenMarket Ltd +/* _REDACT_KEEP_KEY_MAP gives the keys we keep when an event is redacted + * + * This is specified here: + * http://matrix.org/speculator/spec/HEAD/client_server/latest.html#redactions + * + * Also: + * - We keep 'unsigned' since that is created by the local server + * - We keep user_id for backwards-compat with v1 + */ +const _REDACT_KEEP_KEY_MAP = [ + 'event_id', 'type', 'room_id', 'user_id', 'sender', 'state_key', 'prev_state', + 'content', 'unsigned', 'origin_server_ts', +].reduce(function(ret, val) { + ret[val] = 1; return ret; +}, {}); + +// a map from event type to the .content keys we keep when an event is redacted +const _REDACT_KEEP_CONTENT_MAP = { + 'm.room.member': {'membership': 1}, + 'm.room.create': {'creator': 1}, + 'm.room.join_rules': {'join_rule': 1}, + 'm.room.power_levels': {'ban': 1, 'events': 1, 'events_default': 1, + 'kick': 1, 'redact': 1, 'state_default': 1, + 'users': 1, 'users_default': 1, + }, + 'm.room.aliases': {'aliases': 1}, +}; +// end of matrix-js-sdk code + +export function redactEvent(redactionEvent, redactedEvent) { + for (const key of Object.keys(redactedEvent)) { + if (!_REDACT_KEEP_KEY_MAP[key]) { + delete redactedEvent[key]; + } + } + const { content } = redactedEvent; + const keepMap = _REDACT_KEEP_CONTENT_MAP[redactedEvent.type]; + for (const key of Object.keys(content)) { + if (!keepMap?.[key]) { + delete content[key]; + } + } + redactedEvent.unsigned = redactedEvent.unsigned || {}; + redactedEvent.unsigned.redacted_because = redactionEvent; +} diff --git a/src/matrix/room/timeline/entries/EventEntry.js b/src/matrix/room/timeline/entries/EventEntry.js index 513d427a..ea019917 100644 --- a/src/matrix/room/timeline/entries/EventEntry.js +++ b/src/matrix/room/timeline/entries/EventEntry.js @@ -26,7 +26,6 @@ export class EventEntry extends BaseEventEntry { this._decryptionResult = null; this._contextEntry = null; this._contextForEntries = null; - this._markedAsRedacted = false; } clone() { @@ -58,10 +57,6 @@ export class EventEntry extends BaseEventEntry { this._contextForEntries.push(entry); } - setAsRedacted() { - this._markedAsRedacted = true; - } - get contextForEntries() { return this._contextForEntries; } @@ -160,7 +155,7 @@ export class EventEntry extends BaseEventEntry { } get isRedacted() { - return this._markedAsRedacted || super.isRedacted || isRedacted(this._eventEntry.event); + return super.isRedacted || isRedacted(this._eventEntry.event); } get redactionReason() { diff --git a/src/matrix/room/timeline/entries/NonPersistedEventEntry.js b/src/matrix/room/timeline/entries/NonPersistedEventEntry.js index 8ec9f72e..eebc9273 100644 --- a/src/matrix/room/timeline/entries/NonPersistedEventEntry.js +++ b/src/matrix/room/timeline/entries/NonPersistedEventEntry.js @@ -15,6 +15,7 @@ limitations under the License. */ import {EventEntry} from "./EventEntry.js"; +import {redactEvent} from "../common.js"; // EventEntry but without the two properties that are populated via SyncWriter // Useful if you want to create an EventEntry that is ephemeral @@ -27,4 +28,21 @@ export class NonPersistedEventEntry extends EventEntry { get entryIndex() { throw new Error("Cannot access entryIndex for non-persisted EventEntry"); } + + /** + * This method is needed because NonPersistedEventEntry cannot rely on RelationWriter to handle redactions + */ + redact(redactionEvent) { + redactEvent(redactionEvent.event, this.event); + } + + clone() { + const clone = new NonPersistedEventEntry(this._eventEntry, this._fragmentIdComparer); + clone.updateFrom(this); + return clone; + } + + get isNonPersisted() { + return true; + } } diff --git a/src/matrix/room/timeline/persistence/RelationWriter.js b/src/matrix/room/timeline/persistence/RelationWriter.js index 60a2b618..92f97671 100644 --- a/src/matrix/room/timeline/persistence/RelationWriter.js +++ b/src/matrix/room/timeline/persistence/RelationWriter.js @@ -17,6 +17,7 @@ limitations under the License. import {EventEntry} from "../entries/EventEntry.js"; import {REDACTION_TYPE, isRedacted} from "../../common.js"; import {ANNOTATION_RELATION_TYPE, getRelation} from "../relations.js"; +import {redactEvent} from "../common.js"; export class RelationWriter { constructor({roomId, ownUserId, fragmentIdComparer}) { @@ -127,21 +128,7 @@ export class RelationWriter { // check if we're the target of a relation and remove all relations then as well txn.timelineRelations.removeAllForTarget(this._roomId, redactedEvent.event_id); - for (const key of Object.keys(redactedEvent)) { - if (!_REDACT_KEEP_KEY_MAP[key]) { - delete redactedEvent[key]; - } - } - const {content} = redactedEvent; - const keepMap = _REDACT_KEEP_CONTENT_MAP[redactedEvent.type]; - for (const key of Object.keys(content)) { - if (!keepMap?.[key]) { - delete content[key]; - } - } - redactedEvent.unsigned = redactedEvent.unsigned || {}; - redactedEvent.unsigned.redacted_because = redactionEvent; - + redactEvent(redactionEvent, redactedEvent); delete redactedStorageEntry.annotations; return true; @@ -223,35 +210,6 @@ function isObjectEmpty(obj) { return true; } -// copied over from matrix-js-sdk, copyright 2016 OpenMarket Ltd -/* _REDACT_KEEP_KEY_MAP gives the keys we keep when an event is redacted - * - * This is specified here: - * http://matrix.org/speculator/spec/HEAD/client_server/latest.html#redactions - * - * Also: - * - We keep 'unsigned' since that is created by the local server - * - We keep user_id for backwards-compat with v1 - */ -const _REDACT_KEEP_KEY_MAP = [ - 'event_id', 'type', 'room_id', 'user_id', 'sender', 'state_key', 'prev_state', - 'content', 'unsigned', 'origin_server_ts', -].reduce(function(ret, val) { - ret[val] = 1; return ret; -}, {}); - -// a map from event type to the .content keys we keep when an event is redacted -const _REDACT_KEEP_CONTENT_MAP = { - 'm.room.member': {'membership': 1}, - 'm.room.create': {'creator': 1}, - 'm.room.join_rules': {'join_rule': 1}, - 'm.room.power_levels': {'ban': 1, 'events': 1, 'events_default': 1, - 'kick': 1, 'redact': 1, 'state_default': 1, - 'users': 1, 'users_default': 1, - }, - 'm.room.aliases': {'aliases': 1}, -}; -// end of matrix-js-sdk code import {createMockStorage} from "../../../../mocks/Storage"; import {createEvent, withTextBody, withRedacts, withContent} from "../../../../mocks/event.js"; From 3fecce6fe6d120e915d9b02c05eb60433ff822c8 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 7 Jan 2022 19:39:51 +0530 Subject: [PATCH 49/82] Fix tests --- src/matrix/room/timeline/Timeline.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index eec848c7..d24a30ff 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -819,7 +819,7 @@ export function tests() { "context entry in contextEntryNotInTimeline gets updated based on incoming redaction": async assert => { const timeline = new Timeline({roomId, storage: await createMockStorage(), closeCallback: () => {}, fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock()}); - const entryA = new EventEntry({ event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)) }); + const entryA = new NonPersistedEventEntry({ event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)) }); let event = withContent({ body: "bar", msgtype: "m.text", @@ -830,7 +830,8 @@ export function tests() { } }, createEvent("m.room.message", "event_id_2", bob)); const entryB = new EventEntry({ event }); - timeline._getEventFromStorage = () => entryA + timeline._getEventFromStorage = () => null; + timeline._getEventFromHomeserver = () => entryA; await timeline.load(new User(alice), "join", new NullLogItem()); await timeline._loadContextEntriesWhereNeeded([entryB]); const redactingEntry = new EventEntry({ event: withRedacts(entryA.id, "foo", createEvent("m.room.redaction", "event_id_3", alice)) }); @@ -842,7 +843,7 @@ export function tests() { "redaction of context entry triggers updates in other entries": async assert => { const timeline = new Timeline({roomId, storage: await createMockStorage(), closeCallback: () => {}, fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock()}); - const entryA = new EventEntry({ event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)) }); + const entryA = new NonPersistedEventEntry({ event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)) }); const content = { body: "bar", msgtype: "m.text", @@ -855,9 +856,10 @@ export function tests() { const entryB = new EventEntry({ event: withContent(content, createEvent("m.room.message", "event_id_2", bob)) }); const entryC = new EventEntry({ event: withContent(content, createEvent("m.room.message", "event_id_3", bob)) }); await timeline.load(new User(alice), "join", new NullLogItem()); - timeline._getEventFromStorage = () => entryA; + timeline._getEventFromStorage = () => null; + timeline._getEventFromHomeserver = () => entryA; timeline.addEntries([entryB, entryC]); - await poll(() => timeline._remoteEntries.array.length === 2); + await poll(() => timeline._remoteEntries.array.length === 2 && timeline._contextEntriesNotInTimeline.get(entryA.id)); const bin = [entryB, entryC]; timeline.entries.subscribe({ onUpdate: (index) => { From 7ad73bb453afbe20f04ffdded0ccad5f35e3cc3d Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 7 Jan 2022 19:56:31 +0530 Subject: [PATCH 50/82] Move check down --- src/matrix/room/timeline/Timeline.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index d24a30ff..55b3e4f5 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -267,7 +267,7 @@ export class Timeline { _updateEntriesFetchedFromHomeserver(entries) { for (const entry of entries) { const relatedEntry = this._contextEntriesNotInTimeline.get(entry.relatedEventId); - if (!relatedEntry || !relatedEntry.isNonPersisted) { + if (!relatedEntry) { /** * Updates for entries in timeline is handled by remoteEntries observable collection * Updates for entries not in timeline but fetched from storage is handled in this.replaceEntries() @@ -294,7 +294,7 @@ export class Timeline { * @returns a new entry or undefined */ _createEntryFromRelatedEntries(entry, relatedEntry) { - if (entry.isRedaction) { + if (entry.isRedaction && relatedEntry.isNonPersisted) { const newEntry = relatedEntry.clone(); newEntry.redact(entry); return newEntry; From ec8f6e8e0a887960df0f3d986743cca5d01a4e77 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 10 Jan 2022 12:58:45 +0530 Subject: [PATCH 51/82] use addLocalRelation --- src/matrix/room/timeline/Timeline.js | 34 ++++--------------- .../entries/NonPersistedEventEntry.js | 18 ---------- 2 files changed, 7 insertions(+), 45 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 55b3e4f5..ab24ef68 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -265,42 +265,22 @@ export class Timeline { * This is specific to events that are not in the timeline but had to be fetched from the homeserver. */ _updateEntriesFetchedFromHomeserver(entries) { + /** + * Updates for entries in timeline is handled by remoteEntries observable collection + * Updates for entries not in timeline but fetched from storage is handled in this.replaceEntries() + * This code is specific to entries fetched from HomeServer i.e NonPersistedEventEntry + */ for (const entry of entries) { const relatedEntry = this._contextEntriesNotInTimeline.get(entry.relatedEventId); - if (!relatedEntry) { - /** - * Updates for entries in timeline is handled by remoteEntries observable collection - * Updates for entries not in timeline but fetched from storage is handled in this.replaceEntries() - * This code is specific to entries fetched from HomeServer i.e NonPersistedEventEntry - */ - continue; - } - const newEntry = this._createEntryFromRelatedEntries(entry, relatedEntry); - if (newEntry) { - Timeline._entryUpdater(relatedEntry, newEntry); - this._contextEntriesNotInTimeline.set(newEntry.id, newEntry); + if (relatedEntry?.addLocalRelation(entry)) { // update other entries for which this entry is a context entry relatedEntry.contextForEntries?.forEach(e => { - this._remoteEntries.findAndUpdate(te => te.id === e.id, () => { return { reply: newEntry }; }); + this._remoteEntries.findAndUpdate(te => te.id === e.id, () => { return { reply: relatedEntry }; }); }); } } } - /** - * Creates a new entry based on two related entries - * @param {EventEntry} entry an entry - * @param {EventEntry} relatedEntry `entry` specifies something about this entry (eg: this entry is redacted) - * @returns a new entry or undefined - */ - _createEntryFromRelatedEntries(entry, relatedEntry) { - if (entry.isRedaction && relatedEntry.isNonPersisted) { - const newEntry = relatedEntry.clone(); - newEntry.redact(entry); - return newEntry; - } - } - /** * If an event we had to fetch from hs/storage is now in the timeline (for eg, due to gap fill), * remove the event from _contextEntriesNotInTimeline since it is now in remoteEntries diff --git a/src/matrix/room/timeline/entries/NonPersistedEventEntry.js b/src/matrix/room/timeline/entries/NonPersistedEventEntry.js index eebc9273..8ec9f72e 100644 --- a/src/matrix/room/timeline/entries/NonPersistedEventEntry.js +++ b/src/matrix/room/timeline/entries/NonPersistedEventEntry.js @@ -15,7 +15,6 @@ limitations under the License. */ import {EventEntry} from "./EventEntry.js"; -import {redactEvent} from "../common.js"; // EventEntry but without the two properties that are populated via SyncWriter // Useful if you want to create an EventEntry that is ephemeral @@ -28,21 +27,4 @@ export class NonPersistedEventEntry extends EventEntry { get entryIndex() { throw new Error("Cannot access entryIndex for non-persisted EventEntry"); } - - /** - * This method is needed because NonPersistedEventEntry cannot rely on RelationWriter to handle redactions - */ - redact(redactionEvent) { - redactEvent(redactionEvent.event, this.event); - } - - clone() { - const clone = new NonPersistedEventEntry(this._eventEntry, this._fragmentIdComparer); - clone.updateFrom(this); - return clone; - } - - get isNonPersisted() { - return true; - } } From 091b55a2652952296ab31759fd80c876c01d7a8d Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 10 Jan 2022 18:05:33 +0530 Subject: [PATCH 52/82] Rename method and add comment --- src/matrix/room/timeline/Timeline.js | 8 ++++---- src/matrix/room/timeline/entries/BaseEventEntry.js | 7 +++++-- src/matrix/room/timeline/entries/EventEntry.js | 4 ++-- src/matrix/room/timeline/entries/FragmentBoundaryEntry.js | 2 +- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index ab24ef68..1d2c9a17 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -124,7 +124,7 @@ export class Timeline { pendingEvent: pe, member: this._ownMember, clock: this._clock, redactingEntry }); - this._applyAndEmitLocalRelationChange(pee, target => target.addLocalRelation(pee)); + this._applyAndEmitLocalRelationChange(pee, target => target.addRelation(pee)); return pee; } @@ -203,12 +203,12 @@ export class Timeline { if (pee.relatedEventId) { const relationTarget = entries.find(e => e.id === pee.relatedEventId); // no need to emit here as this entry is about to be added - relationTarget?.addLocalRelation(pee); + relationTarget?.addRelation(pee); } if (pee.redactingEntry) { const eventId = pee.redactingEntry.relatedEventId; const relationTarget = entries.find(e => e.id === eventId); - relationTarget?.addLocalRelation(pee); + relationTarget?.addRelation(pee); } } } @@ -272,7 +272,7 @@ export class Timeline { */ for (const entry of entries) { const relatedEntry = this._contextEntriesNotInTimeline.get(entry.relatedEventId); - if (relatedEntry?.addLocalRelation(entry)) { + if (relatedEntry?.addRelation(entry)) { // update other entries for which this entry is a context entry relatedEntry.contextForEntries?.forEach(e => { this._remoteEntries.findAndUpdate(te => te.id === e.id, () => { return { reply: relatedEntry }; }); diff --git a/src/matrix/room/timeline/entries/BaseEventEntry.js b/src/matrix/room/timeline/entries/BaseEventEntry.js index 2869bf68..4ee23b50 100644 --- a/src/matrix/room/timeline/entries/BaseEventEntry.js +++ b/src/matrix/room/timeline/entries/BaseEventEntry.js @@ -53,10 +53,13 @@ export class BaseEventEntry extends BaseEntry { } /** - aggregates local relation or local redaction of remote relation. + Aggregates relation or redaction of remote relation. + Used in two situations: + - to aggregate local relation/redaction of remote relation + - to mark this entry as being redacted in Timeline._updateEntriesFetchedFromHomeserver @return [string] returns the name of the field that has changed, if any */ - addLocalRelation(entry) { + addRelation(entry) { if (entry.eventType === REDACTION_TYPE && entry.isRelatedToId(this.id)) { if (!this._pendingRedactions) { this._pendingRedactions = []; diff --git a/src/matrix/room/timeline/entries/EventEntry.js b/src/matrix/room/timeline/entries/EventEntry.js index ea019917..13a10103 100644 --- a/src/matrix/room/timeline/entries/EventEntry.js +++ b/src/matrix/room/timeline/entries/EventEntry.js @@ -190,7 +190,7 @@ export function tests() { function addPendingReaction(target, key) { queueIndex += 1; - target.addLocalRelation(new PendingEventEntry({ + target.addRelation(new PendingEventEntry({ pendingEvent: new PendingEvent({data: { eventType: "m.reaction", content: createAnnotation(target.id, key), @@ -212,7 +212,7 @@ export function tests() { }); } queueIndex += 1; - target.addLocalRelation(new PendingEventEntry({ + target.addRelation(new PendingEventEntry({ pendingEvent: new PendingEvent({data: { eventType: "m.room.redaction", relatedTxnId: pendingReaction ? pendingReaction.id : null, diff --git a/src/matrix/room/timeline/entries/FragmentBoundaryEntry.js b/src/matrix/room/timeline/entries/FragmentBoundaryEntry.js index 7a8ed5d0..5d9ae4ca 100644 --- a/src/matrix/room/timeline/entries/FragmentBoundaryEntry.js +++ b/src/matrix/room/timeline/entries/FragmentBoundaryEntry.js @@ -134,6 +134,6 @@ export class FragmentBoundaryEntry extends BaseEntry { return new FragmentBoundaryEntry(neighbour, !this._isFragmentStart, this._fragmentIdComparer); } - addLocalRelation() {} + addRelation() {} removeLocalRelation() {} } From 66fa8d84a791fae963d2ef346fcd7e62ddd85682 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 10 Jan 2022 18:51:12 +0530 Subject: [PATCH 53/82] Make setAsContextOf private --- src/matrix/room/timeline/entries/EventEntry.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/matrix/room/timeline/entries/EventEntry.js b/src/matrix/room/timeline/entries/EventEntry.js index 13a10103..032c26c4 100644 --- a/src/matrix/room/timeline/entries/EventEntry.js +++ b/src/matrix/room/timeline/entries/EventEntry.js @@ -47,10 +47,10 @@ export class EventEntry extends BaseEventEntry { setContextEntry(entry) { this._contextEntry = entry; - entry.setAsContextOf(this); + entry._setAsContextOf(this); } - setAsContextOf(entry) { + _setAsContextOf(entry) { if (!this._contextForEntries) { this._contextForEntries = []; } From 93bbeee400ce209dd6f5ffaf83b3f797e1a5ca19 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 11 Jan 2022 11:49:06 +0530 Subject: [PATCH 54/82] Don't pass relatedEntry in param --- src/matrix/room/timeline/Timeline.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 1d2c9a17..4b685fcd 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -275,7 +275,7 @@ export class Timeline { if (relatedEntry?.addRelation(entry)) { // update other entries for which this entry is a context entry relatedEntry.contextForEntries?.forEach(e => { - this._remoteEntries.findAndUpdate(te => te.id === e.id, () => { return { reply: relatedEntry }; }); + this._remoteEntries.findAndUpdate(te => te.id === e.id, () => true); }); } } From 63b6564f70e9a8bd0a00c5b077df1403ad096d55 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 11 Jan 2022 11:54:41 +0530 Subject: [PATCH 55/82] Pass prop change --- src/matrix/room/timeline/Timeline.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 4b685fcd..1bf68795 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -275,7 +275,7 @@ export class Timeline { if (relatedEntry?.addRelation(entry)) { // update other entries for which this entry is a context entry relatedEntry.contextForEntries?.forEach(e => { - this._remoteEntries.findAndUpdate(te => te.id === e.id, () => true); + this._remoteEntries.findAndUpdate(te => te.id === e.id, () => "contextEntry"); }); } } From fda211e7b33ff0a71c9c9c2fcf46d7fbe39b99fb Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 11 Jan 2022 13:10:40 +0530 Subject: [PATCH 56/82] Remove dead code --- src/matrix/room/timeline/Timeline.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 1bf68795..de1a50cf 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -289,8 +289,10 @@ export class Timeline { for (const entry of entries) { const fetchedEntry = this._contextEntriesNotInTimeline.get(entry.id); if (fetchedEntry) { - fetchedEntry.contextForEntries.forEach(e => e.setContextEntry(entry)); - entry.updateFrom(fetchedEntry); + fetchedEntry.contextForEntries.forEach(e => { + e.setContextEntry(entry); + this._updateEntry(e); + }); this._contextEntriesNotInTimeline.delete(entry.id); } } From 62dcb61536f7d225032217dab838ecd5ae91c65e Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 11 Jan 2022 13:11:50 +0530 Subject: [PATCH 57/82] Rename updateEntry -> emitUpdateForEntry --- src/matrix/room/timeline/Timeline.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index de1a50cf..8f488611 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -233,7 +233,7 @@ export class Timeline { this._contextEntriesNotInTimeline.set(entry.id, entry); } // Since this entry changed, all dependent entries should be updated - entry.contextForEntries?.forEach(e => this._updateEntry(e)); + entry.contextForEntries?.forEach(e => this._emitUpdateForEntry(e)); } catch (err) { if (err.name === "CompareError") { // see FragmentIdComparer, if the replacing entry is on a fragment @@ -291,14 +291,14 @@ export class Timeline { if (fetchedEntry) { fetchedEntry.contextForEntries.forEach(e => { e.setContextEntry(entry); - this._updateEntry(e); + this._emitUpdateForEntry(e); }); this._contextEntriesNotInTimeline.delete(entry.id); } } } - _updateEntry(entry) { + _emitUpdateForEntry(entry) { const txnId = entry.isPending ? entry.id : null; const eventId = entry.isPending ? null : entry.id; this._findAndUpdateEntryById(txnId, eventId, () => true); @@ -327,7 +327,7 @@ export class Timeline { } if (contextEvent) { entry.setContextEntry(contextEvent); - this._updateEntry(entry); + this._emitUpdateForEntry(entry); } } } From 31a8227e531eddfc18edf9af333af6939232116a Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 11 Jan 2022 13:14:13 +0530 Subject: [PATCH 58/82] stylistic change --- src/matrix/room/timeline/Timeline.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 8f488611..8dbe8b69 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -313,8 +313,10 @@ export class Timeline { * @param {EventEntry[]} entries */ async _loadContextEntriesWhereNeeded(entries) { - const entriesNeedingContext = entries.filter(e => !!e.contextEventId); - for (const entry of entriesNeedingContext) { + for (const entry of entries) { + if (!entry.contextEventId) { + continue; + } const id = entry.contextEventId; let contextEvent = this._getTrackedEntry(id); if (!contextEvent) { From f605608098705399d599f9b978020989046d682f Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 11 Jan 2022 13:20:42 +0530 Subject: [PATCH 59/82] getTrackedEntry -> findLoadedEventById --- src/matrix/room/timeline/Timeline.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 8dbe8b69..635ad0ce 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -318,7 +318,7 @@ export class Timeline { continue; } const id = entry.contextEventId; - let contextEvent = this._getTrackedEntry(id); + let contextEvent = this._findLoadedEventById(id); if (!contextEvent) { contextEvent = await this._getEventFromStorage(id) ?? await this._getEventFromHomeserver(id); if (contextEvent) { @@ -339,7 +339,7 @@ export class Timeline { * @param {string} eventId event-id of the entry * @returns entry if found, undefined otherwise */ - _getTrackedEntry(eventId) { + _findLoadedEventById(eventId) { return this.getByEventId(eventId) ?? this._contextEntriesNotInTimeline.get(eventId); } From bf6dfcfcadf1016e6b6f7c998f12834dd86fd65d Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 11 Jan 2022 13:28:35 +0530 Subject: [PATCH 60/82] update comment --- src/matrix/room/timeline/Timeline.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 635ad0ce..acc2af77 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -262,7 +262,9 @@ export class Timeline { /** * Update entries based on newly received events. - * This is specific to events that are not in the timeline but had to be fetched from the homeserver. + * This is specific to events that are not in the timeline but had to be fetched from the homeserver + * because they are context-events for other events in the timeline (i.e fetched from hs so that we + * can render things like reply previews) */ _updateEntriesFetchedFromHomeserver(entries) { /** From 73733ce145ababc3e1a86d22d98c8603114b5152 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 11 Jan 2022 14:49:59 +0530 Subject: [PATCH 61/82] Guard entry from storage being processed by method --- src/matrix/room/timeline/Timeline.js | 2 +- src/matrix/room/timeline/entries/NonPersistedEventEntry.js | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index acc2af77..3c1d8236 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -274,7 +274,7 @@ export class Timeline { */ for (const entry of entries) { const relatedEntry = this._contextEntriesNotInTimeline.get(entry.relatedEventId); - if (relatedEntry?.addRelation(entry)) { + if (relatedEntry?.isNonPersisted && relatedEntry?.addRelation(entry)) { // update other entries for which this entry is a context entry relatedEntry.contextForEntries?.forEach(e => { this._remoteEntries.findAndUpdate(te => te.id === e.id, () => "contextEntry"); diff --git a/src/matrix/room/timeline/entries/NonPersistedEventEntry.js b/src/matrix/room/timeline/entries/NonPersistedEventEntry.js index 8ec9f72e..cb8df8a3 100644 --- a/src/matrix/room/timeline/entries/NonPersistedEventEntry.js +++ b/src/matrix/room/timeline/entries/NonPersistedEventEntry.js @@ -27,4 +27,8 @@ export class NonPersistedEventEntry extends EventEntry { get entryIndex() { throw new Error("Cannot access entryIndex for non-persisted EventEntry"); } + + get isNonPersisted() { + return true; + } } From 5c1813888c352856181025533d6aa68eba5820d8 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 11 Jan 2022 14:57:22 +0530 Subject: [PATCH 62/82] Check in all entries for context --- src/matrix/room/timeline/Timeline.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 3c1d8236..c5fc98a2 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -337,12 +337,12 @@ export class Timeline { } /** - * Fetches an entry with the given event-id from remoteEntries or contextEntriesNotInTimeline. + * Fetches an entry with the given event-id from localEntries, remoteEntries or contextEntriesNotInTimeline. * @param {string} eventId event-id of the entry * @returns entry if found, undefined otherwise */ _findLoadedEventById(eventId) { - return this.getByEventId(eventId) ?? this._contextEntriesNotInTimeline.get(eventId); + return this.getFromAllEntriesById(eventId) ?? this._contextEntriesNotInTimeline.get(eventId); } async _getEventFromStorage(eventId) { @@ -421,6 +421,16 @@ export class Timeline { return null; } + getFromAllEntriesById(eventId) { + for (let i = 0; i < this.entries.length; i += 1) { + const entry = this.entries.get(i); + if (entry.id === eventId) { + return entry; + } + } + return null; + } + /** @public */ get entries() { return this._allEntries; From a59bf7c002955ae82a1c4bfd8b68432f575c048d Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 11 Jan 2022 20:57:29 +0530 Subject: [PATCH 63/82] Fix looking in allEntries --- src/matrix/room/timeline/Timeline.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index c5fc98a2..bb6554b8 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -422,8 +422,11 @@ export class Timeline { } getFromAllEntriesById(eventId) { - for (let i = 0; i < this.entries.length; i += 1) { - const entry = this.entries.get(i); + if (!this._allEntries) { + // if allEntries isn't loaded yet, fallback to looking only in remoteEntries + return this.getByEventId(eventId); + } + for (const entry of this._allEntries) { if (entry.id === eventId) { return entry; } From acafae7d3aa8eb635da119e38601fa8f5b35d2f8 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 11 Jan 2022 20:58:27 +0530 Subject: [PATCH 64/82] Implement offline support for context entries --- src/matrix/room/timeline/Timeline.js | 2 + .../room/timeline/entries/BaseEventEntry.js | 22 +++++++++++ .../room/timeline/entries/EventEntry.js | 38 +++++++------------ .../timeline/entries/PendingEventEntry.js | 7 ++++ 4 files changed, 45 insertions(+), 24 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index bb6554b8..56920c6f 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -101,6 +101,7 @@ export class Timeline { pe => this._mapPendingEventToEntry(pe), (pee, params) => { // is sending but redacted, who do we detect that here to remove the relation? + this._loadContextEntriesWhereNeeded([pee]); pee.notifyUpdate(params); }, pee => this._applyAndEmitLocalRelationChange(pee, target => target.removeLocalRelation(pee)) @@ -124,6 +125,7 @@ export class Timeline { pendingEvent: pe, member: this._ownMember, clock: this._clock, redactingEntry }); + this._loadContextEntriesWhereNeeded([pee]); this._applyAndEmitLocalRelationChange(pee, target => target.addRelation(pee)); return pee; } diff --git a/src/matrix/room/timeline/entries/BaseEventEntry.js b/src/matrix/room/timeline/entries/BaseEventEntry.js index 4ee23b50..eeda4464 100644 --- a/src/matrix/room/timeline/entries/BaseEventEntry.js +++ b/src/matrix/room/timeline/entries/BaseEventEntry.js @@ -27,6 +27,8 @@ export class BaseEventEntry extends BaseEntry { super(fragmentIdComparer); this._pendingRedactions = null; this._pendingAnnotations = null; + this._contextEntry = null; + this._contextForEntries = null; } get isReply() { @@ -52,6 +54,26 @@ export class BaseEventEntry extends BaseEntry { return null; } + setContextEntry(entry) { + this._contextEntry = entry; + entry._setAsContextOf(this); + } + + _setAsContextOf(entry) { + if (!this._contextForEntries) { + this._contextForEntries = []; + } + this._contextForEntries.push(entry); + } + + get contextForEntries() { + return this._contextForEntries; + } + + get contextEntry() { + return this._contextEntry; + } + /** Aggregates relation or redaction of remote relation. Used in two situations: diff --git a/src/matrix/room/timeline/entries/EventEntry.js b/src/matrix/room/timeline/entries/EventEntry.js index 032c26c4..0d493825 100644 --- a/src/matrix/room/timeline/entries/EventEntry.js +++ b/src/matrix/room/timeline/entries/EventEntry.js @@ -24,8 +24,6 @@ export class EventEntry extends BaseEventEntry { this._eventEntry = eventEntry; this._decryptionError = null; this._decryptionResult = null; - this._contextEntry = null; - this._contextForEntries = null; } clone() { @@ -45,20 +43,8 @@ export class EventEntry extends BaseEventEntry { this._contextEntry = other.contextEntry; } - setContextEntry(entry) { - this._contextEntry = entry; - entry._setAsContextOf(this); - } - - _setAsContextOf(entry) { - if (!this._contextForEntries) { - this._contextForEntries = []; - } - this._contextForEntries.push(entry); - } - - get contextForEntries() { - return this._contextForEntries; + setRelatedEntry(entry) { + this._relatedEntry = entry; } get event() { @@ -142,18 +128,13 @@ export class EventEntry extends BaseEventEntry { return getRelatedEventId(this.event); } - // similar to relatedEventID but only for replies - get contextEventId() { - if (this.isReply) { - return this.relatedEventId; + get threadEventId() { + if (this.isThread) { + return this.relation?.event_id; } return null; } - get contextEntry() { - return this._contextEntry; - } - get isRedacted() { return super.isRedacted || isRedacted(this._eventEntry.event); } @@ -176,6 +157,15 @@ export class EventEntry extends BaseEventEntry { const originalRelation = originalContent && getRelationFromContent(originalContent); return originalRelation || getRelationFromContent(this.content); } + + // similar to relatedEventID but only for replies + get contextEventId() { + if (this.isReply) { + return this.relatedEventId; + } + return null; + } + } import {withTextBody, withContent, createEvent} from "../../../../mocks/event.js"; diff --git a/src/matrix/room/timeline/entries/PendingEventEntry.js b/src/matrix/room/timeline/entries/PendingEventEntry.js index 4bc3c47e..bb4ed407 100644 --- a/src/matrix/room/timeline/entries/PendingEventEntry.js +++ b/src/matrix/room/timeline/entries/PendingEventEntry.js @@ -100,4 +100,11 @@ export class PendingEventEntry extends BaseEventEntry { get redactingEntry() { return this._redactingEntry; } + + get contextEventId() { + if (this.isReply) { + return this._pendingEvent.relatedEventId ?? this._pendingEvent.relatedTxnId; + } + return null; + } } From d0f7570f5e9a3eb0339bbf70732e7c9798c30953 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 12 Jan 2022 18:44:17 +0530 Subject: [PATCH 65/82] Fix tests --- src/matrix/room/timeline/Timeline.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 56920c6f..71b699c9 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -751,6 +751,8 @@ export function tests() { }, createEvent("m.room.message", "event_id_2", bob)); const entryB = new EventEntry({ event }); await timeline.load(new User(alice), "join", new NullLogItem()); + timeline._setupEntries([]); + timeline._localEntries.onSubscribeFirst(); timeline._remoteEntries.setManyUnsorted([entryA, entryB]); await timeline._loadContextEntriesWhereNeeded([entryA, entryB]); assert.deepEqual(entryB.contextEntry, entryA); @@ -772,6 +774,8 @@ export function tests() { const entryB = new EventEntry({ event }); timeline._getEventFromStorage = () => entryA await timeline.load(new User(alice), "join", new NullLogItem()); + timeline._setupEntries([]); + timeline._localEntries.onSubscribeFirst(); await timeline._loadContextEntriesWhereNeeded([entryB]); assert.deepEqual(entryB.contextEntry, entryA); }, @@ -792,6 +796,8 @@ export function tests() { const entryB = new EventEntry({ event }); timeline._getEventFromHomeserver = () => entryA await timeline.load(new User(alice), "join", new NullLogItem()); + timeline._setupEntries([]); + timeline._localEntries.onSubscribeFirst(); await timeline._loadContextEntriesWhereNeeded([entryB]); assert.deepEqual(entryB.contextEntry, entryA); }, @@ -811,7 +817,12 @@ export function tests() { }; const entryB = new EventEntry({ event: withContent(content, createEvent("m.room.message", "event_id_2", bob)) }); const entryC = new EventEntry({ event: withContent(content, createEvent("m.room.message", "event_id_3", bob)) }); + entryA._eventEntry.eventIndex = 1; + entryB._eventEntry.eventIndex = 2; + entryC._eventEntry.eventIndex = 3; await timeline.load(new User(alice), "join", new NullLogItem()); + timeline._setupEntries([]); + timeline._localEntries.onSubscribeFirst(); timeline._remoteEntries.setManyUnsorted([entryA, entryB, entryC]); await timeline._loadContextEntriesWhereNeeded([entryA, entryB, entryC]); assert.deepEqual(entryA.contextForEntries, [entryB, entryC]); @@ -834,6 +845,8 @@ export function tests() { timeline._getEventFromStorage = () => null; timeline._getEventFromHomeserver = () => entryA; await timeline.load(new User(alice), "join", new NullLogItem()); + timeline._setupEntries([]); + timeline._localEntries.onSubscribeFirst(); await timeline._loadContextEntriesWhereNeeded([entryB]); const redactingEntry = new EventEntry({ event: withRedacts(entryA.id, "foo", createEvent("m.room.redaction", "event_id_3", alice)) }); timeline.addEntries([redactingEntry]); @@ -856,7 +869,12 @@ export function tests() { }; const entryB = new EventEntry({ event: withContent(content, createEvent("m.room.message", "event_id_2", bob)) }); const entryC = new EventEntry({ event: withContent(content, createEvent("m.room.message", "event_id_3", bob)) }); + entryA._eventEntry.eventIndex = 1; + entryB._eventEntry.eventIndex = 2; + entryC._eventEntry.eventIndex = 3; await timeline.load(new User(alice), "join", new NullLogItem()); + timeline._setupEntries([]); + timeline._localEntries.onSubscribeFirst(); timeline._getEventFromStorage = () => null; timeline._getEventFromHomeserver = () => entryA; timeline.addEntries([entryB, entryC]); @@ -888,8 +906,12 @@ export function tests() { } }, createEvent("m.room.message", "event_id_2", bob)); const entryB = new EventEntry({ event }); + entryA._eventEntry.eventIndex = 1; + entryB._eventEntry.eventIndex = 2; timeline._getEventFromHomeserver = () => entryA await timeline.load(new User(alice), "join", new NullLogItem()); + timeline._setupEntries([]); + timeline._localEntries.onSubscribeFirst(); timeline.addEntries([entryB]); await timeline._loadContextEntriesWhereNeeded([entryB]); assert.strictEqual(timeline._contextEntriesNotInTimeline.has(entryA.id), true); From ed8818475783c4bd542f238c73fb5453a629577c Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 12 Jan 2022 19:14:38 +0530 Subject: [PATCH 66/82] Remove statement --- src/matrix/room/timeline/Timeline.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 71b699c9..c9dfaf17 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -101,7 +101,6 @@ export class Timeline { pe => this._mapPendingEventToEntry(pe), (pee, params) => { // is sending but redacted, who do we detect that here to remove the relation? - this._loadContextEntriesWhereNeeded([pee]); pee.notifyUpdate(params); }, pee => this._applyAndEmitLocalRelationChange(pee, target => target.removeLocalRelation(pee)) From 2f4c0623d0dfd5be5b67174907a2ee5e810ec072 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 12 Jan 2022 19:20:32 +0530 Subject: [PATCH 67/82] Restore earlier name --- src/matrix/room/timeline/Timeline.js | 8 ++++---- src/matrix/room/timeline/entries/BaseEventEntry.js | 2 +- src/matrix/room/timeline/entries/EventEntry.js | 4 ++-- src/matrix/room/timeline/entries/FragmentBoundaryEntry.js | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index c9dfaf17..bc7f213a 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -125,7 +125,7 @@ export class Timeline { clock: this._clock, redactingEntry }); this._loadContextEntriesWhereNeeded([pee]); - this._applyAndEmitLocalRelationChange(pee, target => target.addRelation(pee)); + this._applyAndEmitLocalRelationChange(pee, target => target.addLocalRelation(pee)); return pee; } @@ -204,12 +204,12 @@ export class Timeline { if (pee.relatedEventId) { const relationTarget = entries.find(e => e.id === pee.relatedEventId); // no need to emit here as this entry is about to be added - relationTarget?.addRelation(pee); + relationTarget?.addLocalRelation(pee); } if (pee.redactingEntry) { const eventId = pee.redactingEntry.relatedEventId; const relationTarget = entries.find(e => e.id === eventId); - relationTarget?.addRelation(pee); + relationTarget?.addLocalRelation(pee); } } } @@ -275,7 +275,7 @@ export class Timeline { */ for (const entry of entries) { const relatedEntry = this._contextEntriesNotInTimeline.get(entry.relatedEventId); - if (relatedEntry?.isNonPersisted && relatedEntry?.addRelation(entry)) { + if (relatedEntry?.isNonPersisted && relatedEntry?.addLocalRelation(entry)) { // update other entries for which this entry is a context entry relatedEntry.contextForEntries?.forEach(e => { this._remoteEntries.findAndUpdate(te => te.id === e.id, () => "contextEntry"); diff --git a/src/matrix/room/timeline/entries/BaseEventEntry.js b/src/matrix/room/timeline/entries/BaseEventEntry.js index eeda4464..ec20f48a 100644 --- a/src/matrix/room/timeline/entries/BaseEventEntry.js +++ b/src/matrix/room/timeline/entries/BaseEventEntry.js @@ -81,7 +81,7 @@ export class BaseEventEntry extends BaseEntry { - to mark this entry as being redacted in Timeline._updateEntriesFetchedFromHomeserver @return [string] returns the name of the field that has changed, if any */ - addRelation(entry) { + addLocalRelation(entry) { if (entry.eventType === REDACTION_TYPE && entry.isRelatedToId(this.id)) { if (!this._pendingRedactions) { this._pendingRedactions = []; diff --git a/src/matrix/room/timeline/entries/EventEntry.js b/src/matrix/room/timeline/entries/EventEntry.js index 0d493825..247c1b21 100644 --- a/src/matrix/room/timeline/entries/EventEntry.js +++ b/src/matrix/room/timeline/entries/EventEntry.js @@ -180,7 +180,7 @@ export function tests() { function addPendingReaction(target, key) { queueIndex += 1; - target.addRelation(new PendingEventEntry({ + target.addLocalRelation(new PendingEventEntry({ pendingEvent: new PendingEvent({data: { eventType: "m.reaction", content: createAnnotation(target.id, key), @@ -202,7 +202,7 @@ export function tests() { }); } queueIndex += 1; - target.addRelation(new PendingEventEntry({ + target.addLocalRelation(new PendingEventEntry({ pendingEvent: new PendingEvent({data: { eventType: "m.room.redaction", relatedTxnId: pendingReaction ? pendingReaction.id : null, diff --git a/src/matrix/room/timeline/entries/FragmentBoundaryEntry.js b/src/matrix/room/timeline/entries/FragmentBoundaryEntry.js index 5d9ae4ca..7a8ed5d0 100644 --- a/src/matrix/room/timeline/entries/FragmentBoundaryEntry.js +++ b/src/matrix/room/timeline/entries/FragmentBoundaryEntry.js @@ -134,6 +134,6 @@ export class FragmentBoundaryEntry extends BaseEntry { return new FragmentBoundaryEntry(neighbour, !this._isFragmentStart, this._fragmentIdComparer); } - addRelation() {} + addLocalRelation() {} removeLocalRelation() {} } From ca1831fef65bb3f38c8d225a948db613836386df Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 13 Jan 2022 14:38:05 +0530 Subject: [PATCH 68/82] update contextForEntries --- src/matrix/room/timeline/Timeline.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index bc7f213a..4db4abf0 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -141,6 +141,7 @@ export class Timeline { // redactingEntry might be a PendingEventEntry or an EventEntry, so don't assume pendingEvent const relatedTxnId = pee.redactingEntry.pendingEvent?.relatedTxnId; this._findAndUpdateEntryById(relatedTxnId, pee.redactingEntry.relatedEventId, updateOrFalse); + pee.redactingEntry.contextForEntries?.forEach(e => this._emitUpdateForEntry(e)); } } From 764541d3ca504632b7ace91bf6bc23c09f2921b5 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 13 Jan 2022 18:32:18 +0530 Subject: [PATCH 69/82] Remove unused method --- src/matrix/room/timeline/entries/EventEntry.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/matrix/room/timeline/entries/EventEntry.js b/src/matrix/room/timeline/entries/EventEntry.js index 247c1b21..3d9c982b 100644 --- a/src/matrix/room/timeline/entries/EventEntry.js +++ b/src/matrix/room/timeline/entries/EventEntry.js @@ -43,10 +43,6 @@ export class EventEntry extends BaseEventEntry { this._contextEntry = other.contextEntry; } - setRelatedEntry(entry) { - this._relatedEntry = entry; - } - get event() { return this._eventEntry.event; } From 239d16747d4061953b4148785e2d2d3c25e8be22 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 13 Jan 2022 19:14:20 +0530 Subject: [PATCH 70/82] Clean test code; try not to peek into internals --- src/matrix/room/timeline/Timeline.js | 86 +++++++++++----------------- 1 file changed, 35 insertions(+), 51 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 4db4abf0..c1989a0d 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -751,17 +751,15 @@ export function tests() { }, createEvent("m.room.message", "event_id_2", bob)); const entryB = new EventEntry({ event }); await timeline.load(new User(alice), "join", new NullLogItem()); - timeline._setupEntries([]); - timeline._localEntries.onSubscribeFirst(); - timeline._remoteEntries.setManyUnsorted([entryA, entryB]); - await timeline._loadContextEntriesWhereNeeded([entryA, entryB]); + timeline.entries.subscribe({ onAdd: () => null, }); + timeline.addEntries([entryA, entryB]); assert.deepEqual(entryB.contextEntry, entryA); }, "context entry is fetched from storage": async assert => { const timeline = new Timeline({roomId, storage: await createMockStorage(), closeCallback: () => {}, fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock()}); - const entryA = new EventEntry({ event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)) }); + const entryA = new EventEntry({ event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)), eventIndex: 1 }); let event = withContent({ body: "bar", msgtype: "m.text", @@ -771,19 +769,19 @@ export function tests() { } } }, createEvent("m.room.message", "event_id_2", bob)); - const entryB = new EventEntry({ event }); + const entryB = new EventEntry({ event, eventIndex: 2 }); timeline._getEventFromStorage = () => entryA await timeline.load(new User(alice), "join", new NullLogItem()); - timeline._setupEntries([]); - timeline._localEntries.onSubscribeFirst(); - await timeline._loadContextEntriesWhereNeeded([entryB]); + timeline.entries.subscribe({ onAdd: () => null, onUpdate: () => null }); + timeline.addEntries([entryB]); + await poll(() => entryB.contextEntry); assert.deepEqual(entryB.contextEntry, entryA); }, "context entry is fetched from hs": async assert => { const timeline = new Timeline({roomId, storage: await createMockStorage(), closeCallback: () => {}, fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock()}); - const entryA = new EventEntry({ event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)) }); + const entryA = new EventEntry({ event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)), eventIndex: 1 }); let event = withContent({ body: "bar", msgtype: "m.text", @@ -793,19 +791,19 @@ export function tests() { } } }, createEvent("m.room.message", "event_id_2", bob)); - const entryB = new EventEntry({ event }); + const entryB = new EventEntry({ event, eventIndex: 2 }); timeline._getEventFromHomeserver = () => entryA await timeline.load(new User(alice), "join", new NullLogItem()); - timeline._setupEntries([]); - timeline._localEntries.onSubscribeFirst(); - await timeline._loadContextEntriesWhereNeeded([entryB]); + timeline.entries.subscribe({ onAdd: () => null, onUpdate: () => null }); + timeline.addEntries([entryB]); + await poll(() => entryB.contextEntry); assert.deepEqual(entryB.contextEntry, entryA); }, "context entry has a list of entries to which it forms the context": async assert => { const timeline = new Timeline({roomId, storage: await createMockStorage(), closeCallback: () => {}, fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock()}); - const entryA = new EventEntry({ event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)) }); + const entryA = new EventEntry({ event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)), eventIndex: 1 }); const content = { body: "bar", msgtype: "m.text", @@ -815,23 +813,19 @@ export function tests() { } } }; - const entryB = new EventEntry({ event: withContent(content, createEvent("m.room.message", "event_id_2", bob)) }); - const entryC = new EventEntry({ event: withContent(content, createEvent("m.room.message", "event_id_3", bob)) }); - entryA._eventEntry.eventIndex = 1; - entryB._eventEntry.eventIndex = 2; - entryC._eventEntry.eventIndex = 3; + const entryB = new EventEntry({ event: withContent(content, createEvent("m.room.message", "event_id_2", bob)), eventIndex: 2 }); + const entryC = new EventEntry({ event: withContent(content, createEvent("m.room.message", "event_id_3", bob)), eventIndex: 3 }); await timeline.load(new User(alice), "join", new NullLogItem()); - timeline._setupEntries([]); - timeline._localEntries.onSubscribeFirst(); - timeline._remoteEntries.setManyUnsorted([entryA, entryB, entryC]); - await timeline._loadContextEntriesWhereNeeded([entryA, entryB, entryC]); + timeline.entries.subscribe({ onAdd: () => null, onUpdate: () => null }); + timeline.addEntries([entryA, entryB, entryC]); + await poll(() => entryA.contextForEntries.length === 2); assert.deepEqual(entryA.contextForEntries, [entryB, entryC]); }, "context entry in contextEntryNotInTimeline gets updated based on incoming redaction": async assert => { const timeline = new Timeline({roomId, storage: await createMockStorage(), closeCallback: () => {}, fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock()}); - const entryA = new NonPersistedEventEntry({ event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)) }); + const entryA = new NonPersistedEventEntry({ event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)), eventIndex: 1 }); let event = withContent({ body: "bar", msgtype: "m.text", @@ -841,14 +835,13 @@ export function tests() { } } }, createEvent("m.room.message", "event_id_2", bob)); - const entryB = new EventEntry({ event }); - timeline._getEventFromStorage = () => null; + const entryB = new EventEntry({ event, eventIndex: 2 }); timeline._getEventFromHomeserver = () => entryA; await timeline.load(new User(alice), "join", new NullLogItem()); - timeline._setupEntries([]); - timeline._localEntries.onSubscribeFirst(); - await timeline._loadContextEntriesWhereNeeded([entryB]); - const redactingEntry = new EventEntry({ event: withRedacts(entryA.id, "foo", createEvent("m.room.redaction", "event_id_3", alice)) }); + timeline.entries.subscribe({ onAdd: () => null, onUpdate: () => null }); + timeline.addEntries([entryB]); + await poll(() => entryB.contextEntry); + const redactingEntry = new EventEntry({ event: withRedacts(entryA.id, "foo", createEvent("m.room.redaction", "event_id_3", alice)), eventIndex: 3 }); timeline.addEntries([redactingEntry]); const contextEntry = timeline._contextEntriesNotInTimeline.get(entryA.id); assert.strictEqual(contextEntry.isRedacted, true); @@ -857,7 +850,7 @@ export function tests() { "redaction of context entry triggers updates in other entries": async assert => { const timeline = new Timeline({roomId, storage: await createMockStorage(), closeCallback: () => {}, fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock()}); - const entryA = new NonPersistedEventEntry({ event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)) }); + const entryA = new NonPersistedEventEntry({ event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)), eventIndex: 1 }); const content = { body: "bar", msgtype: "m.text", @@ -867,26 +860,20 @@ export function tests() { } } }; - const entryB = new EventEntry({ event: withContent(content, createEvent("m.room.message", "event_id_2", bob)) }); - const entryC = new EventEntry({ event: withContent(content, createEvent("m.room.message", "event_id_3", bob)) }); - entryA._eventEntry.eventIndex = 1; - entryB._eventEntry.eventIndex = 2; - entryC._eventEntry.eventIndex = 3; + const entryB = new EventEntry({ event: withContent(content, createEvent("m.room.message", "event_id_2", bob)), eventIndex: 2 }); + const entryC = new EventEntry({ event: withContent(content, createEvent("m.room.message", "event_id_3", bob)), eventIndex: 3 }); await timeline.load(new User(alice), "join", new NullLogItem()); - timeline._setupEntries([]); - timeline._localEntries.onSubscribeFirst(); - timeline._getEventFromStorage = () => null; - timeline._getEventFromHomeserver = () => entryA; - timeline.addEntries([entryB, entryC]); - await poll(() => timeline._remoteEntries.array.length === 2 && timeline._contextEntriesNotInTimeline.get(entryA.id)); const bin = [entryB, entryC]; timeline.entries.subscribe({ onUpdate: (index) => { const i = bin.findIndex(e => e.id === index); bin.splice(i, 1); }, - onAdd: () => { } + onAdd: () => null, }); + timeline._getEventFromHomeserver = () => entryA; + timeline.addEntries([entryB, entryC]); + await poll(() => timeline._remoteEntries.array.length === 2 && timeline._contextEntriesNotInTimeline.get(entryA.id)); const redactingEntry = new EventEntry({ event: withRedacts(entryA.id, "foo", createEvent("m.room.redaction", "event_id_3", alice)) }); timeline.addEntries([redactingEntry]); assert.strictEqual(bin.length, 0); @@ -895,7 +882,7 @@ export function tests() { "context entries fetched from storage/hs are moved to remoteEntries": async assert => { const timeline = new Timeline({roomId, storage: await createMockStorage(), closeCallback: () => {}, fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock()}); - const entryA = new EventEntry({ event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)) }); + const entryA = new EventEntry({ event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)), eventIndex: 1 }); let event = withContent({ body: "bar", msgtype: "m.text", @@ -905,15 +892,12 @@ export function tests() { } } }, createEvent("m.room.message", "event_id_2", bob)); - const entryB = new EventEntry({ event }); - entryA._eventEntry.eventIndex = 1; - entryB._eventEntry.eventIndex = 2; + const entryB = new EventEntry({ event, eventIndex: 2 }); timeline._getEventFromHomeserver = () => entryA await timeline.load(new User(alice), "join", new NullLogItem()); - timeline._setupEntries([]); - timeline._localEntries.onSubscribeFirst(); + timeline.entries.subscribe({ onAdd: () => null, onUpdate: () => null }); timeline.addEntries([entryB]); - await timeline._loadContextEntriesWhereNeeded([entryB]); + await poll(() => entryB.contextEntry); assert.strictEqual(timeline._contextEntriesNotInTimeline.has(entryA.id), true); timeline.addEntries([entryA]); assert.strictEqual(timeline._contextEntriesNotInTimeline.has(entryA.id), false); From 2c4610c132012bc58fa539a2a904e519092465fa Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 13 Jan 2022 19:20:37 +0530 Subject: [PATCH 71/82] add param to emitUpdateForEntry --- src/matrix/room/timeline/Timeline.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index c1989a0d..5592ff88 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -141,7 +141,7 @@ export class Timeline { // redactingEntry might be a PendingEventEntry or an EventEntry, so don't assume pendingEvent const relatedTxnId = pee.redactingEntry.pendingEvent?.relatedTxnId; this._findAndUpdateEntryById(relatedTxnId, pee.redactingEntry.relatedEventId, updateOrFalse); - pee.redactingEntry.contextForEntries?.forEach(e => this._emitUpdateForEntry(e)); + pee.redactingEntry.contextForEntries?.forEach(e => this._emitUpdateForEntry(e, "contextEntry")); } } @@ -235,7 +235,7 @@ export class Timeline { this._contextEntriesNotInTimeline.set(entry.id, entry); } // Since this entry changed, all dependent entries should be updated - entry.contextForEntries?.forEach(e => this._emitUpdateForEntry(e)); + entry.contextForEntries?.forEach(e => this._emitUpdateForEntry(e, "contextEntry")); } catch (err) { if (err.name === "CompareError") { // see FragmentIdComparer, if the replacing entry is on a fragment @@ -295,17 +295,17 @@ export class Timeline { if (fetchedEntry) { fetchedEntry.contextForEntries.forEach(e => { e.setContextEntry(entry); - this._emitUpdateForEntry(e); + this._emitUpdateForEntry(e, "contextEntry"); }); this._contextEntriesNotInTimeline.delete(entry.id); } } } - _emitUpdateForEntry(entry) { + _emitUpdateForEntry(entry, param) { const txnId = entry.isPending ? entry.id : null; const eventId = entry.isPending ? null : entry.id; - this._findAndUpdateEntryById(txnId, eventId, () => true); + this._findAndUpdateEntryById(txnId, eventId, () => param); } /** @@ -333,7 +333,7 @@ export class Timeline { } if (contextEvent) { entry.setContextEntry(contextEvent); - this._emitUpdateForEntry(entry); + this._emitUpdateForEntry(entry, "contextEntry"); } } } From 3c28ee1adf9e4bfc2a88f1ccd47602d32dd362f8 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 13 Jan 2022 21:05:18 +0530 Subject: [PATCH 72/82] Remove unused getter --- src/matrix/room/timeline/entries/EventEntry.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/matrix/room/timeline/entries/EventEntry.js b/src/matrix/room/timeline/entries/EventEntry.js index 3d9c982b..7b957e01 100644 --- a/src/matrix/room/timeline/entries/EventEntry.js +++ b/src/matrix/room/timeline/entries/EventEntry.js @@ -124,13 +124,6 @@ export class EventEntry extends BaseEventEntry { return getRelatedEventId(this.event); } - get threadEventId() { - if (this.isThread) { - return this.relation?.event_id; - } - return null; - } - get isRedacted() { return super.isRedacted || isRedacted(this._eventEntry.event); } From 4fa32bac2fb87632c6dd580dafc5c96a3b42c1a2 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 14 Jan 2022 16:14:06 +0530 Subject: [PATCH 73/82] check only in remoteEntries --- src/matrix/room/timeline/Timeline.js | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 5592ff88..816ae3d1 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -344,7 +344,7 @@ export class Timeline { * @returns entry if found, undefined otherwise */ _findLoadedEventById(eventId) { - return this.getFromAllEntriesById(eventId) ?? this._contextEntriesNotInTimeline.get(eventId); + return this.getByEventId(eventId) ?? this._contextEntriesNotInTimeline.get(eventId); } async _getEventFromStorage(eventId) { @@ -423,19 +423,6 @@ export class Timeline { return null; } - getFromAllEntriesById(eventId) { - if (!this._allEntries) { - // if allEntries isn't loaded yet, fallback to looking only in remoteEntries - return this.getByEventId(eventId); - } - for (const entry of this._allEntries) { - if (entry.id === eventId) { - return entry; - } - } - return null; - } - /** @public */ get entries() { return this._allEntries; From b238357c5315d32ef448a8bf2f7ec41b2b817f73 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 14 Jan 2022 16:14:42 +0530 Subject: [PATCH 74/82] Use emitUpdateForEntry --- src/matrix/room/timeline/Timeline.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 816ae3d1..e1d87ee9 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -278,9 +278,7 @@ export class Timeline { const relatedEntry = this._contextEntriesNotInTimeline.get(entry.relatedEventId); if (relatedEntry?.isNonPersisted && relatedEntry?.addLocalRelation(entry)) { // update other entries for which this entry is a context entry - relatedEntry.contextForEntries?.forEach(e => { - this._remoteEntries.findAndUpdate(te => te.id === e.id, () => "contextEntry"); - }); + relatedEntry.contextForEntries?.forEach(e => this._emitUpdateForEntry(e, "contextEntry")); } } } From 277638b1078caa1553c622aec3fe76d54c518e40 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 14 Jan 2022 16:15:16 +0530 Subject: [PATCH 75/82] Override methods in NonPersistedEventEntry This will prevent redactions to entries fetched from hs showing "message is being redacted" and will instead show "message is redacted" --- .../room/timeline/entries/NonPersistedEventEntry.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/matrix/room/timeline/entries/NonPersistedEventEntry.js b/src/matrix/room/timeline/entries/NonPersistedEventEntry.js index cb8df8a3..cc3be88a 100644 --- a/src/matrix/room/timeline/entries/NonPersistedEventEntry.js +++ b/src/matrix/room/timeline/entries/NonPersistedEventEntry.js @@ -31,4 +31,14 @@ export class NonPersistedEventEntry extends EventEntry { get isNonPersisted() { return true; } + + // overridden here because we reuse addLocalRelation() for updating this entry + // we don't want the RedactedTile created using this entry to ever show "is being redacted" + get isRedacting() { + return false; + } + + get isRedacted() { + return !!this._pendingRedactions; + } } From 310790c84e62c9aae492710a9e8df20817e26b3a Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 14 Jan 2022 16:51:06 +0530 Subject: [PATCH 76/82] Use mock storage --- src/matrix/room/timeline/Timeline.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index e1d87ee9..aba57aab 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -742,9 +742,12 @@ export function tests() { }, "context entry is fetched from storage": async assert => { - const timeline = new Timeline({roomId, storage: await createMockStorage(), closeCallback: () => {}, + const storage = await createMockStorage(); + const txn = await storage.readWriteTxn([storage.storeNames.timelineEvents, storage.storeNames.timelineRelations]); + txn.timelineEvents.tryInsert({ event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)), fragmentId: 1, eventIndex: 1, roomId }); + await txn.complete(); + const timeline = new Timeline({roomId, storage, closeCallback: () => {}, fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock()}); - const entryA = new EventEntry({ event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)), eventIndex: 1 }); let event = withContent({ body: "bar", msgtype: "m.text", @@ -755,12 +758,11 @@ export function tests() { } }, createEvent("m.room.message", "event_id_2", bob)); const entryB = new EventEntry({ event, eventIndex: 2 }); - timeline._getEventFromStorage = () => entryA await timeline.load(new User(alice), "join", new NullLogItem()); timeline.entries.subscribe({ onAdd: () => null, onUpdate: () => null }); timeline.addEntries([entryB]); await poll(() => entryB.contextEntry); - assert.deepEqual(entryB.contextEntry, entryA); + assert.strictEqual(entryB.contextEntry.id, "event_id_1"); }, "context entry is fetched from hs": async assert => { From 315acf2fbc2a99eaaebdad07bf65d6a08ff5083e Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 14 Jan 2022 16:54:16 +0530 Subject: [PATCH 77/82] Remove dead code from test --- src/matrix/room/timeline/Timeline.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index aba57aab..ee0de044 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -604,8 +604,7 @@ export function tests() { // 1. setup timeline const pendingEvents = new ObservableArray(); const timeline = new Timeline({roomId, storage: await createMockStorage(), - closeCallback: () => { }, fragmentIdComparer, pendingEvents, clock: new MockClock(), - fetchEventFromStorage: () => undefined, fetchEventFromHomeserver: () => undefined}); + closeCallback: () => { }, fragmentIdComparer, pendingEvents, clock: new MockClock()}); await timeline.load(new User(bob), "join", new NullLogItem()); timeline.entries.subscribe(new ListObserver()); // 2. add message and reaction to timeline From e9a49fdf740d19817701eeb710dae239ab919fa7 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 14 Jan 2022 17:07:06 +0530 Subject: [PATCH 78/82] Use hsApi mock --- src/matrix/room/timeline/Timeline.js | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index ee0de044..69ab4eba 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -765,9 +765,24 @@ export function tests() { }, "context entry is fetched from hs": async assert => { + const hsApi = { + context() { + const result = { + event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)), + state: [{ + type: MEMBER_EVENT_TYPE, + user_id: alice, + content: { + displayName: "", + avatarUrl: "" + } + }] + }; + return { response: () => result }; + } + }; const timeline = new Timeline({roomId, storage: await createMockStorage(), closeCallback: () => {}, - fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock()}); - const entryA = new EventEntry({ event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)), eventIndex: 1 }); + fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock(), hsApi}); let event = withContent({ body: "bar", msgtype: "m.text", @@ -778,12 +793,11 @@ export function tests() { } }, createEvent("m.room.message", "event_id_2", bob)); const entryB = new EventEntry({ event, eventIndex: 2 }); - timeline._getEventFromHomeserver = () => entryA await timeline.load(new User(alice), "join", new NullLogItem()); timeline.entries.subscribe({ onAdd: () => null, onUpdate: () => null }); timeline.addEntries([entryB]); await poll(() => entryB.contextEntry); - assert.deepEqual(entryB.contextEntry, entryA); + assert.strictEqual(entryB.contextEntry.id, "event_id_1"); }, "context entry has a list of entries to which it forms the context": async assert => { From 75012eda9c217ccf61a1cc0b8cbc44889148cb36 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 14 Jan 2022 17:28:31 +0530 Subject: [PATCH 79/82] Fix tests --- src/matrix/room/timeline/Timeline.js | 54 +++++++++++++--------------- 1 file changed, 24 insertions(+), 30 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 69ab4eba..a26237e7 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -475,6 +475,22 @@ export function tests() { const roomId = "$abc"; const alice = "@alice:hs.tld"; const bob = "@bob:hs.tld"; + const hsApi = { + context() { + const result = { + event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)), + state: [{ + type: MEMBER_EVENT_TYPE, + user_id: alice, + content: { + displayName: "", + avatarUrl: "" + } + }] + }; + return { response: () => result }; + } + }; function getIndexFromIterable(it, n) { let i = 0; @@ -765,22 +781,6 @@ export function tests() { }, "context entry is fetched from hs": async assert => { - const hsApi = { - context() { - const result = { - event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)), - state: [{ - type: MEMBER_EVENT_TYPE, - user_id: alice, - content: { - displayName: "", - avatarUrl: "" - } - }] - }; - return { response: () => result }; - } - }; const timeline = new Timeline({roomId, storage: await createMockStorage(), closeCallback: () => {}, fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock(), hsApi}); let event = withContent({ @@ -824,8 +824,7 @@ export function tests() { "context entry in contextEntryNotInTimeline gets updated based on incoming redaction": async assert => { const timeline = new Timeline({roomId, storage: await createMockStorage(), closeCallback: () => {}, - fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock()}); - const entryA = new NonPersistedEventEntry({ event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)), eventIndex: 1 }); + fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock(), hsApi}); let event = withContent({ body: "bar", msgtype: "m.text", @@ -836,21 +835,18 @@ export function tests() { } }, createEvent("m.room.message", "event_id_2", bob)); const entryB = new EventEntry({ event, eventIndex: 2 }); - timeline._getEventFromHomeserver = () => entryA; await timeline.load(new User(alice), "join", new NullLogItem()); timeline.entries.subscribe({ onAdd: () => null, onUpdate: () => null }); timeline.addEntries([entryB]); await poll(() => entryB.contextEntry); - const redactingEntry = new EventEntry({ event: withRedacts(entryA.id, "foo", createEvent("m.room.redaction", "event_id_3", alice)), eventIndex: 3 }); + const redactingEntry = new EventEntry({ event: withRedacts("event_id_1", "foo", createEvent("m.room.redaction", "event_id_3", alice)), eventIndex: 3 }); timeline.addEntries([redactingEntry]); - const contextEntry = timeline._contextEntriesNotInTimeline.get(entryA.id); - assert.strictEqual(contextEntry.isRedacted, true); + assert.strictEqual(entryB.contextEntry.isRedacted, true); }, "redaction of context entry triggers updates in other entries": async assert => { const timeline = new Timeline({roomId, storage: await createMockStorage(), closeCallback: () => {}, - fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock()}); - const entryA = new NonPersistedEventEntry({ event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)), eventIndex: 1 }); + fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock(), hsApi}); const content = { body: "bar", msgtype: "m.text", @@ -871,17 +867,16 @@ export function tests() { }, onAdd: () => null, }); - timeline._getEventFromHomeserver = () => entryA; timeline.addEntries([entryB, entryC]); - await poll(() => timeline._remoteEntries.array.length === 2 && timeline._contextEntriesNotInTimeline.get(entryA.id)); - const redactingEntry = new EventEntry({ event: withRedacts(entryA.id, "foo", createEvent("m.room.redaction", "event_id_3", alice)) }); + await poll(() => timeline._remoteEntries.array.length === 2 && timeline._contextEntriesNotInTimeline.get("event_id_1")); + const redactingEntry = new EventEntry({ event: withRedacts("event_id_1", "foo", createEvent("m.room.redaction", "event_id_3", alice)) }); timeline.addEntries([redactingEntry]); assert.strictEqual(bin.length, 0); }, "context entries fetched from storage/hs are moved to remoteEntries": async assert => { const timeline = new Timeline({roomId, storage: await createMockStorage(), closeCallback: () => {}, - fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock()}); + fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock(), hsApi}); const entryA = new EventEntry({ event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)), eventIndex: 1 }); let event = withContent({ body: "bar", @@ -893,7 +888,6 @@ export function tests() { } }, createEvent("m.room.message", "event_id_2", bob)); const entryB = new EventEntry({ event, eventIndex: 2 }); - timeline._getEventFromHomeserver = () => entryA await timeline.load(new User(alice), "join", new NullLogItem()); timeline.entries.subscribe({ onAdd: () => null, onUpdate: () => null }); timeline.addEntries([entryB]); @@ -903,7 +897,7 @@ export function tests() { assert.strictEqual(timeline._contextEntriesNotInTimeline.has(entryA.id), false); const movedEntry = timeline.remoteEntries[0]; assert.deepEqual(movedEntry, entryA); - assert.deepEqual(movedEntry.contextForEntries.pop(), entryB); + assert.deepEqual(movedEntry.contextForEntries[0], entryB); assert.deepEqual(entryB.contextEntry, movedEntry); } }; From 8cd430ac0703a717e5bbe002a1c9d6ba73287ed8 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 14 Jan 2022 17:48:25 +0530 Subject: [PATCH 80/82] Improve test logic --- src/matrix/room/timeline/Timeline.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index a26237e7..9638f60c 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -859,11 +859,11 @@ export function tests() { const entryB = new EventEntry({ event: withContent(content, createEvent("m.room.message", "event_id_2", bob)), eventIndex: 2 }); const entryC = new EventEntry({ event: withContent(content, createEvent("m.room.message", "event_id_3", bob)), eventIndex: 3 }); await timeline.load(new User(alice), "join", new NullLogItem()); - const bin = [entryB, entryC]; + const bin = []; timeline.entries.subscribe({ onUpdate: (index) => { - const i = bin.findIndex(e => e.id === index); - bin.splice(i, 1); + const e = timeline.remoteEntries[index]; + bin.push(e.id); }, onAdd: () => null, }); @@ -871,7 +871,8 @@ export function tests() { await poll(() => timeline._remoteEntries.array.length === 2 && timeline._contextEntriesNotInTimeline.get("event_id_1")); const redactingEntry = new EventEntry({ event: withRedacts("event_id_1", "foo", createEvent("m.room.redaction", "event_id_3", alice)) }); timeline.addEntries([redactingEntry]); - assert.strictEqual(bin.length, 0); + assert.strictEqual(bin.includes("event_id_2"), true); + assert.strictEqual(bin.includes("event_id_3"), true); }, "context entries fetched from storage/hs are moved to remoteEntries": async assert => { From 30b8e5b5ea1da83bdbcdb79a1caf51fb0fd94b62 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 14 Jan 2022 18:15:26 +0530 Subject: [PATCH 81/82] use withReply --- src/matrix/room/timeline/Timeline.js | 83 ++++------------------------ src/mocks/event.js | 10 ++++ 2 files changed, 20 insertions(+), 73 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 9638f60c..e77e7d8e 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -463,7 +463,7 @@ import {poll} from "../../../mocks/poll.js"; import {Clock as MockClock} from "../../../mocks/Clock.js"; import {createMockStorage} from "../../../mocks/Storage"; import {ListObserver} from "../../../mocks/ListObserver.js"; -import {createEvent, withTextBody, withContent, withSender, withRedacts} from "../../../mocks/event.js"; +import {createEvent, withTextBody, withContent, withSender, withRedacts, withReply} from "../../../mocks/event.js"; import {NullLogItem} from "../../../logging/NullLogger"; import {EventEntry} from "./entries/EventEntry.js"; import {User} from "../../User.js"; @@ -740,16 +740,7 @@ export function tests() { const timeline = new Timeline({roomId, storage: await createMockStorage(), closeCallback: () => {}, fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock()}); const entryA = new EventEntry({ event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)) }); - let event = withContent({ - body: "bar", - msgtype: "m.text", - "m.relates_to": { - "m.in_reply_to": { - "event_id": "event_id_1" - } - } - }, createEvent("m.room.message", "event_id_2", bob)); - const entryB = new EventEntry({ event }); + const entryB = new EventEntry({ event: withReply("event_id_1", createEvent("m.room.message", "event_id_2", bob)), eventIndex: 2 }); await timeline.load(new User(alice), "join", new NullLogItem()); timeline.entries.subscribe({ onAdd: () => null, }); timeline.addEntries([entryA, entryB]); @@ -763,16 +754,7 @@ export function tests() { await txn.complete(); const timeline = new Timeline({roomId, storage, closeCallback: () => {}, fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock()}); - let event = withContent({ - body: "bar", - msgtype: "m.text", - "m.relates_to": { - "m.in_reply_to": { - "event_id": "event_id_1" - } - } - }, createEvent("m.room.message", "event_id_2", bob)); - const entryB = new EventEntry({ event, eventIndex: 2 }); + const entryB = new EventEntry({ event: withReply("event_id_1", createEvent("m.room.message", "event_id_2", bob)), eventIndex: 2 }); await timeline.load(new User(alice), "join", new NullLogItem()); timeline.entries.subscribe({ onAdd: () => null, onUpdate: () => null }); timeline.addEntries([entryB]); @@ -783,16 +765,7 @@ export function tests() { "context entry is fetched from hs": async assert => { const timeline = new Timeline({roomId, storage: await createMockStorage(), closeCallback: () => {}, fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock(), hsApi}); - let event = withContent({ - body: "bar", - msgtype: "m.text", - "m.relates_to": { - "m.in_reply_to": { - "event_id": "event_id_1" - } - } - }, createEvent("m.room.message", "event_id_2", bob)); - const entryB = new EventEntry({ event, eventIndex: 2 }); + const entryB = new EventEntry({ event: withReply("event_id_1", createEvent("m.room.message", "event_id_2", bob)), eventIndex: 2 }); await timeline.load(new User(alice), "join", new NullLogItem()); timeline.entries.subscribe({ onAdd: () => null, onUpdate: () => null }); timeline.addEntries([entryB]); @@ -804,17 +777,8 @@ export function tests() { const timeline = new Timeline({roomId, storage: await createMockStorage(), closeCallback: () => {}, fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock()}); const entryA = new EventEntry({ event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)), eventIndex: 1 }); - const content = { - body: "bar", - msgtype: "m.text", - "m.relates_to": { - "m.in_reply_to": { - "event_id": "event_id_1" - } - } - }; - const entryB = new EventEntry({ event: withContent(content, createEvent("m.room.message", "event_id_2", bob)), eventIndex: 2 }); - const entryC = new EventEntry({ event: withContent(content, createEvent("m.room.message", "event_id_3", bob)), eventIndex: 3 }); + const entryB = new EventEntry({ event: withReply("event_id_1", createEvent("m.room.message", "event_id_2", bob)), eventIndex: 2 }); + const entryC = new EventEntry({ event: withReply("event_id_1", createEvent("m.room.message", "event_id_3", bob)), eventIndex: 3 }); await timeline.load(new User(alice), "join", new NullLogItem()); timeline.entries.subscribe({ onAdd: () => null, onUpdate: () => null }); timeline.addEntries([entryA, entryB, entryC]); @@ -825,16 +789,7 @@ export function tests() { "context entry in contextEntryNotInTimeline gets updated based on incoming redaction": async assert => { const timeline = new Timeline({roomId, storage: await createMockStorage(), closeCallback: () => {}, fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock(), hsApi}); - let event = withContent({ - body: "bar", - msgtype: "m.text", - "m.relates_to": { - "m.in_reply_to": { - "event_id": "event_id_1" - } - } - }, createEvent("m.room.message", "event_id_2", bob)); - const entryB = new EventEntry({ event, eventIndex: 2 }); + const entryB = new EventEntry({ event: withReply("event_id_1", createEvent("m.room.message", "event_id_2", bob)), eventIndex: 2 }); await timeline.load(new User(alice), "join", new NullLogItem()); timeline.entries.subscribe({ onAdd: () => null, onUpdate: () => null }); timeline.addEntries([entryB]); @@ -847,17 +802,8 @@ export function tests() { "redaction of context entry triggers updates in other entries": async assert => { const timeline = new Timeline({roomId, storage: await createMockStorage(), closeCallback: () => {}, fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock(), hsApi}); - const content = { - body: "bar", - msgtype: "m.text", - "m.relates_to": { - "m.in_reply_to": { - "event_id": "event_id_1" - } - } - }; - const entryB = new EventEntry({ event: withContent(content, createEvent("m.room.message", "event_id_2", bob)), eventIndex: 2 }); - const entryC = new EventEntry({ event: withContent(content, createEvent("m.room.message", "event_id_3", bob)), eventIndex: 3 }); + const entryB = new EventEntry({ event: withReply("event_id_1", createEvent("m.room.message", "event_id_2", bob)), eventIndex: 2 }); + const entryC = new EventEntry({ event: withReply("event_id_1", createEvent("m.room.message", "event_id_3", bob)), eventIndex: 3 }); await timeline.load(new User(alice), "join", new NullLogItem()); const bin = []; timeline.entries.subscribe({ @@ -879,16 +825,7 @@ export function tests() { const timeline = new Timeline({roomId, storage: await createMockStorage(), closeCallback: () => {}, fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock(), hsApi}); const entryA = new EventEntry({ event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)), eventIndex: 1 }); - let event = withContent({ - body: "bar", - msgtype: "m.text", - "m.relates_to": { - "m.in_reply_to": { - "event_id": "event_id_1" - } - } - }, createEvent("m.room.message", "event_id_2", bob)); - const entryB = new EventEntry({ event, eventIndex: 2 }); + const entryB = new EventEntry({ event: withReply("event_id_1", createEvent("m.room.message", "event_id_2", bob)), eventIndex: 2 }); await timeline.load(new User(alice), "join", new NullLogItem()); timeline.entries.subscribe({ onAdd: () => null, onUpdate: () => null }); timeline.addEntries([entryB]); diff --git a/src/mocks/event.js b/src/mocks/event.js index a4a9e094..0900992a 100644 --- a/src/mocks/event.js +++ b/src/mocks/event.js @@ -37,3 +37,13 @@ export function withTxnId(txnId, event) { export function withRedacts(redacts, reason, event) { return Object.assign({redacts, content: {reason}}, event); } + +export function withReply(replyToId, event) { + return withContent({ + "m.relates_to": { + "m.in_reply_to": { + "event_id": replyToId + } + } + }, event); +} From 4fb0a84d0aa73d22be6f3ecee16519c0c064f011 Mon Sep 17 00:00:00 2001 From: R Midhun Suresh Date: Fri, 14 Jan 2022 18:16:38 +0530 Subject: [PATCH 82/82] Return property from super Co-authored-by: Bruno Windels --- src/matrix/room/timeline/entries/NonPersistedEventEntry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/room/timeline/entries/NonPersistedEventEntry.js b/src/matrix/room/timeline/entries/NonPersistedEventEntry.js index cc3be88a..01223f9e 100644 --- a/src/matrix/room/timeline/entries/NonPersistedEventEntry.js +++ b/src/matrix/room/timeline/entries/NonPersistedEventEntry.js @@ -39,6 +39,6 @@ export class NonPersistedEventEntry extends EventEntry { } get isRedacted() { - return !!this._pendingRedactions; + return super.isRedacting; } }