1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-07-30 04:23:07 +03:00

Refactor Relations to not be per-EventTimelineSet (#2412)

* Refactor Relations to not be per-EventTimelineSet

* Fix comment and relations-container init

* Revert timing tweaks

* Fix relations order test

* Add test and simplify thread relations handling

* Fix order of initialising a room object

* Fix test

* Re-add thread handling for relations of unloaded threads

* Ditch confusing experimental getter `MatrixEvent::isThreadRelation`

* Fix room handling in RelationsContainer

* Iterate PR

* Tweak method naming to closer match spec
This commit is contained in:
Michael Telatynski
2022-06-07 11:16:53 +01:00
committed by GitHub
parent 07189f0637
commit bfed6edf41
11 changed files with 214 additions and 243 deletions

View File

@ -40,8 +40,8 @@ describe('EventTimelineSet', () => {
const itShouldReturnTheRelatedEvents = () => {
it('should return the related events', () => {
eventTimelineSet.aggregateRelations(messageEvent);
const relations = eventTimelineSet.getRelationsForEvent(
eventTimelineSet.relations.aggregateChildEvent(messageEvent);
const relations = eventTimelineSet.relations.getChildEventsForEvent(
messageEvent.getId(),
"m.in_reply_to",
EventType.RoomMessage,
@ -55,9 +55,7 @@ describe('EventTimelineSet', () => {
beforeEach(() => {
client = utils.mock(MatrixClient, 'MatrixClient');
room = new Room(roomId, client, userA);
eventTimelineSet = new EventTimelineSet(room, {
unstableClientRelationAggregation: true,
});
eventTimelineSet = new EventTimelineSet(room);
eventTimeline = new EventTimeline(eventTimelineSet);
messageEvent = utils.mkMessage({
room: roomId,
@ -189,8 +187,8 @@ describe('EventTimelineSet', () => {
});
it('should not return the related events', () => {
eventTimelineSet.aggregateRelations(messageEvent);
const relations = eventTimelineSet.getRelationsForEvent(
eventTimelineSet.relations.aggregateChildEvent(messageEvent);
const relations = eventTimelineSet.relations.getChildEventsForEvent(
messageEvent.getId(),
"m.in_reply_to",
EventType.RoomMessage,

View File

@ -96,19 +96,14 @@ describe("Relations", function() {
},
});
// Stub the room
const room = new Room("room123", null, null);
// Add the target event first, then the relation event
{
const room = new Room("room123", null, null);
const relationsCreated = new Promise(resolve => {
targetEvent.once(MatrixEventEvent.RelationsCreated, resolve);
});
const timelineSet = new EventTimelineSet(room, {
unstableClientRelationAggregation: true,
});
const timelineSet = new EventTimelineSet(room);
timelineSet.addLiveEvent(targetEvent);
timelineSet.addLiveEvent(relationEvent);
@ -117,13 +112,12 @@ describe("Relations", function() {
// Add the relation event first, then the target event
{
const room = new Room("room123", null, null);
const relationsCreated = new Promise(resolve => {
targetEvent.once(MatrixEventEvent.RelationsCreated, resolve);
});
const timelineSet = new EventTimelineSet(room, {
unstableClientRelationAggregation: true,
});
const timelineSet = new EventTimelineSet(room);
timelineSet.addLiveEvent(relationEvent);
timelineSet.addLiveEvent(targetEvent);
@ -131,6 +125,14 @@ describe("Relations", function() {
}
});
it("should re-use Relations between all timeline sets in a room", async () => {
const room = new Room("room123", null, null);
const timelineSet1 = new EventTimelineSet(room);
const timelineSet2 = new EventTimelineSet(room);
expect(room.relations).toBe(timelineSet1.relations);
expect(room.relations).toBe(timelineSet2.relations);
});
it("should ignore m.replace for state events", async () => {
const userId = "@bob:example.com";
const room = new Room("room123", null, userId);

View File

@ -2334,7 +2334,7 @@ describe("Room", function() {
const thread = threadRoot.getThread();
expect(thread.rootEvent).toBe(threadRoot);
const rootRelations = thread.timelineSet.getRelationsForEvent(
const rootRelations = thread.timelineSet.relations.getChildEventsForEvent(
threadRoot.getId(),
RelationType.Annotation,
EventType.Reaction,
@ -2344,7 +2344,7 @@ describe("Room", function() {
expect(rootRelations[0][1].size).toEqual(1);
expect(rootRelations[0][1].has(rootReaction)).toBeTruthy();
const responseRelations = thread.timelineSet.getRelationsForEvent(
const responseRelations = thread.timelineSet.relations.getChildEventsForEvent(
threadResponse.getId(),
RelationType.Annotation,
EventType.Reaction,

View File

@ -323,13 +323,6 @@ export interface ICreateClientOpts {
*/
sessionStore?: SessionStore;
/**
* Set to true to enable client-side aggregation of event relations
* via `EventTimelineSet#getRelationsForEvent`.
* This feature is currently unstable and the API may change without notice.
*/
unstableClientRelationAggregation?: boolean;
verificationMethods?: Array<VerificationMethod>;
/**
@ -903,7 +896,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
public clientRunning = false;
public timelineSupport = false;
public urlPreviewCache: { [key: string]: Promise<IPreviewUrlResponse> } = {};
public unstableClientRelationAggregation = false;
public identityServer: IIdentityServerProvider;
public sessionStore: SessionStore; // XXX: Intended private, used in code.
public http: MatrixHttpApi; // XXX: Intended private, used in code.
@ -1035,7 +1027,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
this.timelineSupport = Boolean(opts.timelineSupport);
this.unstableClientRelationAggregation = !!opts.unstableClientRelationAggregation;
this.cryptoStore = opts.cryptoStore;
this.sessionStore = opts.sessionStore;

View File

@ -19,14 +19,14 @@ limitations under the License.
*/
import { EventTimeline, IAddEventOptions } from "./event-timeline";
import { EventStatus, MatrixEvent, MatrixEventEvent } from "./event";
import { MatrixEvent } from "./event";
import { logger } from '../logger';
import { Relations } from './relations';
import { Room, RoomEvent } from "./room";
import { Filter } from "../filter";
import { EventType, RelationType } from "../@types/event";
import { RoomState } from "./room-state";
import { TypedEventEmitter } from "./typed-event-emitter";
import { RelationsContainer } from "./relations-container";
import { MatrixClient } from "../client";
const DEBUG = true;
@ -41,7 +41,6 @@ if (DEBUG) {
interface IOpts {
timelineSupport?: boolean;
filter?: Filter;
unstableClientRelationAggregation?: boolean;
pendingEvents?: boolean;
}
@ -81,14 +80,13 @@ export type EventTimelineSetHandlerMap = {
};
export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTimelineSetHandlerMap> {
public readonly relations?: RelationsContainer;
private readonly timelineSupport: boolean;
private unstableClientRelationAggregation: boolean;
private displayPendingEvents: boolean;
private readonly displayPendingEvents: boolean;
private liveTimeline: EventTimeline;
private timelines: EventTimeline[];
private _eventIdToTimeline: Record<string, EventTimeline>;
private filter?: Filter;
private relations: Record<string, Record<string, Record<RelationType, Relations>>>;
/**
* Construct a set of EventTimeline objects, typically on behalf of a given
@ -121,17 +119,18 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
* Set to true to enable improved timeline support.
* @param {Object} [opts.filter = null]
* The filter object, if any, for this timelineSet.
* @param {boolean} [opts.unstableClientRelationAggregation = false]
* Optional. Set to true to enable client-side aggregation of event relations
* via `getRelationsForEvent`.
* This feature is currently unstable and the API may change without notice.
* @param {MatrixClient} client the Matrix client which owns this EventTimelineSet,
* can be omitted if room is specified.
*/
constructor(public readonly room: Room, opts: IOpts) {
constructor(
public readonly room: Room | undefined,
opts: IOpts = {},
client?: MatrixClient,
) {
super();
this.timelineSupport = Boolean(opts.timelineSupport);
this.liveTimeline = new EventTimeline(this);
this.unstableClientRelationAggregation = !!opts.unstableClientRelationAggregation;
this.displayPendingEvents = opts.pendingEvents !== false;
// just a list - *not* ordered.
@ -140,11 +139,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
this.filter = opts.filter;
if (this.unstableClientRelationAggregation) {
// A tree of objects to access a set of relations for an event, as in:
// this.relations[relatesToEventId][relationType][relationEventType]
this.relations = {};
}
this.relations = this.room?.relations ?? new RelationsContainer(room?.client ?? client);
}
/**
@ -606,8 +601,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
const timeline = this._eventIdToTimeline[event.getId()];
if (timeline) {
if (duplicateStrategy === DuplicateStrategy.Replace) {
debuglog("EventTimelineSet.addLiveEvent: replacing duplicate event " +
event.getId());
debuglog("EventTimelineSet.addLiveEvent: replacing duplicate event " + event.getId());
const tlEvents = timeline.getEvents();
for (let j = 0; j < tlEvents.length; j++) {
if (tlEvents[j].getId() === event.getId()) {
@ -627,8 +621,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
}
}
} else {
debuglog("EventTimelineSet.addLiveEvent: ignoring duplicate event " +
event.getId());
debuglog("EventTimelineSet.addLiveEvent: ignoring duplicate event " + event.getId());
}
return;
}
@ -703,8 +696,8 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
});
this._eventIdToTimeline[eventId] = timeline;
this.setRelationsTarget(event);
this.aggregateRelations(event);
this.relations.aggregateParentEvent(event);
this.relations.aggregateChildEvent(event, this);
const data: IRoomTimelineData = {
timeline: timeline,
@ -804,8 +797,8 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
if (timeline1 === timeline2) {
// both events are in the same timeline - figure out their
// relative indices
let idx1;
let idx2;
let idx1: number;
let idx2: number;
const events = timeline1.getEvents();
for (let idx = 0; idx < events.length &&
(idx1 === undefined || idx2 === undefined); idx++) {
@ -846,145 +839,6 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
// the timelines are not contiguous.
return null;
}
/**
* Get a collection of relations to a given event in this timeline set.
*
* @param {String} eventId
* The ID of the event that you'd like to access relation events for.
* For example, with annotations, this would be the ID of the event being annotated.
* @param {String} relationType
* The type of relation involved, such as "m.annotation", "m.reference", "m.replace", etc.
* @param {String} eventType
* The relation event's type, such as "m.reaction", etc.
* @throws If <code>eventId</code>, <code>relationType</code> or <code>eventType</code>
* are not valid.
*
* @returns {?Relations}
* A container for relation events or undefined if there are no relation events for
* the relationType.
*/
public getRelationsForEvent(
eventId: string,
relationType: RelationType | string,
eventType: EventType | string,
): Relations | undefined {
if (!this.unstableClientRelationAggregation) {
throw new Error("Client-side relation aggregation is disabled");
}
if (!eventId || !relationType || !eventType) {
throw new Error("Invalid arguments for `getRelationsForEvent`");
}
// debuglog("Getting relations for: ", eventId, relationType, eventType);
const relationsForEvent = this.relations[eventId] || {};
const relationsWithRelType = relationsForEvent[relationType] || {};
return relationsWithRelType[eventType];
}
public getAllRelationsEventForEvent(eventId: string): MatrixEvent[] {
const relationsForEvent = this.relations?.[eventId] || {};
const events = [];
for (const relationsRecord of Object.values(relationsForEvent)) {
for (const relations of Object.values(relationsRecord)) {
events.push(...relations.getRelations());
}
}
return events;
}
/**
* Set an event as the target event if any Relations exist for it already
*
* @param {MatrixEvent} event
* The event to check as relation target.
*/
public setRelationsTarget(event: MatrixEvent): void {
if (!this.unstableClientRelationAggregation) {
return;
}
const relationsForEvent = this.relations[event.getId()];
if (!relationsForEvent) {
return;
}
for (const relationsWithRelType of Object.values(relationsForEvent)) {
for (const relationsWithEventType of Object.values(relationsWithRelType)) {
relationsWithEventType.setTargetEvent(event);
}
}
}
/**
* Add relation events to the relevant relation collection.
*
* @param {MatrixEvent} event
* The new relation event to be aggregated.
*/
public aggregateRelations(event: MatrixEvent): void {
if (!this.unstableClientRelationAggregation) {
return;
}
if (event.isRedacted() || event.status === EventStatus.CANCELLED) {
return;
}
const onEventDecrypted = (event: MatrixEvent) => {
if (event.isDecryptionFailure()) {
// This could for example happen if the encryption keys are not yet available.
// The event may still be decrypted later. Register the listener again.
event.once(MatrixEventEvent.Decrypted, onEventDecrypted);
return;
}
this.aggregateRelations(event);
};
// If the event is currently encrypted, wait until it has been decrypted.
if (event.isBeingDecrypted() || event.shouldAttemptDecryption()) {
event.once(MatrixEventEvent.Decrypted, onEventDecrypted);
return;
}
const relation = event.getRelation();
if (!relation) {
return;
}
const relatesToEventId = relation.event_id;
const relationType = relation.rel_type;
const eventType = event.getType();
// debuglog("Aggregating relation: ", event.getId(), eventType, relation);
let relationsForEvent: Record<string, Partial<Record<string, Relations>>> = this.relations[relatesToEventId];
if (!relationsForEvent) {
relationsForEvent = this.relations[relatesToEventId] = {};
}
let relationsWithRelType = relationsForEvent[relationType];
if (!relationsWithRelType) {
relationsWithRelType = relationsForEvent[relationType] = {};
}
let relationsWithEventType = relationsWithRelType[eventType];
if (!relationsWithEventType) {
relationsWithEventType = relationsWithRelType[eventType] = new Relations(
relationType,
eventType,
this.room,
);
const relatesToEvent = this.findEventById(relatesToEventId) || this.room.getPendingEvent(relatesToEventId);
if (relatesToEvent) {
relationsWithEventType.setTargetEvent(relatesToEvent);
}
}
relationsWithEventType.addEvent(event);
}
}
/**

View File

@ -514,13 +514,6 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
}
}
/**
* @experimental
*/
public get isThreadRelation(): boolean {
return !!this.threadRootId && this.threadId !== this.getId();
}
/**
* @experimental
*/

View File

@ -0,0 +1,155 @@
/*
Copyright 2022 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 { Relations } from "./relations";
import { EventType, RelationType } from "../@types/event";
import { EventStatus, MatrixEvent, MatrixEventEvent } from "./event";
import { EventTimelineSet } from "./event-timeline-set";
import { MatrixClient } from "../client";
import { Room } from "./room";
export class RelationsContainer {
// A tree of objects to access a set of related children for an event, as in:
// this.relations[parentEventId][relationType][relationEventType]
private relations: {
[parentEventId: string]: {
[relationType: RelationType | string]: {
[eventType: EventType | string]: Relations;
};
};
} = {};
constructor(private readonly client: MatrixClient, private readonly room?: Room) {
}
/**
* Get a collection of child events to a given event in this timeline set.
*
* @param {String} eventId
* The ID of the event that you'd like to access child events for.
* For example, with annotations, this would be the ID of the event being annotated.
* @param {String} relationType
* The type of relationship involved, such as "m.annotation", "m.reference", "m.replace", etc.
* @param {String} eventType
* The relation event's type, such as "m.reaction", etc.
* @throws If <code>eventId</code>, <code>relationType</code> or <code>eventType</code>
* are not valid.
*
* @returns {?Relations}
* A container for relation events or undefined if there are no relation events for
* the relationType.
*/
public getChildEventsForEvent(
eventId: string,
relationType: RelationType | string,
eventType: EventType | string,
): Relations | undefined {
return this.relations[eventId]?.[relationType]?.[eventType];
}
public getAllChildEventsForEvent(parentEventId: string): MatrixEvent[] {
const relationsForEvent = this.relations[parentEventId] ?? {};
const events: MatrixEvent[] = [];
for (const relationsRecord of Object.values(relationsForEvent)) {
for (const relations of Object.values(relationsRecord)) {
events.push(...relations.getRelations());
}
}
return events;
}
/**
* Set an event as the target event if any Relations exist for it already.
* Child events can point to other child events as their parent, so this method may be
* called for events which are also logically child events.
*
* @param {MatrixEvent} event The event to check as relation target.
*/
public aggregateParentEvent(event: MatrixEvent): void {
const relationsForEvent = this.relations[event.getId()];
if (!relationsForEvent) return;
for (const relationsWithRelType of Object.values(relationsForEvent)) {
for (const relationsWithEventType of Object.values(relationsWithRelType)) {
relationsWithEventType.setTargetEvent(event);
}
}
}
/**
* Add relation events to the relevant relation collection.
*
* @param {MatrixEvent} event The new child event to be aggregated.
* @param {EventTimelineSet} timelineSet The event timeline set within which to search for the related event if any.
*/
public aggregateChildEvent(event: MatrixEvent, timelineSet?: EventTimelineSet): void {
if (event.isRedacted() || event.status === EventStatus.CANCELLED) {
return;
}
const relation = event.getRelation();
if (!relation) return;
const onEventDecrypted = () => {
if (event.isDecryptionFailure()) {
// This could for example happen if the encryption keys are not yet available.
// The event may still be decrypted later. Register the listener again.
event.once(MatrixEventEvent.Decrypted, onEventDecrypted);
return;
}
this.aggregateChildEvent(event, timelineSet);
};
// If the event is currently encrypted, wait until it has been decrypted.
if (event.isBeingDecrypted() || event.shouldAttemptDecryption()) {
event.once(MatrixEventEvent.Decrypted, onEventDecrypted);
return;
}
const { event_id: relatesToEventId, rel_type: relationType } = relation;
const eventType = event.getType();
let relationsForEvent = this.relations[relatesToEventId];
if (!relationsForEvent) {
relationsForEvent = this.relations[relatesToEventId] = {};
}
let relationsWithRelType = relationsForEvent[relationType];
if (!relationsWithRelType) {
relationsWithRelType = relationsForEvent[relationType] = {};
}
let relationsWithEventType = relationsWithRelType[eventType];
if (!relationsWithEventType) {
relationsWithEventType = relationsWithRelType[eventType] = new Relations(
relationType,
eventType,
this.client,
);
const room = this.room ?? timelineSet?.room;
const relatesToEvent = timelineSet?.findEventById(relatesToEventId)
?? room?.findEventById(relatesToEventId)
?? room?.getPendingEvent(relatesToEventId);
if (relatesToEvent) {
relationsWithEventType.setTargetEvent(relatesToEvent);
}
}
relationsWithEventType.addEvent(event);
}
}

View File

@ -15,10 +15,11 @@ limitations under the License.
*/
import { EventStatus, IAggregatedRelation, MatrixEvent, MatrixEventEvent } from './event';
import { Room } from './room';
import { logger } from '../logger';
import { RelationType } from "../@types/event";
import { TypedEventEmitter } from "./typed-event-emitter";
import { MatrixClient } from "../client";
import { Room } from "./room";
export enum RelationsEvent {
Add = "Relations.add",
@ -48,6 +49,7 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
private sortedAnnotationsByKey: [string, Set<MatrixEvent>][] = [];
private targetEvent: MatrixEvent = null;
private creationEmitted = false;
private readonly client: MatrixClient;
/**
* @param {RelationType} relationType
@ -55,16 +57,16 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
* "m.replace", etc.
* @param {String} eventType
* The relation event's type, such as "m.reaction", etc.
* @param {?Room} room
* Room for this container. May be null for non-room cases, such as the
* notification timeline.
* @param {MatrixClient|Room} client
* The client which created this instance. For backwards compatibility also accepts a Room.
*/
constructor(
public readonly relationType: RelationType | string,
public readonly eventType: string,
private readonly room: Room,
client: MatrixClient | Room,
) {
super();
this.client = client instanceof Room ? client.client : client;
}
/**
@ -347,7 +349,7 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
}, null);
if (lastReplacement?.shouldAttemptDecryption()) {
await lastReplacement.attemptDecryption(this.room.client.crypto);
await lastReplacement.attemptDecryption(this.client.crypto);
} else if (lastReplacement?.isBeingDecrypted()) {
await lastReplacement.getDecryptionPromise();
}

View File

@ -49,6 +49,7 @@ import {
import { TypedEventEmitter } from "./typed-event-emitter";
import { ReceiptType } from "../@types/read_receipts";
import { IStateEventWithRoomId } from "../@types/search";
import { RelationsContainer } from "./relations-container";
// These constants are used as sane defaults when the homeserver doesn't support
// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be
@ -80,7 +81,6 @@ interface IOpts {
storageToken?: string;
pendingEventOrdering?: PendingEventOrdering;
timelineSupport?: boolean;
unstableClientRelationAggregation?: boolean;
lazyLoadMembers?: boolean;
}
@ -277,6 +277,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
* prefer getLiveTimeline().getState(EventTimeline.FORWARDS).
*/
public currentState: RoomState;
public readonly relations = new RelationsContainer(this.client, this);
/**
* @experimental
@ -338,10 +339,6 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
* "chronological".
* @param {boolean} [opts.timelineSupport = false] Set to true to enable improved
* timeline support.
* @param {boolean} [opts.unstableClientRelationAggregation = false]
* Optional. Set to true to enable client-side aggregation of event relations
* via `EventTimelineSet#getRelationsForEvent`.
* This feature is currently unstable and the API may change without notice.
*/
constructor(
public readonly roomId: string,
@ -1737,7 +1734,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
}
// A thread relation is always only shown in a thread
if (event.isThreadRelation) {
if (event.isRelation(THREAD_RELATION_TYPE.name)) {
return {
shouldLiveInRoom: false,
shouldLiveInThread: true,
@ -1816,8 +1813,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
toStartOfTimeline: boolean,
): Thread {
if (rootEvent) {
const tl = this.getTimelineForEvent(rootEvent.getId());
const relatedEvents = tl?.getTimelineSet().getAllRelationsEventForEvent(rootEvent.getId());
const relatedEvents = this.relations.getAllChildEventsForEvent(rootEvent.getId());
if (relatedEvents?.length) {
// Include all relations of the root event, given it'll be visible in both timelines,
// except `m.replace` as that will already be applied atop the event using `MatrixEvent::makeReplaced`
@ -2102,24 +2098,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
* @param {module:models/event.MatrixEvent} event the relation event that needs to be aggregated.
*/
private aggregateNonLiveRelation(event: MatrixEvent): void {
const { shouldLiveInRoom, threadId } = this.eventShouldLiveIn(event);
const thread = this.getThread(threadId);
thread?.timelineSet.aggregateRelations(event);
if (shouldLiveInRoom) {
// TODO: We should consider whether this means it would be a better
// design to lift the relations handling up to the room instead.
for (let i = 0; i < this.timelineSets.length; i++) {
const timelineSet = this.timelineSets[i];
if (timelineSet.getFilter()) {
if (timelineSet.getFilter().filterRoomTimeline([event]).length) {
timelineSet.aggregateRelations(event);
}
} else {
timelineSet.aggregateRelations(event);
}
}
}
this.relations.aggregateChildEvent(event);
}
public getEventForTxnId(txnId: string): MatrixEvent {
@ -2405,7 +2384,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
private findThreadRoots(events: MatrixEvent[]): Set<string> {
const threadRoots = new Set<string>();
for (const event of events) {
if (event.isThreadRelation) {
if (event.isRelation(THREAD_RELATION_TYPE.name)) {
threadRoots.add(event.relationEventId);
}
}

View File

@ -88,7 +88,6 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
this.room = opts.room;
this.client = opts.client;
this.timelineSet = new EventTimelineSet(this.room, {
unstableClientRelationAggregation: true,
timelineSupport: true,
pendingEvents: true,
});
@ -166,6 +165,7 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
private onEcho = (event: MatrixEvent) => {
if (event.threadRootId !== this.id) return; // ignore echoes for other timelines
if (this.lastEvent === event) return;
if (!event.isRelation(THREAD_RELATION_TYPE.name)) return;
// There is a risk that the `localTimestamp` approximation will not be accurate
// when threads are used over federation. That could result in the reply
@ -229,13 +229,6 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
this._currentUserParticipated = true;
}
if ([RelationType.Annotation, RelationType.Replace].includes(event.getRelation()?.rel_type as RelationType)) {
// Apply annotations and replace relations to the relations of the timeline only
this.timelineSet.setRelationsTarget(event);
this.timelineSet.aggregateRelations(event);
return;
}
// Add all incoming events to the thread's timeline set when there's no server support
if (!Thread.hasServerSideSupport) {
// all the relevant membership info to hydrate events with a sender
@ -251,6 +244,11 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
) {
this.fetchEditsWhereNeeded(event);
this.addEventToTimeline(event, false);
} else if (event.isRelation(RelationType.Annotation) || event.isRelation(RelationType.Replace)) {
// Apply annotations and replace relations to the relations of the timeline only
this.timelineSet.relations.aggregateParentEvent(event);
this.timelineSet.relations.aggregateChildEvent(event, this.timelineSet);
return;
}
// If no thread support exists we want to count all thread relation
@ -293,6 +291,7 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
// XXX: Workaround for https://github.com/matrix-org/matrix-spec-proposals/pull/2676/files#r827240084
private async fetchEditsWhereNeeded(...events: MatrixEvent[]): Promise<unknown> {
return Promise.all(events.filter(e => e.isEncrypted()).map((event: MatrixEvent) => {
if (event.isRelation()) return; // skip - relations don't get edits
return this.client.relations(this.roomId, event.getId(), RelationType.Replace, event.getType(), {
limit: 1,
}).then(relations => {

View File

@ -202,13 +202,11 @@ export class SyncApi {
const client = this.client;
const {
timelineSupport,
unstableClientRelationAggregation,
} = client;
const room = new Room(roomId, client, client.getUserId(), {
lazyLoadMembers: this.opts.lazyLoadMembers,
pendingEventOrdering: this.opts.pendingEventOrdering,
timelineSupport,
unstableClientRelationAggregation,
});
client.reEmitter.reEmit(room, [
RoomEvent.Name,