You've already forked matrix-js-sdk
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:
committed by
GitHub
parent
07189f0637
commit
bfed6edf41
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -514,13 +514,6 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
public get isThreadRelation(): boolean {
|
||||
return !!this.threadRootId && this.threadId !== this.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
|
155
src/models/relations-container.ts
Normal file
155
src/models/relations-container.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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 => {
|
||||
|
@ -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,
|
||||
|
Reference in New Issue
Block a user