1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-25 05:23:13 +03:00

Improve thread partitioning for 2nd degree relations (#2165)

This commit is contained in:
Germain
2022-02-10 15:09:46 +00:00
committed by GitHub
parent 47c5c4645e
commit 6b822ccd61
9 changed files with 190 additions and 228 deletions

View File

@@ -3,7 +3,6 @@ import { CRYPTO_ENABLED } from "../../src/client";
import { MatrixEvent } from "../../src/models/event"; import { MatrixEvent } from "../../src/models/event";
import { Filter, MemoryStore, Room } from "../../src/matrix"; import { Filter, MemoryStore, Room } from "../../src/matrix";
import { TestClient } from "../TestClient"; import { TestClient } from "../TestClient";
import { Thread } from "../../src/models/thread";
describe("MatrixClient", function() { describe("MatrixClient", function() {
let client = null; let client = null;
@@ -405,6 +404,11 @@ describe("MatrixClient", function() {
it("copies pre-thread in-timeline vote events onto both timelines", function() { it("copies pre-thread in-timeline vote events onto both timelines", function() {
client.clientOpts = { experimentalThreadSupport: true }; client.clientOpts = { experimentalThreadSupport: true };
const eventMessageInThread = buildEventMessageInThread();
const eventPollResponseReference = buildEventPollResponseReference();
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
const events = [ const events = [
eventMessageInThread, eventMessageInThread,
eventPollResponseReference, eventPollResponseReference,
@@ -435,6 +439,11 @@ describe("MatrixClient", function() {
it("copies pre-thread in-timeline reactions onto both timelines", function() { it("copies pre-thread in-timeline reactions onto both timelines", function() {
client.clientOpts = { experimentalThreadSupport: true }; client.clientOpts = { experimentalThreadSupport: true };
const eventMessageInThread = buildEventMessageInThread();
const eventReaction = buildEventReaction();
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
const events = [ const events = [
eventMessageInThread, eventMessageInThread,
eventReaction, eventReaction,
@@ -456,6 +465,11 @@ describe("MatrixClient", function() {
it("copies post-thread in-timeline vote events onto both timelines", function() { it("copies post-thread in-timeline vote events onto both timelines", function() {
client.clientOpts = { experimentalThreadSupport: true }; client.clientOpts = { experimentalThreadSupport: true };
const eventPollResponseReference = buildEventPollResponseReference();
const eventMessageInThread = buildEventMessageInThread();
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
const events = [ const events = [
eventPollResponseReference, eventPollResponseReference,
eventMessageInThread, eventMessageInThread,
@@ -475,119 +489,13 @@ describe("MatrixClient", function() {
]); ]);
}); });
it("copies post-thread in-thread vote events onto both timelines", function() {
client.clientOpts = { experimentalThreadSupport: true };
// Events for this test only, because we hack around with them
const eventMessageInThread2 = new MatrixEvent({
"age": 80098509,
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
"ciphertext": "ENCRYPTEDSTUFF",
"device_id": "XISFUZSKHH",
"m.relates_to": {
"event_id": "$AAA2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
"m.in_reply_to": {
"event_id": "$AAA2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
},
"rel_type": "io.element.thread",
},
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
},
"event_id": "$AAAhKIGYowtBblVLkRimeIg8TcdjETnxhDPGfi6NpDg",
"origin_server_ts": 1643815466378,
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
"sender": "@andybalaam-test1:matrix.org",
"type": "m.room.encrypted",
"unsigned": { "age": 80098509 },
"user_id": "@andybalaam-test1:matrix.org",
});
const eventPollStartThreadRoot2 = new MatrixEvent({
"age": 80108647,
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
"ciphertext": "ENCRYPTEDSTUFF",
"device_id": "XISFUZSKHH",
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
},
"event_id": "$AAA2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
"origin_server_ts": 1643815456240,
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
"sender": "@andybalaam-test1:matrix.org",
"type": "m.room.encrypted",
"unsigned": { "age": 80108647 },
"user_id": "@andybalaam-test1:matrix.org",
});
const eventPollResponseReference2 = new MatrixEvent({
"age": 80098509,
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
"ciphertext": "ENCRYPTEDSTUFF",
"device_id": "XISFUZSKHH",
"m.relates_to": {
"event_id": "$AAA2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
"rel_type": "m.reference",
},
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
},
"event_id": "$AAAvpezvsF0cKgav3g8W-uEVS4WkDHgxbJZvL3uMR1g",
"origin_server_ts": 1643815458650,
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
"sender": "@andybalaam-test1:matrix.org",
"type": "m.room.encrypted",
"unsigned": { "age": 80106237 },
"user_id": "@andybalaam-test1:matrix.org",
});
// When we react within a thread, sometimes the thread root
// has isThreadRelation === true, because thread is set on it,
// but threadId is not.
eventPollStartThreadRoot2.setThread(
new Thread(
eventPollStartThreadRoot2,
{
client,
room: new Room(),
},
),
);
const events = [
eventPollResponseReference2,
eventMessageInThread2,
eventPollStartThreadRoot2,
];
const [timeline, threaded] = client.partitionThreadedEvents(events);
expect(timeline).toEqual([
eventPollResponseReference2,
// eventPollStartThreadRoot2,
// This is weird: by hacking the thread root to have an inconsistency
// between thread and threadId (which is what I have observed in the
// wild), we have persuaded the code that the thread root is actually
// within the thread, so it is not provided to the main timeline.
//
// This should go away when we fix this inconsistency. When that
// happens, we should probably delete this test.
]);
expect(threaded).toEqual([
withThreadId(
eventPollResponseReference2, eventPollStartThreadRoot2.getId(),
),
eventMessageInThread2,
eventPollStartThreadRoot2, // See note above for why this appears here.
]);
});
it("copies post-thread in-timeline reactions onto both timelines", function() { it("copies post-thread in-timeline reactions onto both timelines", function() {
client.clientOpts = { experimentalThreadSupport: true }; client.clientOpts = { experimentalThreadSupport: true };
const eventReaction = buildEventReaction();
const eventMessageInThread = buildEventMessageInThread();
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
const events = [ const events = [
eventReaction, eventReaction,
eventMessageInThread, eventMessageInThread,
@@ -610,6 +518,19 @@ describe("MatrixClient", function() {
it("sends room state events to the main timeline only", function() { it("sends room state events to the main timeline only", function() {
client.clientOpts = { experimentalThreadSupport: true }; client.clientOpts = { experimentalThreadSupport: true };
// This is based on recording the events in a real room: // This is based on recording the events in a real room:
const eventMessageInThread = buildEventMessageInThread();
const eventPollResponseReference = buildEventPollResponseReference();
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
const eventRoomName = buildEventRoomName();
const eventEncryption = buildEventEncryption();
const eventGuestAccess = buildEventGuestAccess();
const eventHistoryVisibility = buildEventHistoryVisibility();
const eventJoinRules = buildEventJoinRules();
const eventPowerLevels = buildEventPowerLevels();
const eventMember = buildEventMember();
const eventCreate = buildEventCreate();
const events = [ const events = [
eventMessageInThread, eventMessageInThread,
eventPollResponseReference, eventPollResponseReference,
@@ -655,7 +576,7 @@ function withThreadId(event, newThreadId) {
return ret; return ret;
} }
const eventMessageInThread = new MatrixEvent({ const buildEventMessageInThread = () => new MatrixEvent({
"age": 80098509, "age": 80098509,
"content": { "content": {
"algorithm": "m.megolm.v1.aes-sha2", "algorithm": "m.megolm.v1.aes-sha2",
@@ -680,7 +601,7 @@ const eventMessageInThread = new MatrixEvent({
"user_id": "@andybalaam-test1:matrix.org", "user_id": "@andybalaam-test1:matrix.org",
}); });
const eventPollResponseReference = new MatrixEvent({ const buildEventPollResponseReference = () => new MatrixEvent({
"age": 80098509, "age": 80098509,
"content": { "content": {
"algorithm": "m.megolm.v1.aes-sha2", "algorithm": "m.megolm.v1.aes-sha2",
@@ -702,7 +623,7 @@ const eventPollResponseReference = new MatrixEvent({
"user_id": "@andybalaam-test1:matrix.org", "user_id": "@andybalaam-test1:matrix.org",
}); });
const eventReaction = new MatrixEvent({ const buildEventReaction = () => new MatrixEvent({
"content": { "content": {
"m.relates_to": { "m.relates_to": {
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo", "event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
@@ -721,7 +642,7 @@ const eventReaction = new MatrixEvent({
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org", "room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
}); });
const eventPollStartThreadRoot = new MatrixEvent({ const buildEventPollStartThreadRoot = () => new MatrixEvent({
"age": 80108647, "age": 80108647,
"content": { "content": {
"algorithm": "m.megolm.v1.aes-sha2", "algorithm": "m.megolm.v1.aes-sha2",
@@ -739,7 +660,7 @@ const eventPollStartThreadRoot = new MatrixEvent({
"user_id": "@andybalaam-test1:matrix.org", "user_id": "@andybalaam-test1:matrix.org",
}); });
const eventRoomName = new MatrixEvent({ const buildEventRoomName = () => new MatrixEvent({
"age": 80123249, "age": 80123249,
"content": { "content": {
"name": "1 poll, 1 vote, 1 thread", "name": "1 poll, 1 vote, 1 thread",
@@ -754,7 +675,7 @@ const eventRoomName = new MatrixEvent({
"user_id": "@andybalaam-test1:matrix.org", "user_id": "@andybalaam-test1:matrix.org",
}); });
const eventEncryption = new MatrixEvent({ const buildEventEncryption = () => new MatrixEvent({
"age": 80123383, "age": 80123383,
"content": { "content": {
"algorithm": "m.megolm.v1.aes-sha2", "algorithm": "m.megolm.v1.aes-sha2",
@@ -769,7 +690,7 @@ const eventEncryption = new MatrixEvent({
"user_id": "@andybalaam-test1:matrix.org", "user_id": "@andybalaam-test1:matrix.org",
}); });
const eventGuestAccess = new MatrixEvent({ const buildEventGuestAccess = () => new MatrixEvent({
"age": 80123473, "age": 80123473,
"content": { "content": {
"guest_access": "can_join", "guest_access": "can_join",
@@ -784,7 +705,7 @@ const eventGuestAccess = new MatrixEvent({
"user_id": "@andybalaam-test1:matrix.org", "user_id": "@andybalaam-test1:matrix.org",
}); });
const eventHistoryVisibility = new MatrixEvent({ const buildEventHistoryVisibility = () => new MatrixEvent({
"age": 80123556, "age": 80123556,
"content": { "content": {
"history_visibility": "shared", "history_visibility": "shared",
@@ -799,7 +720,7 @@ const eventHistoryVisibility = new MatrixEvent({
"user_id": "@andybalaam-test1:matrix.org", "user_id": "@andybalaam-test1:matrix.org",
}); });
const eventJoinRules = new MatrixEvent({ const buildEventJoinRules = () => new MatrixEvent({
"age": 80123696, "age": 80123696,
"content": { "content": {
"join_rule": "invite", "join_rule": "invite",
@@ -814,7 +735,7 @@ const eventJoinRules = new MatrixEvent({
"user_id": "@andybalaam-test1:matrix.org", "user_id": "@andybalaam-test1:matrix.org",
}); });
const eventPowerLevels = new MatrixEvent({ const buildEventPowerLevels = () => new MatrixEvent({
"age": 80124105, "age": 80124105,
"content": { "content": {
"ban": 50, "ban": 50,
@@ -849,7 +770,7 @@ const eventPowerLevels = new MatrixEvent({
"user_id": "@andybalaam-test1:matrix.org", "user_id": "@andybalaam-test1:matrix.org",
}); });
const eventMember = new MatrixEvent({ const buildEventMember = () => new MatrixEvent({
"age": 80125279, "age": 80125279,
"content": { "content": {
"avatar_url": "mxc://matrix.org/aNtbVcFfwotudypZcHsIcPOc", "avatar_url": "mxc://matrix.org/aNtbVcFfwotudypZcHsIcPOc",
@@ -866,7 +787,7 @@ const eventMember = new MatrixEvent({
"user_id": "@andybalaam-test1:matrix.org", "user_id": "@andybalaam-test1:matrix.org",
}); });
const eventCreate = new MatrixEvent({ const buildEventCreate = () => new MatrixEvent({
"age": 80126105, "age": 80126105,
"content": { "content": {
"creator": "@andybalaam-test1:matrix.org", "creator": "@andybalaam-test1:matrix.org",

View File

@@ -92,6 +92,7 @@ export enum EventType {
export enum RelationType { export enum RelationType {
Annotation = "m.annotation", Annotation = "m.annotation",
Replace = "m.replace", Replace = "m.replace",
Reference = "m.reference",
/** /**
* Note, "io.element.thread" is hardcoded * Note, "io.element.thread" is hardcoded
* Should be replaced with "m.thread" once MSC3440 lands * Should be replaced with "m.thread" once MSC3440 lands

View File

@@ -3594,17 +3594,20 @@ export class MatrixClient extends EventEmitter {
threadId = null; threadId = null;
} }
if (threadId && content["m.relates_to"]?.rel_type !== RelationType.Thread) { // If we expect that an event is part of a thread but is missing the relation
// we need to add it manually, as well as the reply fallback
if (threadId && !content["m.relates_to"]?.rel_type) {
content["m.relates_to"] = { content["m.relates_to"] = {
...content["m.relates_to"], ...content["m.relates_to"],
"rel_type": RelationType.Thread, "rel_type": RelationType.Thread,
"event_id": threadId, "event_id": threadId,
}; };
const thread = this.getRoom(roomId)?.threads.get(threadId); const thread = this.getRoom(roomId)?.threads.get(threadId);
if (thread) { if (thread) {
content["m.relates_to"]["m.in_reply_to"] = { content["m.relates_to"]["m.in_reply_to"] = {
"event_id": thread.replyToEvent.getId(), "event_id": thread.lastReply((ev: MatrixEvent) => {
return ev.isThreadRelation && !ev.status;
}),
}; };
} }
} }
@@ -3652,6 +3655,7 @@ export class MatrixClient extends EventEmitter {
const thread = room?.threads.get(threadId); const thread = room?.threads.get(threadId);
if (thread) { if (thread) {
localEvent.setThread(thread); localEvent.setThread(thread);
localEvent.setThreadId(thread.id);
} }
// if this is a relation or redaction of an event // if this is a relation or redaction of an event
@@ -9089,6 +9093,52 @@ export class MatrixClient extends EventEmitter {
return threadRoots; return threadRoots;
} }
private eventShouldLiveIn(event: MatrixEvent, room: Room, events: MatrixEvent[], roots: Set<string>): {
shouldLiveInRoom: boolean;
shouldLiveInThread: boolean;
threadId?: string;
} {
// A thread relation is always only shown in a thread
if (event.isThreadRelation) {
return {
shouldLiveInRoom: false,
shouldLiveInThread: true,
threadId: event.relationEventId,
};
}
const parentEventId = event.getAssociatedId();
const parentEvent = room?.findEventById(parentEventId) || events.find((mxEv: MatrixEvent) => (
mxEv.getId() === parentEventId
));
// A reaction targetting the thread root needs to be routed to both the
// the main timeline and the associated thread
const targetingThreadRoot = parentEvent?.isThreadRoot || roots.has(event.relationEventId);
if (targetingThreadRoot) {
return {
shouldLiveInRoom: true,
shouldLiveInThread: true,
threadId: event.relationEventId,
};
}
// If the parent event also has an associated ID we want to re-run the
// computation for that parent event.
// In the case of the redaction of a reaction that targets a root event
// we want that redaction to be pushed to both timeline
if (parentEvent?.getAssociatedId()) {
return this.eventShouldLiveIn(parentEvent, room, events, roots);
} else {
// We've exhausted all scenarios, can safely assume that this event
// should live in the room timeline
return {
shouldLiveInRoom: true,
shouldLiveInThread: false,
};
}
}
public partitionThreadedEvents(events: MatrixEvent[]): [MatrixEvent[], MatrixEvent[]] { public partitionThreadedEvents(events: MatrixEvent[]): [MatrixEvent[], MatrixEvent[]] {
// Indices to the events array, for readibility // Indices to the events array, for readibility
const ROOM = 0; const ROOM = 0;
@@ -9097,35 +9147,22 @@ export class MatrixClient extends EventEmitter {
const threadRoots = this.findThreadRoots(events); const threadRoots = this.findThreadRoots(events);
return events.reduce((memo, event: MatrixEvent) => { return events.reduce((memo, event: MatrixEvent) => {
const room = this.getRoom(event.getRoomId()); const room = this.getRoom(event.getRoomId());
// An event should live in the thread timeline if
// - It's a reply in thread event
// - It's related to a reply in thread event
let shouldLiveInThreadTimeline = event.isThreadRelation;
if (!shouldLiveInThreadTimeline) {
const parentEventId = event.parentEventId;
const parentEvent = room?.findEventById(parentEventId) || events.find((mxEv: MatrixEvent) => {
return mxEv.getId() === parentEventId;
});
const targetingThreadRoot = parentEvent?.isThreadRoot || threadRoots.has(event.relationEventId);
if (targetingThreadRoot && !event.isThreadRelation && event.relationEventId) { const {
// If we refer to the thread root, we should be copied shouldLiveInRoom,
// into the thread as well as the main timeline. shouldLiveInThread,
// This happens for reactions, annotations, poll votes etc. threadId,
const copiedEvent = event.toSnapshot(); } = this.eventShouldLiveIn(event, room, events, threadRoots);
// The copied event is in this thread: if (shouldLiveInRoom) {
copiedEvent.setThreadId(parentEventId); memo[ROOM].push(event);
memo[THREAD].push(copiedEvent);
} else if (parentEvent?.isThreadRelation) {
// If our parent is in a thread, we are in that
// same thread too. (E.g. if I reply within a thread.)
shouldLiveInThreadTimeline = true;
event.setThreadId(parentEvent.threadRootId);
}
} }
const targetTimeline = shouldLiveInThreadTimeline ? THREAD : ROOM;
memo[targetTimeline].push(event); if (shouldLiveInThread) {
event.setThreadId(threadId);
memo[THREAD].push(event);
}
return memo; return memo;
}, [[], []]); }, [[], []]);
} else { } else {

View File

@@ -28,7 +28,6 @@ import { Room } from "./room";
import { Filter } from "../filter"; import { Filter } from "../filter";
import { EventType, RelationType } from "../@types/event"; import { EventType, RelationType } from "../@types/event";
import { RoomState } from "./room-state"; import { RoomState } from "./room-state";
import { Thread } from "./thread";
// var DEBUG = false; // var DEBUG = false;
const DEBUG = true; const DEBUG = true;
@@ -159,17 +158,12 @@ export class EventTimelineSet extends EventEmitter {
* *
* @throws If <code>opts.pendingEventOrdering</code> was not 'detached' * @throws If <code>opts.pendingEventOrdering</code> was not 'detached'
*/ */
public getPendingEvents(thread?: Thread): MatrixEvent[] { public getPendingEvents(): MatrixEvent[] {
if (!this.room || !this.displayPendingEvents) { if (!this.room || !this.displayPendingEvents) {
return []; return [];
} }
const pendingEvents = this.room.getPendingEvents(thread); return this.room.getPendingEvents();
if (this.filter) {
return this.filter.filterRoomTimeline(pendingEvents);
} else {
return pendingEvents;
}
} }
/** /**
* Get the live timeline for this room. * Get the live timeline for this room.
@@ -756,7 +750,7 @@ export class EventTimelineSet extends EventEmitter {
*/ */
public getRelationsForEvent( public getRelationsForEvent(
eventId: string, eventId: string,
relationType: RelationType, relationType: RelationType | string,
eventType: EventType | string, eventType: EventType | string,
): Relations | undefined { ): Relations | undefined {
if (!this.unstableClientRelationAggregation) { if (!this.unstableClientRelationAggregation) {
@@ -774,6 +768,17 @@ export class EventTimelineSet extends EventEmitter {
return relationsWithRelType[eventType]; 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 * Set an event as the target event if any Relations exist for it already
* *

View File

@@ -512,8 +512,7 @@ export class MatrixEvent extends EventEmitter {
if (relatesTo?.rel_type === RelationType.Thread) { if (relatesTo?.rel_type === RelationType.Thread) {
return relatesTo.event_id; return relatesTo.event_id;
} else { } else {
return this.threadId return this.getThread()?.id || this.threadId;
|| this.getThread()?.id;
} }
} }
@@ -537,10 +536,6 @@ export class MatrixEvent extends EventEmitter {
return !!threadDetails || (this.getThread()?.id === this.getId()); return !!threadDetails || (this.getThread()?.id === this.getId());
} }
public get parentEventId(): string {
return this.replyEventId || this.relationEventId;
}
public get replyEventId(): string { public get replyEventId(): string {
// We're prefer ev.getContent() over ev.getWireContent() to make sure // We're prefer ev.getContent() over ev.getWireContent() to make sure
// we grab the latest edit with potentially new relations. But we also // we grab the latest edit with potentially new relations. But we also
@@ -1427,7 +1422,9 @@ export class MatrixEvent extends EventEmitter {
*/ */
public getAssociatedId(): string | undefined { public getAssociatedId(): string | undefined {
const relation = this.getRelation(); const relation = this.getRelation();
if (relation) { if (this.replyEventId) {
return this.replyEventId;
} else if (relation) {
return relation.event_id; return relation.event_id;
} else if (this.isRedaction()) { } else if (this.isRedaction()) {
return this.event.redacts; return this.event.redacts;
@@ -1561,6 +1558,7 @@ export class MatrixEvent extends EventEmitter {
*/ */
public setThread(thread: Thread): void { public setThread(thread: Thread): void {
this.thread = thread; this.thread = thread;
this.setThreadId(thread.id);
this.reEmitter.reEmit(thread, [ThreadEvent.Ready, ThreadEvent.Update]); this.reEmitter.reEmit(thread, [ThreadEvent.Ready, ThreadEvent.Update]);
} }

View File

@@ -171,11 +171,11 @@ export class Relations extends EventEmitter {
* @return {Array} * @return {Array}
* Relation events in insertion order. * Relation events in insertion order.
*/ */
public getRelations() { public getRelations(): MatrixEvent[] {
return [...this.relations]; return [...this.relations];
} }
private addAnnotationToAggregation(event: MatrixEvent) { private addAnnotationToAggregation(event: MatrixEvent): void {
const { key } = event.getRelation(); const { key } = event.getRelation();
if (!key) { if (!key) {
return; return;
@@ -204,7 +204,7 @@ export class Relations extends EventEmitter {
eventsFromSender.add(event); eventsFromSender.add(event);
} }
private removeAnnotationFromAggregation(event: MatrixEvent) { private removeAnnotationFromAggregation(event: MatrixEvent): void {
const { key } = event.getRelation(); const { key } = event.getRelation();
if (!key) { if (!key) {
return; return;
@@ -240,7 +240,7 @@ export class Relations extends EventEmitter {
* @param {MatrixEvent} redactedEvent * @param {MatrixEvent} redactedEvent
* The original relation event that is about to be redacted. * The original relation event that is about to be redacted.
*/ */
private onBeforeRedaction = async (redactedEvent: MatrixEvent) => { private onBeforeRedaction = async (redactedEvent: MatrixEvent): Promise<void> => {
if (!this.relations.has(redactedEvent)) { if (!this.relations.has(redactedEvent)) {
return; return;
} }

View File

@@ -512,16 +512,14 @@ export class Room extends EventEmitter {
* *
* @throws If <code>opts.pendingEventOrdering</code> was not 'detached' * @throws If <code>opts.pendingEventOrdering</code> was not 'detached'
*/ */
public getPendingEvents(thread?: Thread): MatrixEvent[] { public getPendingEvents(): MatrixEvent[] {
if (this.opts.pendingEventOrdering !== PendingEventOrdering.Detached) { if (this.opts.pendingEventOrdering !== PendingEventOrdering.Detached) {
throw new Error( throw new Error(
"Cannot call getPendingEvents with pendingEventOrdering == " + "Cannot call getPendingEvents with pendingEventOrdering == " +
this.opts.pendingEventOrdering); this.opts.pendingEventOrdering);
} }
return this.pendingEventList.filter(event => { return this.pendingEventList;
return !thread || thread.id === event.threadRootId;
});
} }
/** /**
@@ -1358,7 +1356,7 @@ export class Room extends EventEmitter {
} else if (event.isThreadRoot) { } else if (event.isThreadRoot) {
return this.threads.get(event.getId()); return this.threads.get(event.getId());
} else { } else {
const parentEvent = this.findEventById(event.parentEventId); const parentEvent = this.findEventById(event.getAssociatedId());
return this.findThreadForEvent(parentEvent); return this.findThreadForEvent(parentEvent);
} }
} }
@@ -1396,21 +1394,15 @@ export class Room extends EventEmitter {
} }
} }
if (event.getUnsigned().transaction_id) {
const existingEvent = this.txnToEvent[event.getUnsigned().transaction_id];
if (existingEvent) {
// remote echo of an event we sent earlier
this.handleRemoteEcho(event, existingEvent);
return;
}
}
this.emit(ThreadEvent.Update, thread); this.emit(ThreadEvent.Update, thread);
} }
public createThread(rootEvent: MatrixEvent, events?: MatrixEvent[]): Thread | undefined { public createThread(rootEvent: MatrixEvent, events: MatrixEvent[] = []): Thread | undefined {
const tl = this.getTimelineForEvent(rootEvent.getId());
const relatedEvents = tl?.getTimelineSet().getAllRelationsEventForEvent(rootEvent.getId()) ?? [];
const thread = new Thread(rootEvent, { const thread = new Thread(rootEvent, {
initialEvents: events, initialEvents: events.concat(relatedEvents),
room: this, room: this,
client: this.client, client: this.client,
}); });
@@ -1564,8 +1556,7 @@ export class Room extends EventEmitter {
EventTimeline.setEventMetadata(event, this.getLiveTimeline().getState(EventTimeline.FORWARDS), false); EventTimeline.setEventMetadata(event, this.getLiveTimeline().getState(EventTimeline.FORWARDS), false);
this.txnToEvent[txnId] = event; this.txnToEvent[txnId] = event;
const thread = this.findThreadForEvent(event); if (this.opts.pendingEventOrdering === PendingEventOrdering.Detached) {
if (this.opts.pendingEventOrdering === PendingEventOrdering.Detached && !thread) {
if (this.pendingEventList.some((e) => e.status === EventStatus.NOT_SENT)) { if (this.pendingEventList.some((e) => e.status === EventStatus.NOT_SENT)) {
logger.warn("Setting event as NOT_SENT due to messages in the same state"); logger.warn("Setting event as NOT_SENT due to messages in the same state");
event.setStatus(EventStatus.NOT_SENT); event.setStatus(EventStatus.NOT_SENT);
@@ -1581,8 +1572,7 @@ export class Room extends EventEmitter {
if (event.isRedaction()) { if (event.isRedaction()) {
const redactId = event.event.redacts; const redactId = event.event.redacts;
let redactedEvent = this.pendingEventList && let redactedEvent = this.pendingEventList?.find(e => e.getId() === redactId);
this.pendingEventList.find(e => e.getId() === redactId);
if (!redactedEvent) { if (!redactedEvent) {
redactedEvent = this.findEventById(redactId); redactedEvent = this.findEventById(redactId);
} }
@@ -1592,20 +1582,16 @@ export class Room extends EventEmitter {
} }
} }
} else { } else {
if (thread) { for (let i = 0; i < this.timelineSets.length; i++) {
thread.addEvent(event, false); const timelineSet = this.timelineSets[i];
} else { if (timelineSet.getFilter()) {
for (let i = 0; i < this.timelineSets.length; i++) { if (timelineSet.getFilter().filterRoomTimeline([event]).length) {
const timelineSet = this.timelineSets[i];
if (timelineSet.getFilter()) {
if (timelineSet.getFilter().filterRoomTimeline([event]).length) {
timelineSet.addEventToTimeline(event,
timelineSet.getLiveTimeline(), false);
}
} else {
timelineSet.addEventToTimeline(event, timelineSet.addEventToTimeline(event,
timelineSet.getLiveTimeline(), false); timelineSet.getLiveTimeline(), false);
} }
} else {
timelineSet.addEventToTimeline(event,
timelineSet.getLiveTimeline(), false);
} }
} }
} }
@@ -1666,7 +1652,9 @@ export class Room extends EventEmitter {
const thread = this.findThreadForEvent(event); const thread = this.findThreadForEvent(event);
if (thread) { if (thread) {
thread.timelineSet.aggregateRelations(event); thread.timelineSet.aggregateRelations(event);
} else { }
if (thread?.id === event.getAssociatedId() || !thread) {
// TODO: We should consider whether this means it would be a better // TODO: We should consider whether this means it would be a better
// design to lift the relations handling up to the room instead. // design to lift the relations handling up to the room instead.
for (let i = 0; i < this.timelineSets.length; i++) { for (let i = 0; i < this.timelineSets.length; i++) {
@@ -1682,6 +1670,10 @@ export class Room extends EventEmitter {
} }
} }
public getEventForTxnId(txnId: string): MatrixEvent {
return this.txnToEvent[txnId];
}
/** /**
* Deal with the echo of a message we sent. * Deal with the echo of a message we sent.
* *
@@ -1696,7 +1688,7 @@ export class Room extends EventEmitter {
* @fires module:client~MatrixClient#event:"Room.localEchoUpdated" * @fires module:client~MatrixClient#event:"Room.localEchoUpdated"
* @private * @private
*/ */
private handleRemoteEcho(remoteEvent: MatrixEvent, localEvent: MatrixEvent): void { public handleRemoteEcho(remoteEvent: MatrixEvent, localEvent: MatrixEvent): void {
const oldEventId = localEvent.getId(); const oldEventId = localEvent.getId();
const newEventId = remoteEvent.getId(); const newEventId = remoteEvent.getId();
const oldStatus = localEvent.status; const oldStatus = localEvent.status;
@@ -1721,7 +1713,9 @@ export class Room extends EventEmitter {
const thread = this.findThreadForEvent(remoteEvent); const thread = this.findThreadForEvent(remoteEvent);
if (thread) { if (thread) {
thread.timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId); thread.timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId);
} else { }
if (thread?.id === remoteEvent.getAssociatedId() || !thread) {
for (let i = 0; i < this.timelineSets.length; i++) { for (let i = 0; i < this.timelineSets.length; i++) {
const timelineSet = this.timelineSets[i]; const timelineSet = this.timelineSets[i];
@@ -1791,7 +1785,8 @@ export class Room extends EventEmitter {
const thread = this.findThreadForEvent(event); const thread = this.findThreadForEvent(event);
if (thread) { if (thread) {
thread.timelineSet.replaceEventId(oldEventId, newEventId); thread.timelineSet.replaceEventId(oldEventId, newEventId);
} else { }
if (thread?.id === event.getAssociatedId() || !thread) {
// if the event was already in the timeline (which will be the case if // if the event was already in the timeline (which will be the case if
// opts.pendingEventOrdering==chronological), we need to update the // opts.pendingEventOrdering==chronological), we need to update the
// timeline map. // timeline map.

View File

@@ -113,6 +113,27 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
return this.room.getLiveTimeline().getState(EventTimeline.FORWARDS); return this.room.getLiveTimeline().getState(EventTimeline.FORWARDS);
} }
private addEventToTimeline(event: MatrixEvent, toStartOfTimeline: boolean): void {
if (event.getUnsigned().transaction_id) {
const existingEvent = this.room.getEventForTxnId(event.getUnsigned().transaction_id);
if (existingEvent) {
// remote echo of an event we sent earlier
this.room.handleRemoteEcho(event, existingEvent);
return;
}
}
if (!this.findEventById(event.getId())) {
this.timelineSet.addEventToTimeline(
event,
this.liveTimeline,
toStartOfTimeline,
false,
this.roomState,
);
}
}
/** /**
* Add an event to the thread and updates * Add an event to the thread and updates
* the tail/root references if needed * the tail/root references if needed
@@ -123,36 +144,20 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
// Add all incoming events to the thread's timeline set when there's // Add all incoming events to the thread's timeline set when there's
// no server support // no server support
if (!this.hasServerSideSupport) { if (!this.hasServerSideSupport) {
if (this.timelineSet.findEventById(event.getId())) {
return;
}
// all the relevant membership info to hydrate events with a sender // all the relevant membership info to hydrate events with a sender
// is held in the main room timeline // is held in the main room timeline
// We want to fetch the room state from there and pass it down to this thread // We want to fetch the room state from there and pass it down to this thread
// timeline set to let it reconcile an event with its relevant RoomMember // timeline set to let it reconcile an event with its relevant RoomMember
event.setThread(this); event.setThread(this);
this.timelineSet.addEventToTimeline( this.addEventToTimeline(event, toStartOfTimeline);
event,
this.liveTimeline,
toStartOfTimeline,
false,
this.roomState,
);
await this.client.decryptEventIfNeeded(event, {}); await this.client.decryptEventIfNeeded(event, {});
} }
if (this.hasServerSideSupport && this.initialEventsFetched) { if (this.hasServerSideSupport && this.initialEventsFetched) {
if (event.localTimestamp > this.lastReply().localTimestamp && !this.findEventById(event.getId())) { if (event.localTimestamp > this.lastReply().localTimestamp) {
this.timelineSet.addEventToTimeline( this.addEventToTimeline(event, false);
event,
this.liveTimeline,
false,
false,
this.roomState,
);
} }
} }

View File

@@ -1727,7 +1727,7 @@ export class SyncApi {
// extractRelatedEvents(event: MatrixEvent, events: MatrixEvent[], relatedEvents: MatrixEvent[] = []): MatrixEvent[] { // extractRelatedEvents(event: MatrixEvent, events: MatrixEvent[], relatedEvents: MatrixEvent[] = []): MatrixEvent[] {
// relatedEvents.push(event); // relatedEvents.push(event);
// const parentEventId = event.parentEventId; // const parentEventId = event.getAssociatedId();
// const parentEventIndex = events.findIndex(event => event.getId() === parentEventId); // const parentEventIndex = events.findIndex(event => event.getId() === parentEventId);
// if (parentEventIndex > -1) { // if (parentEventIndex > -1) {