From 270be2df7a7a4ee63ab9e486f61ca41f8431341a Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 26 May 2021 16:35:49 +0100 Subject: [PATCH] Emit relations created when target event added later This changes the "relations created" event to ensure it is properly emitted even if the target event is added to the timeline after the relation event. There was perhaps always a risk of this happening in the past, but is seems more likely to bite now with delayed decryption. Part of https://github.com/vector-im/element-web/issues/17461 --- spec/unit/relations.spec.ts | 60 ++++++++++++++++++++++++++++++++ src/models/event-timeline-set.js | 20 +++-------- src/models/relations.js | 23 ++++++++++++ 3 files changed, 87 insertions(+), 16 deletions(-) diff --git a/spec/unit/relations.spec.ts b/spec/unit/relations.spec.ts index 0e561c856..3be34f332 100644 --- a/spec/unit/relations.spec.ts +++ b/spec/unit/relations.spec.ts @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { EventTimelineSet } from "../../src/models/event-timeline-set"; import { MatrixEvent } from "../../src/models/event"; import { Relations } from "../../src/models/relations"; @@ -70,4 +71,63 @@ describe("Relations", function() { expect(events.size).toEqual(1); } }); + + it("should emit created regardless of ordering", async function () { + const targetEvent = new MatrixEvent({ + "sender": "@bob:example.com", + "type": "m.room.message", + "event_id": "$2s4yYpEkVQrPglSCSqB_m6E8vDhWsg0yFNyOJdVIb_o", + "room_id": "!pzVjCQSoQPpXQeHpmK:example.com", + "content": {}, + }); + const relationEvent = new MatrixEvent({ + "sender": "@bob:example.com", + "type": "m.reaction", + "event_id": "$cZ1biX33ENJqIm00ks0W_hgiO_6CHrsAc3ZQrnLeNTw", + "room_id": "!pzVjCQSoQPpXQeHpmK:example.com", + "content": { + "m.relates_to": { + "event_id": "$2s4yYpEkVQrPglSCSqB_m6E8vDhWsg0yFNyOJdVIb_o", + "key": "👍️", + "rel_type": "m.annotation", + }, + }, + }); + + // Stub the room + const room = { + getPendingEvent() { return null; }, + getUnfilteredTimelineSet() { return null; }, + }; + + // Add the target event first, then the relation event + { + const relationsCreated = new Promise(resolve => { + targetEvent.once("Event.relationsCreated", resolve); + }) + + const timelineSet = new EventTimelineSet(room, { + unstableClientRelationAggregation: true, + }); + timelineSet.addLiveEvent(targetEvent); + timelineSet.addLiveEvent(relationEvent); + + await relationsCreated; + } + + // Add the relation event first, then the target event + { + const relationsCreated = new Promise(resolve => { + targetEvent.once("Event.relationsCreated", resolve); + }) + + const timelineSet = new EventTimelineSet(room, { + unstableClientRelationAggregation: true, + }); + timelineSet.addLiveEvent(relationEvent); + timelineSet.addLiveEvent(targetEvent); + + await relationsCreated; + } + }); }); diff --git a/src/models/event-timeline-set.js b/src/models/event-timeline-set.js index 784dea359..c24e3a79d 100644 --- a/src/models/event-timeline-set.js +++ b/src/models/event-timeline-set.js @@ -740,16 +740,11 @@ EventTimelineSet.prototype.setRelationsTarget = function(event) { if (!relationsForEvent) { return; } - // don't need it for non m.replace relations for now - const relationsWithRelType = relationsForEvent["m.replace"]; - if (!relationsWithRelType) { - return; - } - // only doing replacements for messages for now (e.g. edits) - const relationsWithEventType = relationsWithRelType["m.room.message"]; - if (relationsWithEventType) { - relationsWithEventType.setTargetEvent(event); + for (const relationsWithRelType of Object.values(relationsForEvent)) { + for (const relationsWithEventType of Object.values(relationsWithRelType)) { + relationsWithEventType.setTargetEvent(event); + } } }; @@ -797,7 +792,6 @@ EventTimelineSet.prototype.aggregateRelations = function(event) { } let relationsWithEventType = relationsWithRelType[eventType]; - let isNewRelations = false; let relatesToEvent; if (!relationsWithEventType) { relationsWithEventType = relationsWithRelType[eventType] = new Relations( @@ -805,7 +799,6 @@ EventTimelineSet.prototype.aggregateRelations = function(event) { eventType, this.room, ); - isNewRelations = true; relatesToEvent = this.findEventById(relatesToEventId) || this.room.getPendingEvent(relatesToEventId); if (relatesToEvent) { relationsWithEventType.setTargetEvent(relatesToEvent); @@ -813,11 +806,6 @@ EventTimelineSet.prototype.aggregateRelations = function(event) { } relationsWithEventType.addEvent(event); - - // only emit once event has been added to relations - if (isNewRelations && relatesToEvent) { - relatesToEvent.emit("Event.relationsCreated", relationType, eventType); - } }; /** diff --git a/src/models/relations.js b/src/models/relations.js index 6be559af8..3ea0f3bb2 100644 --- a/src/models/relations.js +++ b/src/models/relations.js @@ -48,6 +48,7 @@ export class Relations extends EventEmitter { this._sortedAnnotationsByKey = []; this._targetEvent = null; this._room = room; + this._creationEmitted = false; } /** @@ -94,6 +95,8 @@ export class Relations extends EventEmitter { event.on("Event.beforeRedaction", this._onBeforeRedaction); this.emit("Relations.add", event); + + this._maybeEmitCreated(); } /** @@ -345,6 +348,7 @@ export class Relations extends EventEmitter { return; } this._targetEvent = event; + if (this.relationType === "m.replace") { const replacement = await this.getLastReplacement(); // this is the initial update, so only call it if we already have something @@ -353,5 +357,24 @@ export class Relations extends EventEmitter { this._targetEvent.makeReplaced(replacement); } } + + this._maybeEmitCreated(); + } + + _maybeEmitCreated() { + if (this._creationEmitted) { + return; + } + // Only emit we're "created" once we have a target event instance _and_ + // at least one related event. + if (!this._targetEvent || !this._relations.size) { + return; + } + this._creationEmitted = true; + this._targetEvent.emit( + "Event.relationsCreated", + this.relationType, + this.eventType, + ); } }