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 { Filter, MemoryStore, Room } from "../../src/matrix";
import { TestClient } from "../TestClient";
import { Thread } from "../../src/models/thread";
describe("MatrixClient", function() {
let client = null;
@@ -405,6 +404,11 @@ describe("MatrixClient", function() {
it("copies pre-thread in-timeline vote events onto both timelines", function() {
client.clientOpts = { experimentalThreadSupport: true };
const eventMessageInThread = buildEventMessageInThread();
const eventPollResponseReference = buildEventPollResponseReference();
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
const events = [
eventMessageInThread,
eventPollResponseReference,
@@ -435,6 +439,11 @@ describe("MatrixClient", function() {
it("copies pre-thread in-timeline reactions onto both timelines", function() {
client.clientOpts = { experimentalThreadSupport: true };
const eventMessageInThread = buildEventMessageInThread();
const eventReaction = buildEventReaction();
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
const events = [
eventMessageInThread,
eventReaction,
@@ -456,6 +465,11 @@ describe("MatrixClient", function() {
it("copies post-thread in-timeline vote events onto both timelines", function() {
client.clientOpts = { experimentalThreadSupport: true };
const eventPollResponseReference = buildEventPollResponseReference();
const eventMessageInThread = buildEventMessageInThread();
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
const events = [
eventPollResponseReference,
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() {
client.clientOpts = { experimentalThreadSupport: true };
const eventReaction = buildEventReaction();
const eventMessageInThread = buildEventMessageInThread();
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
const events = [
eventReaction,
eventMessageInThread,
@@ -610,6 +518,19 @@ describe("MatrixClient", function() {
it("sends room state events to the main timeline only", function() {
client.clientOpts = { experimentalThreadSupport: true };
// 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 = [
eventMessageInThread,
eventPollResponseReference,
@@ -655,7 +576,7 @@ function withThreadId(event, newThreadId) {
return ret;
}
const eventMessageInThread = new MatrixEvent({
const buildEventMessageInThread = () => new MatrixEvent({
"age": 80098509,
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
@@ -680,7 +601,7 @@ const eventMessageInThread = new MatrixEvent({
"user_id": "@andybalaam-test1:matrix.org",
});
const eventPollResponseReference = new MatrixEvent({
const buildEventPollResponseReference = () => new MatrixEvent({
"age": 80098509,
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
@@ -702,7 +623,7 @@ const eventPollResponseReference = new MatrixEvent({
"user_id": "@andybalaam-test1:matrix.org",
});
const eventReaction = new MatrixEvent({
const buildEventReaction = () => new MatrixEvent({
"content": {
"m.relates_to": {
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
@@ -721,7 +642,7 @@ const eventReaction = new MatrixEvent({
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
});
const eventPollStartThreadRoot = new MatrixEvent({
const buildEventPollStartThreadRoot = () => new MatrixEvent({
"age": 80108647,
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
@@ -739,7 +660,7 @@ const eventPollStartThreadRoot = new MatrixEvent({
"user_id": "@andybalaam-test1:matrix.org",
});
const eventRoomName = new MatrixEvent({
const buildEventRoomName = () => new MatrixEvent({
"age": 80123249,
"content": {
"name": "1 poll, 1 vote, 1 thread",
@@ -754,7 +675,7 @@ const eventRoomName = new MatrixEvent({
"user_id": "@andybalaam-test1:matrix.org",
});
const eventEncryption = new MatrixEvent({
const buildEventEncryption = () => new MatrixEvent({
"age": 80123383,
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
@@ -769,7 +690,7 @@ const eventEncryption = new MatrixEvent({
"user_id": "@andybalaam-test1:matrix.org",
});
const eventGuestAccess = new MatrixEvent({
const buildEventGuestAccess = () => new MatrixEvent({
"age": 80123473,
"content": {
"guest_access": "can_join",
@@ -784,7 +705,7 @@ const eventGuestAccess = new MatrixEvent({
"user_id": "@andybalaam-test1:matrix.org",
});
const eventHistoryVisibility = new MatrixEvent({
const buildEventHistoryVisibility = () => new MatrixEvent({
"age": 80123556,
"content": {
"history_visibility": "shared",
@@ -799,7 +720,7 @@ const eventHistoryVisibility = new MatrixEvent({
"user_id": "@andybalaam-test1:matrix.org",
});
const eventJoinRules = new MatrixEvent({
const buildEventJoinRules = () => new MatrixEvent({
"age": 80123696,
"content": {
"join_rule": "invite",
@@ -814,7 +735,7 @@ const eventJoinRules = new MatrixEvent({
"user_id": "@andybalaam-test1:matrix.org",
});
const eventPowerLevels = new MatrixEvent({
const buildEventPowerLevels = () => new MatrixEvent({
"age": 80124105,
"content": {
"ban": 50,
@@ -849,7 +770,7 @@ const eventPowerLevels = new MatrixEvent({
"user_id": "@andybalaam-test1:matrix.org",
});
const eventMember = new MatrixEvent({
const buildEventMember = () => new MatrixEvent({
"age": 80125279,
"content": {
"avatar_url": "mxc://matrix.org/aNtbVcFfwotudypZcHsIcPOc",
@@ -866,7 +787,7 @@ const eventMember = new MatrixEvent({
"user_id": "@andybalaam-test1:matrix.org",
});
const eventCreate = new MatrixEvent({
const buildEventCreate = () => new MatrixEvent({
"age": 80126105,
"content": {
"creator": "@andybalaam-test1:matrix.org",

View File

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

View File

@@ -3594,17 +3594,20 @@ export class MatrixClient extends EventEmitter {
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"],
"rel_type": RelationType.Thread,
"event_id": threadId,
};
const thread = this.getRoom(roomId)?.threads.get(threadId);
if (thread) {
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);
if (thread) {
localEvent.setThread(thread);
localEvent.setThreadId(thread.id);
}
// if this is a relation or redaction of an event
@@ -9089,6 +9093,52 @@ export class MatrixClient extends EventEmitter {
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[]] {
// Indices to the events array, for readibility
const ROOM = 0;
@@ -9097,35 +9147,22 @@ export class MatrixClient extends EventEmitter {
const threadRoots = this.findThreadRoots(events);
return events.reduce((memo, event: MatrixEvent) => {
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) {
// If we refer to the thread root, we should be copied
// into the thread as well as the main timeline.
// This happens for reactions, annotations, poll votes etc.
const copiedEvent = event.toSnapshot();
const {
shouldLiveInRoom,
shouldLiveInThread,
threadId,
} = this.eventShouldLiveIn(event, room, events, threadRoots);
// The copied event is in this thread:
copiedEvent.setThreadId(parentEventId);
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);
}
if (shouldLiveInRoom) {
memo[ROOM].push(event);
}
const targetTimeline = shouldLiveInThreadTimeline ? THREAD : ROOM;
memo[targetTimeline].push(event);
if (shouldLiveInThread) {
event.setThreadId(threadId);
memo[THREAD].push(event);
}
return memo;
}, [[], []]);
} else {

View File

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

View File

@@ -512,8 +512,7 @@ export class MatrixEvent extends EventEmitter {
if (relatesTo?.rel_type === RelationType.Thread) {
return relatesTo.event_id;
} else {
return this.threadId
|| this.getThread()?.id;
return this.getThread()?.id || this.threadId;
}
}
@@ -537,10 +536,6 @@ export class MatrixEvent extends EventEmitter {
return !!threadDetails || (this.getThread()?.id === this.getId());
}
public get parentEventId(): string {
return this.replyEventId || this.relationEventId;
}
public get replyEventId(): string {
// We're prefer ev.getContent() over ev.getWireContent() to make sure
// 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 {
const relation = this.getRelation();
if (relation) {
if (this.replyEventId) {
return this.replyEventId;
} else if (relation) {
return relation.event_id;
} else if (this.isRedaction()) {
return this.event.redacts;
@@ -1561,6 +1558,7 @@ export class MatrixEvent extends EventEmitter {
*/
public setThread(thread: Thread): void {
this.thread = thread;
this.setThreadId(thread.id);
this.reEmitter.reEmit(thread, [ThreadEvent.Ready, ThreadEvent.Update]);
}

View File

@@ -171,11 +171,11 @@ export class Relations extends EventEmitter {
* @return {Array}
* Relation events in insertion order.
*/
public getRelations() {
public getRelations(): MatrixEvent[] {
return [...this.relations];
}
private addAnnotationToAggregation(event: MatrixEvent) {
private addAnnotationToAggregation(event: MatrixEvent): void {
const { key } = event.getRelation();
if (!key) {
return;
@@ -204,7 +204,7 @@ export class Relations extends EventEmitter {
eventsFromSender.add(event);
}
private removeAnnotationFromAggregation(event: MatrixEvent) {
private removeAnnotationFromAggregation(event: MatrixEvent): void {
const { key } = event.getRelation();
if (!key) {
return;
@@ -240,7 +240,7 @@ export class Relations extends EventEmitter {
* @param {MatrixEvent} redactedEvent
* 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)) {
return;
}

View File

@@ -512,16 +512,14 @@ export class Room extends EventEmitter {
*
* @throws If <code>opts.pendingEventOrdering</code> was not 'detached'
*/
public getPendingEvents(thread?: Thread): MatrixEvent[] {
public getPendingEvents(): MatrixEvent[] {
if (this.opts.pendingEventOrdering !== PendingEventOrdering.Detached) {
throw new Error(
"Cannot call getPendingEvents with pendingEventOrdering == " +
this.opts.pendingEventOrdering);
}
return this.pendingEventList.filter(event => {
return !thread || thread.id === event.threadRootId;
});
return this.pendingEventList;
}
/**
@@ -1358,7 +1356,7 @@ export class Room extends EventEmitter {
} else if (event.isThreadRoot) {
return this.threads.get(event.getId());
} else {
const parentEvent = this.findEventById(event.parentEventId);
const parentEvent = this.findEventById(event.getAssociatedId());
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);
}
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, {
initialEvents: events,
initialEvents: events.concat(relatedEvents),
room: this,
client: this.client,
});
@@ -1564,8 +1556,7 @@ export class Room extends EventEmitter {
EventTimeline.setEventMetadata(event, this.getLiveTimeline().getState(EventTimeline.FORWARDS), false);
this.txnToEvent[txnId] = event;
const thread = this.findThreadForEvent(event);
if (this.opts.pendingEventOrdering === PendingEventOrdering.Detached && !thread) {
if (this.opts.pendingEventOrdering === PendingEventOrdering.Detached) {
if (this.pendingEventList.some((e) => e.status === EventStatus.NOT_SENT)) {
logger.warn("Setting event as NOT_SENT due to messages in the same state");
event.setStatus(EventStatus.NOT_SENT);
@@ -1581,8 +1572,7 @@ export class Room extends EventEmitter {
if (event.isRedaction()) {
const redactId = event.event.redacts;
let redactedEvent = this.pendingEventList &&
this.pendingEventList.find(e => e.getId() === redactId);
let redactedEvent = this.pendingEventList?.find(e => e.getId() === redactId);
if (!redactedEvent) {
redactedEvent = this.findEventById(redactId);
}
@@ -1592,20 +1582,16 @@ export class Room extends EventEmitter {
}
}
} else {
if (thread) {
thread.addEvent(event, false);
} else {
for (let i = 0; i < this.timelineSets.length; i++) {
const timelineSet = this.timelineSets[i];
if (timelineSet.getFilter()) {
if (timelineSet.getFilter().filterRoomTimeline([event]).length) {
timelineSet.addEventToTimeline(event,
timelineSet.getLiveTimeline(), false);
}
} else {
for (let i = 0; i < this.timelineSets.length; i++) {
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.getLiveTimeline(), false);
}
}
}
@@ -1666,7 +1652,9 @@ export class Room extends EventEmitter {
const thread = this.findThreadForEvent(event);
if (thread) {
thread.timelineSet.aggregateRelations(event);
} else {
}
if (thread?.id === event.getAssociatedId() || !thread) {
// 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++) {
@@ -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.
*
@@ -1696,7 +1688,7 @@ export class Room extends EventEmitter {
* @fires module:client~MatrixClient#event:"Room.localEchoUpdated"
* @private
*/
private handleRemoteEcho(remoteEvent: MatrixEvent, localEvent: MatrixEvent): void {
public handleRemoteEcho(remoteEvent: MatrixEvent, localEvent: MatrixEvent): void {
const oldEventId = localEvent.getId();
const newEventId = remoteEvent.getId();
const oldStatus = localEvent.status;
@@ -1721,7 +1713,9 @@ export class Room extends EventEmitter {
const thread = this.findThreadForEvent(remoteEvent);
if (thread) {
thread.timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId);
} else {
}
if (thread?.id === remoteEvent.getAssociatedId() || !thread) {
for (let i = 0; i < this.timelineSets.length; i++) {
const timelineSet = this.timelineSets[i];
@@ -1791,7 +1785,8 @@ export class Room extends EventEmitter {
const thread = this.findThreadForEvent(event);
if (thread) {
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
// opts.pendingEventOrdering==chronological), we need to update the
// timeline map.

View File

@@ -113,6 +113,27 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
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
* 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
// no server support
if (!this.hasServerSideSupport) {
if (this.timelineSet.findEventById(event.getId())) {
return;
}
// all the relevant membership info to hydrate events with a sender
// is held in the main room timeline
// 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
event.setThread(this);
this.timelineSet.addEventToTimeline(
event,
this.liveTimeline,
toStartOfTimeline,
false,
this.roomState,
);
this.addEventToTimeline(event, toStartOfTimeline);
await this.client.decryptEventIfNeeded(event, {});
}
if (this.hasServerSideSupport && this.initialEventsFetched) {
if (event.localTimestamp > this.lastReply().localTimestamp && !this.findEventById(event.getId())) {
this.timelineSet.addEventToTimeline(
event,
this.liveTimeline,
false,
false,
this.roomState,
);
if (event.localTimestamp > this.lastReply().localTimestamp) {
this.addEventToTimeline(event, false);
}
}

View File

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