You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-08-06 12:02:40 +03:00
Fix issues with duplicated MatrixEvent objects around threads (#2256)
This commit is contained in:
committed by
GitHub
parent
6192325fe0
commit
c541b3f1ce
@@ -85,7 +85,7 @@ export function mkEvent(opts) {
|
|||||||
room_id: opts.room,
|
room_id: opts.room,
|
||||||
sender: opts.sender || opts.user, // opts.user for backwards-compat
|
sender: opts.sender || opts.user, // opts.user for backwards-compat
|
||||||
content: opts.content,
|
content: opts.content,
|
||||||
unsigned: opts.unsigned,
|
unsigned: opts.unsigned || {},
|
||||||
event_id: "$" + Math.random() + "-" + Math.random(),
|
event_id: "$" + Math.random() + "-" + Math.random(),
|
||||||
};
|
};
|
||||||
if (opts.skey !== undefined) {
|
if (opts.skey !== undefined) {
|
||||||
|
180
spec/unit/event-mapper.spec.ts
Normal file
180
spec/unit/event-mapper.spec.ts
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
/*
|
||||||
|
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 { MatrixClient, MatrixEvent, MatrixEventEvent, MatrixScheduler, Room } from "../../src";
|
||||||
|
import { eventMapperFor } from "../../src/event-mapper";
|
||||||
|
import { IStore } from "../../src/store";
|
||||||
|
|
||||||
|
describe("eventMapperFor", function() {
|
||||||
|
let rooms: Room[] = [];
|
||||||
|
|
||||||
|
const userId = "@test:example.org";
|
||||||
|
|
||||||
|
let client: MatrixClient;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
client = new MatrixClient({
|
||||||
|
baseUrl: "https://my.home.server",
|
||||||
|
accessToken: "my.access.token",
|
||||||
|
request: function() {} as any, // NOP
|
||||||
|
store: {
|
||||||
|
getRoom(roomId: string): Room | null {
|
||||||
|
return rooms.find(r => r.roomId === roomId);
|
||||||
|
},
|
||||||
|
} as IStore,
|
||||||
|
scheduler: {
|
||||||
|
setProcessFunction: jest.fn(),
|
||||||
|
} as unknown as MatrixScheduler,
|
||||||
|
userId: userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
rooms = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should de-duplicate MatrixEvent instances by means of findEventById on the room object", async () => {
|
||||||
|
const roomId = "!room:example.org";
|
||||||
|
const room = new Room(roomId, client, userId);
|
||||||
|
rooms.push(room);
|
||||||
|
|
||||||
|
const mapper = eventMapperFor(client, {
|
||||||
|
preventReEmit: true,
|
||||||
|
decrypt: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const eventId = "$event1:server";
|
||||||
|
const eventDefinition = {
|
||||||
|
type: "m.room.message",
|
||||||
|
room_id: roomId,
|
||||||
|
sender: userId,
|
||||||
|
content: {
|
||||||
|
body: "body",
|
||||||
|
},
|
||||||
|
unsigned: {},
|
||||||
|
event_id: eventId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const event = mapper(eventDefinition);
|
||||||
|
expect(event).toBeInstanceOf(MatrixEvent);
|
||||||
|
|
||||||
|
room.addLiveEvents([event]);
|
||||||
|
expect(room.findEventById(eventId)).toBe(event);
|
||||||
|
|
||||||
|
const event2 = mapper(eventDefinition);
|
||||||
|
expect(event).toBe(event2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not de-duplicate state events due to directionality of sentinel members", async () => {
|
||||||
|
const roomId = "!room:example.org";
|
||||||
|
const room = new Room(roomId, client, userId);
|
||||||
|
rooms.push(room);
|
||||||
|
|
||||||
|
const mapper = eventMapperFor(client, {
|
||||||
|
preventReEmit: true,
|
||||||
|
decrypt: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const eventId = "$event1:server";
|
||||||
|
const eventDefinition = {
|
||||||
|
type: "m.room.name",
|
||||||
|
room_id: roomId,
|
||||||
|
sender: userId,
|
||||||
|
content: {
|
||||||
|
name: "Room name",
|
||||||
|
},
|
||||||
|
unsigned: {},
|
||||||
|
event_id: eventId,
|
||||||
|
state_key: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const event = mapper(eventDefinition);
|
||||||
|
expect(event).toBeInstanceOf(MatrixEvent);
|
||||||
|
|
||||||
|
room.oldState.setStateEvents([event]);
|
||||||
|
room.currentState.setStateEvents([event]);
|
||||||
|
room.addLiveEvents([event]);
|
||||||
|
expect(room.findEventById(eventId)).toBe(event);
|
||||||
|
|
||||||
|
const event2 = mapper(eventDefinition);
|
||||||
|
expect(event).not.toBe(event2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should decrypt appropriately", async () => {
|
||||||
|
const roomId = "!room:example.org";
|
||||||
|
const room = new Room(roomId, client, userId);
|
||||||
|
rooms.push(room);
|
||||||
|
|
||||||
|
const eventId = "$event1:server";
|
||||||
|
const eventDefinition = {
|
||||||
|
type: "m.room.encrypted",
|
||||||
|
room_id: roomId,
|
||||||
|
sender: userId,
|
||||||
|
content: {
|
||||||
|
ciphertext: "",
|
||||||
|
},
|
||||||
|
unsigned: {},
|
||||||
|
event_id: eventId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const decryptEventIfNeededSpy = jest.spyOn(client, "decryptEventIfNeeded");
|
||||||
|
decryptEventIfNeededSpy.mockResolvedValue(); // stub it out
|
||||||
|
|
||||||
|
const mapper = eventMapperFor(client, {
|
||||||
|
decrypt: true,
|
||||||
|
});
|
||||||
|
const event = mapper(eventDefinition);
|
||||||
|
expect(event).toBeInstanceOf(MatrixEvent);
|
||||||
|
expect(decryptEventIfNeededSpy).toHaveBeenCalledWith(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should configure re-emitter appropriately", async () => {
|
||||||
|
const roomId = "!room:example.org";
|
||||||
|
const room = new Room(roomId, client, userId);
|
||||||
|
rooms.push(room);
|
||||||
|
|
||||||
|
const eventId = "$event1:server";
|
||||||
|
const eventDefinition = {
|
||||||
|
type: "m.room.message",
|
||||||
|
room_id: roomId,
|
||||||
|
sender: userId,
|
||||||
|
content: {
|
||||||
|
body: "body",
|
||||||
|
},
|
||||||
|
unsigned: {},
|
||||||
|
event_id: eventId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const evListener = jest.fn();
|
||||||
|
client.on(MatrixEventEvent.Replaced, evListener);
|
||||||
|
|
||||||
|
const noReEmitMapper = eventMapperFor(client, {
|
||||||
|
preventReEmit: true,
|
||||||
|
});
|
||||||
|
const event1 = noReEmitMapper(eventDefinition);
|
||||||
|
expect(event1).toBeInstanceOf(MatrixEvent);
|
||||||
|
event1.emit(MatrixEventEvent.Replaced, event1);
|
||||||
|
expect(evListener).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
const reEmitMapper = eventMapperFor(client, {
|
||||||
|
preventReEmit: false,
|
||||||
|
});
|
||||||
|
const event2 = reEmitMapper(eventDefinition);
|
||||||
|
expect(event2).toBeInstanceOf(MatrixEvent);
|
||||||
|
event2.emit(MatrixEventEvent.Replaced, event2);
|
||||||
|
expect(evListener.mock.calls[0][0]).toEqual(event2);
|
||||||
|
|
||||||
|
expect(event1).not.toBe(event2); // the event wasn't added to a room so de-duplication wouldn't occur
|
||||||
|
});
|
||||||
|
});
|
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2017 New Vector Ltd
|
Copyright 2017 New Vector Ltd
|
||||||
Copyright 2019 The Matrix.org Foundaction C.I.C.
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
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 { logger } from "../../src/logger";
|
import { logger } from "../../src/logger";
|
||||||
import { MatrixClient } from "../../src/client";
|
import { MatrixClient } from "../../src/client";
|
||||||
import { Filter } from "../../src/filter";
|
import { Filter } from "../../src/filter";
|
||||||
|
@@ -26,7 +26,6 @@ import { Room } from "../../src/models/room";
|
|||||||
import { RoomState } from "../../src/models/room-state";
|
import { RoomState } from "../../src/models/room-state";
|
||||||
import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event";
|
import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event";
|
||||||
import { TestClient } from "../TestClient";
|
import { TestClient } from "../TestClient";
|
||||||
import { Thread } from "../../src/models/thread";
|
|
||||||
|
|
||||||
describe("Room", function() {
|
describe("Room", function() {
|
||||||
const roomId = "!foo:bar";
|
const roomId = "!foo:bar";
|
||||||
@@ -1867,54 +1866,6 @@ describe("Room", function() {
|
|||||||
|
|
||||||
expect(() => room.createThread(rootEvent, [])).not.toThrow();
|
expect(() => room.createThread(rootEvent, [])).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not add events before server supports is known", function() {
|
|
||||||
Thread.hasServerSideSupport = undefined;
|
|
||||||
|
|
||||||
const rootEvent = new MatrixEvent({
|
|
||||||
event_id: "$666",
|
|
||||||
room_id: roomId,
|
|
||||||
content: {},
|
|
||||||
unsigned: {
|
|
||||||
"age": 1,
|
|
||||||
"m.relations": {
|
|
||||||
"m.thread": {
|
|
||||||
latest_event: null,
|
|
||||||
count: 1,
|
|
||||||
current_user_participated: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
let age = 1;
|
|
||||||
function mkEvt(id): MatrixEvent {
|
|
||||||
return new MatrixEvent({
|
|
||||||
event_id: id,
|
|
||||||
room_id: roomId,
|
|
||||||
content: {
|
|
||||||
"m.relates_to": {
|
|
||||||
"rel_type": "m.thread",
|
|
||||||
"event_id": "$666",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
unsigned: {
|
|
||||||
"age": age++,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const thread = room.createThread(rootEvent, []);
|
|
||||||
expect(thread.length).toBe(0);
|
|
||||||
|
|
||||||
thread.addEvent(mkEvt("$1"));
|
|
||||||
expect(thread.length).toBe(0);
|
|
||||||
|
|
||||||
Thread.hasServerSideSupport = true;
|
|
||||||
|
|
||||||
thread.addEvent(mkEvt("$2"));
|
|
||||||
expect(thread.length).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -3951,7 +3951,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
/**
|
/**
|
||||||
* Returns the eventType that should be used taking encryption into account
|
* Returns the eventType that should be used taking encryption into account
|
||||||
* for a given eventType.
|
* for a given eventType.
|
||||||
* @param {MatrixClient} client the client
|
|
||||||
* @param {string} roomId the room for the events `eventType` relates to
|
* @param {string} roomId the room for the events `eventType` relates to
|
||||||
* @param {string} eventType the event type
|
* @param {string} eventType the event type
|
||||||
* @return {string} the event type taking encryption into account
|
* @return {string} the event type taking encryption into account
|
||||||
@@ -6621,11 +6620,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
fetchedEventType,
|
fetchedEventType,
|
||||||
opts);
|
opts);
|
||||||
const mapper = this.getEventMapper();
|
const mapper = this.getEventMapper();
|
||||||
let originalEvent: MatrixEvent;
|
|
||||||
if (result.original_event) {
|
const originalEvent = result.original_event ? mapper(result.original_event) : undefined;
|
||||||
originalEvent = mapper(result.original_event);
|
|
||||||
}
|
|
||||||
let events = result.chunk.map(mapper);
|
let events = result.chunk.map(mapper);
|
||||||
|
|
||||||
if (fetchedEventType === EventType.RoomMessageEncrypted) {
|
if (fetchedEventType === EventType.RoomMessageEncrypted) {
|
||||||
const allEvents = originalEvent ? events.concat(originalEvent) : events;
|
const allEvents = originalEvent ? events.concat(originalEvent) : events;
|
||||||
await Promise.all(allEvents.map(e => this.decryptEventIfNeeded(e)));
|
await Promise.all(allEvents.map(e => this.decryptEventIfNeeded(e)));
|
||||||
@@ -6633,6 +6631,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
events = events.filter(e => e.getType() === eventType);
|
events = events.filter(e => e.getType() === eventType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (originalEvent && relationType === RelationType.Replace) {
|
if (originalEvent && relationType === RelationType.Replace) {
|
||||||
events = events.filter(e => e.getSender() === originalEvent.getSender());
|
events = events.filter(e => e.getSender() === originalEvent.getSender());
|
||||||
}
|
}
|
||||||
@@ -8866,12 +8865,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
}
|
}
|
||||||
|
|
||||||
const parentEventId = event.getAssociatedId();
|
const parentEventId = event.getAssociatedId();
|
||||||
const parentEvent = room?.findEventById(parentEventId) || events.find((mxEv: MatrixEvent) => (
|
const parentEvent = room?.findEventById(parentEventId) ?? events.find((mxEv: MatrixEvent) => (
|
||||||
mxEv.getId() === parentEventId
|
mxEv.getId() === parentEventId
|
||||||
));
|
));
|
||||||
|
|
||||||
// A reaction targetting the thread root needs to be routed to both the
|
// A reaction targeting the thread root needs to be routed to both the main timeline and the associated thread
|
||||||
// the main timeline and the associated thread
|
|
||||||
const targetingThreadRoot = parentEvent?.isThreadRoot || roots.has(event.relationEventId);
|
const targetingThreadRoot = parentEvent?.isThreadRoot || roots.has(event.relationEventId);
|
||||||
if (targetingThreadRoot) {
|
if (targetingThreadRoot) {
|
||||||
return {
|
return {
|
||||||
@@ -8887,18 +8885,21 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
// we want that redaction to be pushed to both timeline
|
// we want that redaction to be pushed to both timeline
|
||||||
if (parentEvent?.getAssociatedId()) {
|
if (parentEvent?.getAssociatedId()) {
|
||||||
return this.eventShouldLiveIn(parentEvent, room, events, roots);
|
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,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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[]): [
|
||||||
// Indices to the events array, for readibility
|
timelineEvents: MatrixEvent[],
|
||||||
|
threadedEvents: MatrixEvent[],
|
||||||
|
] {
|
||||||
|
// Indices to the events array, for readability
|
||||||
const ROOM = 0;
|
const ROOM = 0;
|
||||||
const THREAD = 1;
|
const THREAD = 1;
|
||||||
if (this.supportsExperimentalThreads()) {
|
if (this.supportsExperimentalThreads()) {
|
||||||
|
@@ -29,9 +29,22 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
|
|||||||
const decrypt = options.decrypt !== false;
|
const decrypt = options.decrypt !== false;
|
||||||
|
|
||||||
function mapper(plainOldJsObject: Partial<IEvent>) {
|
function mapper(plainOldJsObject: Partial<IEvent>) {
|
||||||
const event = new MatrixEvent(plainOldJsObject);
|
const room = client.getRoom(plainOldJsObject.room_id);
|
||||||
|
|
||||||
|
let event: MatrixEvent;
|
||||||
|
// If the event is already known to the room, let's re-use the model rather than duplicating.
|
||||||
|
// We avoid doing this to state events as they may be forward or backwards looking which tweaks behaviour.
|
||||||
|
if (room && plainOldJsObject.state_key === undefined) {
|
||||||
|
event = room.findEventById(plainOldJsObject.event_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!event || event.status) {
|
||||||
|
event = new MatrixEvent(plainOldJsObject);
|
||||||
|
} else {
|
||||||
|
// merge the latest unsigned data from the server
|
||||||
|
event.setUnsigned({ ...event.getUnsigned(), ...plainOldJsObject.unsigned });
|
||||||
|
}
|
||||||
|
|
||||||
const room = client.getRoom(event.getRoomId());
|
|
||||||
if (room?.threads.has(event.getId())) {
|
if (room?.threads.has(event.getId())) {
|
||||||
event.setThread(room.threads.get(event.getId()));
|
event.setThread(room.threads.get(event.getId()));
|
||||||
}
|
}
|
||||||
@@ -46,6 +59,7 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
|
|||||||
client.decryptEventIfNeeded(event);
|
client.decryptEventIfNeeded(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!preventReEmit) {
|
if (!preventReEmit) {
|
||||||
client.reEmitter.reEmit(event, [
|
client.reEmitter.reEmit(event, [
|
||||||
MatrixEventEvent.Replaced,
|
MatrixEventEvent.Replaced,
|
||||||
|
@@ -287,7 +287,7 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
|
|||||||
public target: RoomMember = null;
|
public target: RoomMember = null;
|
||||||
public status: EventStatus = null;
|
public status: EventStatus = null;
|
||||||
public error: MatrixError = null;
|
public error: MatrixError = null;
|
||||||
public forwardLooking = true;
|
public forwardLooking = true; // only state events may be backwards looking
|
||||||
|
|
||||||
/* If the event is a `m.key.verification.request` (or to_device `m.key.verification.start`) event,
|
/* If the event is a `m.key.verification.request` (or to_device `m.key.verification.start`) event,
|
||||||
* `Crypto` will set this the `VerificationRequest` for the event
|
* `Crypto` will set this the `VerificationRequest` for the event
|
||||||
|
@@ -48,6 +48,7 @@ import {
|
|||||||
} from "./thread";
|
} from "./thread";
|
||||||
import { Method } from "../http-api";
|
import { Method } from "../http-api";
|
||||||
import { TypedEventEmitter } from "./typed-event-emitter";
|
import { TypedEventEmitter } from "./typed-event-emitter";
|
||||||
|
import { IMinimalEvent } from "../sync-accumulator";
|
||||||
|
|
||||||
// These constants are used as sane defaults when the homeserver doesn't support
|
// 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
|
// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be
|
||||||
@@ -1005,17 +1006,15 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get an event which is stored in our unfiltered timeline set or in a thread
|
* Get an event which is stored in our unfiltered timeline set, or in a thread
|
||||||
*
|
*
|
||||||
* @param {string} eventId event ID to look for
|
* @param {string} eventId event ID to look for
|
||||||
* @return {?module:models/event.MatrixEvent} the given event, or undefined if unknown
|
* @return {?module:models/event.MatrixEvent} the given event, or undefined if unknown
|
||||||
*/
|
*/
|
||||||
public findEventById(eventId: string): MatrixEvent | undefined {
|
public findEventById(eventId: string): MatrixEvent | undefined {
|
||||||
let event = this.getUnfilteredTimelineSet().findEventById(eventId);
|
let event = this.getUnfilteredTimelineSet().findEventById(eventId);
|
||||||
|
|
||||||
if (event) {
|
if (!event) {
|
||||||
return event;
|
|
||||||
} else {
|
|
||||||
const threads = this.getThreads();
|
const threads = this.getThreads();
|
||||||
for (let i = 0; i < threads.length; i++) {
|
for (let i = 0; i < threads.length; i++) {
|
||||||
const thread = threads[i];
|
const thread = threads[i];
|
||||||
@@ -1025,6 +1024,8 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return event;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1204,10 +1205,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
timeline: EventTimeline,
|
timeline: EventTimeline,
|
||||||
paginationToken?: string,
|
paginationToken?: string,
|
||||||
): void {
|
): void {
|
||||||
timeline.getTimelineSet().addEventsToTimeline(
|
timeline.getTimelineSet().addEventsToTimeline(events, toStartOfTimeline, timeline, paginationToken);
|
||||||
events, toStartOfTimeline,
|
|
||||||
timeline, paginationToken,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1592,10 +1590,9 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
} else {
|
} else {
|
||||||
const events = [event];
|
const events = [event];
|
||||||
let rootEvent = this.findEventById(event.threadRootId);
|
let rootEvent = this.findEventById(event.threadRootId);
|
||||||
// If the rootEvent does not exist in the current sync, then look for
|
// If the rootEvent does not exist in the current sync, then look for it over the network.
|
||||||
// it over the network
|
|
||||||
try {
|
try {
|
||||||
let eventData;
|
let eventData: IMinimalEvent;
|
||||||
if (event.threadRootId) {
|
if (event.threadRootId) {
|
||||||
eventData = await this.client.fetchRoomEvent(this.roomId, event.threadRootId);
|
eventData = await this.client.fetchRoomEvent(this.roomId, event.threadRootId);
|
||||||
}
|
}
|
||||||
@@ -1606,11 +1603,13 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
rootEvent.setUnsigned(eventData.unsigned);
|
rootEvent.setUnsigned(eventData.unsigned);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
// The root event might be not be visible to the person requesting
|
// The root event might be not be visible to the person requesting it.
|
||||||
// it. If it wasn't fetched successfully the thread will work
|
// If it wasn't fetched successfully the thread will work in "limited" mode and won't
|
||||||
// in "limited" mode and won't benefit from all the APIs a homeserver
|
// benefit from all the APIs a homeserver can provide to enhance the thread experience
|
||||||
// can provide to enhance the thread experience
|
|
||||||
thread = this.createThread(rootEvent, events, toStartOfTimeline);
|
thread = this.createThread(rootEvent, events, toStartOfTimeline);
|
||||||
|
if (thread) {
|
||||||
|
rootEvent.setThread(thread);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1672,7 +1671,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
applyRedaction(event: MatrixEvent): void {
|
private applyRedaction(event: MatrixEvent): void {
|
||||||
if (event.isRedaction()) {
|
if (event.isRedaction()) {
|
||||||
const redactId = event.event.redacts;
|
const redactId = event.event.redacts;
|
||||||
|
|
||||||
@@ -1888,6 +1887,14 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private shouldAddEventToMainTimeline(thread: Thread, event: MatrixEvent): boolean {
|
||||||
|
if (!thread) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !event.isThreadRelation && thread.id === event.getAssociatedId();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to aggregate the local echo for a relation, and also
|
* Used to aggregate the local echo for a relation, and also
|
||||||
* for re-applying a relation after it's redaction has been cancelled,
|
* for re-applying a relation after it's redaction has been cancelled,
|
||||||
@@ -1900,11 +1907,9 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
*/
|
*/
|
||||||
private aggregateNonLiveRelation(event: MatrixEvent): void {
|
private aggregateNonLiveRelation(event: MatrixEvent): void {
|
||||||
const thread = this.findThreadForEvent(event);
|
const thread = this.findThreadForEvent(event);
|
||||||
if (thread) {
|
thread?.timelineSet.aggregateRelations(event);
|
||||||
thread.timelineSet.aggregateRelations(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (thread?.id === event.getAssociatedId() || !thread) {
|
if (this.shouldAddEventToMainTimeline(thread, event)) {
|
||||||
// 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++) {
|
||||||
@@ -1961,11 +1966,9 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
localEvent.handleRemoteEcho(remoteEvent.event);
|
localEvent.handleRemoteEcho(remoteEvent.event);
|
||||||
|
|
||||||
const thread = this.findThreadForEvent(remoteEvent);
|
const thread = this.findThreadForEvent(remoteEvent);
|
||||||
if (thread) {
|
thread?.timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId);
|
||||||
thread.timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (thread?.id === remoteEvent.getAssociatedId() || !thread) {
|
if (this.shouldAddEventToMainTimeline(thread, remoteEvent)) {
|
||||||
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];
|
||||||
|
|
||||||
@@ -2032,10 +2035,9 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
event.replaceLocalEventId(newEventId);
|
event.replaceLocalEventId(newEventId);
|
||||||
|
|
||||||
const thread = this.findThreadForEvent(event);
|
const thread = this.findThreadForEvent(event);
|
||||||
if (thread) {
|
thread?.timelineSet.replaceEventId(oldEventId, newEventId);
|
||||||
thread.timelineSet.replaceEventId(oldEventId, newEventId);
|
|
||||||
}
|
if (this.shouldAddEventToMainTimeline(thread, event)) {
|
||||||
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.
|
||||||
@@ -2046,12 +2048,10 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
} else if (newStatus == EventStatus.CANCELLED) {
|
} else if (newStatus == EventStatus.CANCELLED) {
|
||||||
// remove it from the pending event list, or the timeline.
|
// remove it from the pending event list, or the timeline.
|
||||||
if (this.pendingEventList) {
|
if (this.pendingEventList) {
|
||||||
const idx = this.pendingEventList.findIndex(ev => ev.getId() === oldEventId);
|
const removedEvent = this.getPendingEvent(oldEventId);
|
||||||
if (idx !== -1) {
|
this.removePendingEvent(oldEventId);
|
||||||
const [removedEvent] = this.pendingEventList.splice(idx, 1);
|
if (removedEvent.isRedaction()) {
|
||||||
if (removedEvent.isRedaction()) {
|
this.revertRedactionLocalEcho(removedEvent);
|
||||||
this.revertRedactionLocalEcho(removedEvent);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.removeEvent(oldEventId);
|
this.removeEvent(oldEventId);
|
||||||
|
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MatrixClient, RoomEvent } from "../matrix";
|
import { MatrixClient, RelationType, RoomEvent } from "../matrix";
|
||||||
import { TypedReEmitter } from "../ReEmitter";
|
import { TypedReEmitter } from "../ReEmitter";
|
||||||
import { IRelationsRequestOpts } from "../@types/requests";
|
import { IRelationsRequestOpts } from "../@types/requests";
|
||||||
import { IThreadBundledRelationship, MatrixEvent } from "./event";
|
import { IThreadBundledRelationship, MatrixEvent } from "./event";
|
||||||
@@ -24,6 +24,7 @@ import { Room } from './room';
|
|||||||
import { TypedEventEmitter } from "./typed-event-emitter";
|
import { TypedEventEmitter } from "./typed-event-emitter";
|
||||||
import { RoomState } from "./room-state";
|
import { RoomState } from "./room-state";
|
||||||
import { ServerControlledNamespacedValue } from "../NamespacedValue";
|
import { ServerControlledNamespacedValue } from "../NamespacedValue";
|
||||||
|
import { logger } from "../logger";
|
||||||
|
|
||||||
export enum ThreadEvent {
|
export enum ThreadEvent {
|
||||||
New = "Thread.new",
|
New = "Thread.new",
|
||||||
@@ -93,14 +94,9 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
|||||||
RoomEvent.TimelineReset,
|
RoomEvent.TimelineReset,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// If we weren't able to find the root event, it's probably missing
|
// If we weren't able to find the root event, it's probably missing,
|
||||||
// and we define the thread ID from one of the thread relation
|
// and we define the thread ID from one of the thread relation
|
||||||
if (!rootEvent) {
|
this.id = rootEvent?.getId() ?? opts?.initialEvents?.find(event => event.isThreadRelation)?.relationEventId;
|
||||||
this.id = opts?.initialEvents
|
|
||||||
?.find(event => event.isThreadRelation)?.relationEventId;
|
|
||||||
} else {
|
|
||||||
this.id = rootEvent.getId();
|
|
||||||
}
|
|
||||||
this.initialiseThread(this.rootEvent);
|
this.initialiseThread(this.rootEvent);
|
||||||
|
|
||||||
opts?.initialEvents?.forEach(event => this.addEvent(event, false));
|
opts?.initialEvents?.forEach(event => this.addEvent(event, false));
|
||||||
@@ -169,10 +165,10 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
|||||||
this.addEventToTimeline(event, toStartOfTimeline);
|
this.addEventToTimeline(event, toStartOfTimeline);
|
||||||
|
|
||||||
await this.client.decryptEventIfNeeded(event, {});
|
await this.client.decryptEventIfNeeded(event, {});
|
||||||
}
|
} else {
|
||||||
|
await this.fetchEditsWhereNeeded(event);
|
||||||
|
|
||||||
if (Thread.hasServerSideSupport && this.initialEventsFetched) {
|
if (this.initialEventsFetched && event.localTimestamp > this.lastReply().localTimestamp) {
|
||||||
if (event.localTimestamp > this.lastReply().localTimestamp) {
|
|
||||||
this.addEventToTimeline(event, false);
|
this.addEventToTimeline(event, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,10 +217,28 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
|||||||
|
|
||||||
const event = new MatrixEvent(bundledRelationship.latest_event);
|
const event = new MatrixEvent(bundledRelationship.latest_event);
|
||||||
this.setEventMetadata(event);
|
this.setEventMetadata(event);
|
||||||
|
event.setThread(this);
|
||||||
this.lastEvent = event;
|
this.lastEvent = event;
|
||||||
|
|
||||||
|
this.fetchEditsWhereNeeded(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) => {
|
||||||
|
return this.client.relations(this.roomId, event.getId(), RelationType.Replace, event.getType(), {
|
||||||
|
limit: 1,
|
||||||
|
}).then(relations => {
|
||||||
|
if (relations.events.length) {
|
||||||
|
event.makeReplaced(relations.events[0]);
|
||||||
|
}
|
||||||
|
}).catch(e => {
|
||||||
|
logger.error("Failed to load edits for encrypted thread event", e);
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
public async fetchInitialEvents(): Promise<{
|
public async fetchInitialEvents(): Promise<{
|
||||||
originalEvent: MatrixEvent;
|
originalEvent: MatrixEvent;
|
||||||
events: MatrixEvent[];
|
events: MatrixEvent[];
|
||||||
@@ -235,6 +249,7 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
|||||||
this.initialEventsFetched = true;
|
this.initialEventsFetched = true;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.fetchEvents();
|
const response = await this.fetchEvents();
|
||||||
this.initialEventsFetched = true;
|
this.initialEventsFetched = true;
|
||||||
@@ -253,6 +268,11 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
|||||||
* Finds an event by ID in the current thread
|
* Finds an event by ID in the current thread
|
||||||
*/
|
*/
|
||||||
public findEventById(eventId: string) {
|
public findEventById(eventId: string) {
|
||||||
|
// Check the lastEvent as it may have been created based on a bundled relationship and not in a timeline
|
||||||
|
if (this.lastEvent?.getId() === eventId) {
|
||||||
|
return this.lastEvent;
|
||||||
|
}
|
||||||
|
|
||||||
return this.timelineSet.findEventById(eventId);
|
return this.timelineSet.findEventById(eventId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,6 +349,8 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
|||||||
events = [...events, originalEvent];
|
events = [...events, originalEvent];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.fetchEditsWhereNeeded(...events);
|
||||||
|
|
||||||
await Promise.all(events.map(event => {
|
await Promise.all(events.map(event => {
|
||||||
this.setEventMetadata(event);
|
this.setEventMetadata(event);
|
||||||
return this.client.decryptEventIfNeeded(event);
|
return this.client.decryptEventIfNeeded(event);
|
||||||
|
Reference in New Issue
Block a user