Compare commits

...

12 Commits

Author SHA1 Message Date
RiotRobot 59763a84f8 v17.0.0 2022-04-11 16:21:09 +01:00
RiotRobot 1375a4849c Prepare changelog for v17.0.0 2022-04-11 16:21:09 +01:00
RiotRobot c0e3ad4b83 v17.0.0-rc.3 2022-04-11 11:32:30 +01:00
RiotRobot 5305e373a0 Prepare changelog for v17.0.0-rc.3 2022-04-11 11:32:30 +01:00
Michael Telatynski 877e3df71b [Release] Port multiple threads fixes (#2292)
Co-authored-by: Germain Souquet <germains@element.io>
2022-04-11 11:29:22 +01:00
RiotRobot d705a0ed9e v17.0.0-rc.2 2022-04-08 12:08:04 +01:00
RiotRobot 7023fb1c99 Prepare changelog for v17.0.0-rc.2 2022-04-08 12:08:03 +01:00
RiotRobot fe36dafcc7 v3.42.2-rc.3 2022-04-08 10:40:56 +01:00
RiotRobot 04a6dbbedf Prepare changelog for v3.42.2-rc.3 2022-04-08 10:40:56 +01:00
RiotRobot 29b427aab7 v17.0.0-rc.1 2022-04-08 10:36:14 +01:00
RiotRobot 643b783dec Prepare changelog for v17.0.0-rc.1 2022-04-08 10:36:13 +01:00
Germain 872033a552 Port #2283 to release (#2284) 2022-04-08 10:34:09 +01:00
14 changed files with 582 additions and 338 deletions
+21
View File
@@ -1,3 +1,18 @@
Changes in [17.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v17.0.0) (2022-04-11)
==================================================================================================
## 🚨 BREAKING CHANGES
* Remove groups and groups-related APIs ([\#2234](https://github.com/matrix-org/matrix-js-sdk/pull/2234)).
## ✨ Features
* Add Element video room type ([\#2273](https://github.com/matrix-org/matrix-js-sdk/pull/2273)).
* Live location sharing - handle redacted beacons ([\#2269](https://github.com/matrix-org/matrix-js-sdk/pull/2269)).
## 🐛 Bug Fixes
* Fix getSessionsNeedingBackup() limit support ([\#2270](https://github.com/matrix-org/matrix-js-sdk/pull/2270)). Contributed by @adamvy.
* Fix issues with /search and /context API handling for threads ([\#2261](https://github.com/matrix-org/matrix-js-sdk/pull/2261)). Fixes vector-im/element-web#21543.
* Prevent exception 'Unable to set up secret storage' ([\#2260](https://github.com/matrix-org/matrix-js-sdk/pull/2260)).
Changes in [16.0.2-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v16.0.2-rc.1) (2022-04-05)
============================================================================================================
@@ -2003,6 +2018,12 @@ All Changes
* [BREAKING] Refactor the entire build process
[\#1113](https://github.com/matrix-org/matrix-js-sdk/pull/1113)
Changes in [3.42.2-rc.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v3.42.2-rc.3) (2022-04-08)
============================================================================================================
## 🐛 Bug Fixes
* Make self membership less prone to races ([\#2277](https://github.com/matrix-org/matrix-js-sdk/pull/2277)). Fixes vector-im/element-web#21661.
Changes in [3.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v3.0.0) (2020-01-13)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v3.0.0-rc.1...v3.0.0)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "16.0.2-rc.1",
"version": "17.0.0",
"description": "Matrix Client-Server SDK for Javascript",
"scripts": {
"prepublishOnly": "yarn build",
+16 -11
View File
@@ -145,12 +145,14 @@ describe("MatrixClient", function() {
describe("joinRoom", function() {
it("should no-op if you've already joined a room", function() {
const roomId = "!foo:bar";
const room = new Room(roomId, userId);
const room = new Room(roomId, client, userId);
client.fetchRoomEvent = () => Promise.resolve({});
room.addLiveEvents([
utils.mkMembership({
user: userId, room: roomId, mship: "join", event: true,
}),
]);
httpBackend.verifyNoOutstandingRequests();
store.storeRoom(room);
client.joinRoom(roomId);
httpBackend.verifyNoOutstandingRequests();
@@ -556,11 +558,14 @@ describe("MatrixClient", function() {
});
describe("partitionThreadedEvents", function() {
const room = new Room("!STrMRsukXHtqQdSeHa:matrix.org", client, userId);
let room;
beforeEach(() => {
room = new Room("!STrMRsukXHtqQdSeHa:matrix.org", client, userId);
});
it("returns empty arrays when given an empty arrays", function() {
const events = [];
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
const [timeline, threaded] = room.partitionThreadedEvents(events);
expect(timeline).toEqual([]);
expect(threaded).toEqual([]);
});
@@ -580,7 +585,7 @@ describe("MatrixClient", function() {
// Vote has no threadId yet
expect(eventPollResponseReference.threadId).toBeFalsy();
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
const [timeline, threaded] = room.partitionThreadedEvents(events);
expect(timeline).toEqual([
// The message that was sent in a thread is missing
@@ -613,7 +618,7 @@ describe("MatrixClient", function() {
eventReaction,
];
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
const [timeline, threaded] = room.partitionThreadedEvents(events);
expect(timeline).toEqual([
eventPollStartThreadRoot,
@@ -640,7 +645,7 @@ describe("MatrixClient", function() {
eventMessageInThread,
];
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
const [timeline, threaded] = room.partitionThreadedEvents(events);
expect(timeline).toEqual([
eventPollStartThreadRoot,
@@ -667,7 +672,7 @@ describe("MatrixClient", function() {
eventReaction,
];
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
const [timeline, threaded] = room.partitionThreadedEvents(events);
expect(timeline).toEqual([
eventPollStartThreadRoot,
@@ -710,7 +715,7 @@ describe("MatrixClient", function() {
eventMember,
eventCreate,
];
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
const [timeline, threaded] = room.partitionThreadedEvents(events);
expect(timeline).toEqual([
// The message that was sent in a thread is missing
@@ -749,7 +754,7 @@ describe("MatrixClient", function() {
threadedReactionRedaction,
];
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
const [timeline, threaded] = room.partitionThreadedEvents(events);
expect(timeline).toEqual([
threadRootEvent,
@@ -778,7 +783,7 @@ describe("MatrixClient", function() {
replyToReply,
];
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
const [timeline, threaded] = room.partitionThreadedEvents(events);
expect(timeline).toEqual([
threadRootEvent,
@@ -805,7 +810,7 @@ describe("MatrixClient", function() {
replyToThreadResponse,
];
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
const [timeline, threaded] = room.partitionThreadedEvents(events);
expect(timeline).toEqual([
threadRootEvent,
+14 -4
View File
@@ -8,6 +8,7 @@ import { logger } from '../../src/logger';
import { IContent, IEvent, IUnsigned, MatrixEvent, MatrixEventEvent } from "../../src/models/event";
import { ClientEvent, EventType, MatrixClient } from "../../src";
import { SyncState } from "../../src/sync";
import { eventMapperFor } from "../../src/event-mapper";
/**
* Return a promise that is resolved when the client next emits a
@@ -79,6 +80,7 @@ interface IEventOpts {
redacts?: string;
}
let testEventIndex = 1; // counter for events, easier for comparison of randomly generated events
/**
* Create an Event.
* @param {Object} opts Values for the event.
@@ -88,9 +90,10 @@ interface IEventOpts {
* @param {string} opts.skey Optional. The state key (auto inserts empty string)
* @param {Object} opts.content The event.content
* @param {boolean} opts.event True to make a MatrixEvent.
* @param {MatrixClient} client If passed along with opts.event=true will be used to set up re-emitters.
* @return {Object} a JSON object representing this event.
*/
export function mkEvent(opts: IEventOpts): object | MatrixEvent {
export function mkEvent(opts: IEventOpts, client?: MatrixClient): object | MatrixEvent {
if (!opts.type || !opts.content) {
throw new Error("Missing .type or .content =>" + JSON.stringify(opts));
}
@@ -100,7 +103,8 @@ export function mkEvent(opts: IEventOpts): object | MatrixEvent {
sender: opts.sender || opts.user, // opts.user for backwards-compat
content: opts.content,
unsigned: opts.unsigned || {},
event_id: "$" + Math.random() + "-" + Math.random(),
event_id: "$" + testEventIndex++ + "-" + Math.random() + "-" + Math.random(),
txn_id: "~" + Math.random(),
redacts: opts.redacts,
};
if (opts.skey !== undefined) {
@@ -116,6 +120,11 @@ export function mkEvent(opts: IEventOpts): object | MatrixEvent {
].includes(opts.type)) {
event.state_key = "";
}
if (opts.event && client) {
return eventMapperFor(client, {})(event);
}
return opts.event ? new MatrixEvent(event) : event;
}
@@ -208,9 +217,10 @@ interface IMessageOpts {
* @param {string} opts.user The user ID for the event.
* @param {string} opts.msg Optional. The content.body for the event.
* @param {boolean} opts.event True to make a MatrixEvent.
* @param {MatrixClient} client If passed along with opts.event=true will be used to set up re-emitters.
* @return {Object|MatrixEvent} The event
*/
export function mkMessage(opts: IMessageOpts): object | MatrixEvent {
export function mkMessage(opts: IMessageOpts, client?: MatrixClient): object | MatrixEvent {
const eventOpts: IEventOpts = {
...opts,
type: EventType.RoomMessage,
@@ -223,7 +233,7 @@ export function mkMessage(opts: IMessageOpts): object | MatrixEvent {
if (!eventOpts.content.body) {
eventOpts.content.body = "Random->" + Math.random();
}
return mkEvent(eventOpts);
return mkEvent(eventOpts, client);
}
/**
+1 -1
View File
@@ -981,7 +981,7 @@ describe("MatrixClient", function() {
expect(rootEvent.isThreadRoot).toBe(true);
const [roomEvents, threadEvents] = client.partitionThreadedEvents(room, [rootEvent]);
const [roomEvents, threadEvents] = room.partitionThreadedEvents([rootEvent]);
expect(roomEvents).toHaveLength(1);
expect(threadEvents).toHaveLength(1);
+303 -133
View File
@@ -35,6 +35,8 @@ import { Room } from "../../src/models/room";
import { RoomState } from "../../src/models/room-state";
import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event";
import { TestClient } from "../TestClient";
import { emitPromise } from "../test-utils/test-utils";
import { ThreadEvent } from "../../src/models/thread";
describe("Room", function() {
const roomId = "!foo:bar";
@@ -44,8 +46,86 @@ describe("Room", function() {
const userD = "@dorothy:bar";
let room;
const mkMessage = () => utils.mkMessage({
event: true,
user: userA,
room: roomId,
}, room.client) as MatrixEvent;
const mkReply = (target: MatrixEvent) => utils.mkEvent({
event: true,
type: EventType.RoomMessage,
user: userA,
room: roomId,
content: {
"body": "Reply :: " + Math.random(),
"m.relates_to": {
"m.in_reply_to": {
"event_id": target.getId(),
},
},
},
}, room.client) as MatrixEvent;
const mkEdit = (target: MatrixEvent, salt = Math.random()) => utils.mkEvent({
event: true,
type: EventType.RoomMessage,
user: userA,
room: roomId,
content: {
"body": "* Edit of :: " + target.getId() + " :: " + salt,
"m.new_content": {
body: "Edit of :: " + target.getId() + " :: " + salt,
},
"m.relates_to": {
rel_type: RelationType.Replace,
event_id: target.getId(),
},
},
}, room.client) as MatrixEvent;
const mkThreadResponse = (root: MatrixEvent) => utils.mkEvent({
event: true,
type: EventType.RoomMessage,
user: userA,
room: roomId,
content: {
"body": "Thread response :: " + Math.random(),
"m.relates_to": {
"event_id": root.getId(),
"m.in_reply_to": {
"event_id": root.getId(),
},
"rel_type": "m.thread",
},
},
}, room.client) as MatrixEvent;
const mkReaction = (target: MatrixEvent) => utils.mkEvent({
event: true,
type: EventType.Reaction,
user: userA,
room: roomId,
content: {
"m.relates_to": {
"rel_type": RelationType.Annotation,
"event_id": target.getId(),
"key": Math.random().toString(),
},
},
}, room.client) as MatrixEvent;
const mkRedaction = (target: MatrixEvent) => utils.mkEvent({
event: true,
type: EventType.RoomRedaction,
user: userA,
room: roomId,
redacts: target.getId(),
content: {},
}, room.client) as MatrixEvent;
beforeEach(function() {
room = new Room(roomId, null, userA);
room = new Room(roomId, new TestClient(userA, "device").client, userA);
// mock RoomStates
room.oldState = room.getLiveTimeline().startState = utils.mock(RoomState, "oldState");
room.currentState = room.getLiveTimeline().endState = utils.mock(RoomState, "currentState");
@@ -157,19 +237,18 @@ describe("Room", function() {
expect(room.timeline[0]).toEqual(events[0]);
});
it("should emit 'Room.timeline' events",
function() {
let callCount = 0;
room.on("Room.timeline", function(event, emitRoom, toStart) {
callCount += 1;
expect(room.timeline.length).toEqual(callCount);
expect(event).toEqual(events[callCount - 1]);
expect(emitRoom).toEqual(room);
expect(toStart).toBeFalsy();
});
room.addLiveEvents(events);
expect(callCount).toEqual(2);
it("should emit 'Room.timeline' events", function() {
let callCount = 0;
room.on("Room.timeline", function(event, emitRoom, toStart) {
callCount += 1;
expect(room.timeline.length).toEqual(callCount);
expect(event).toEqual(events[callCount - 1]);
expect(emitRoom).toEqual(room);
expect(toStart).toBeFalsy();
});
room.addLiveEvents(events);
expect(callCount).toEqual(2);
});
it("should call setStateEvents on the right RoomState with the right forwardLooking value for new events",
function() {
@@ -338,42 +417,41 @@ describe("Room", function() {
expect(oldEv.sender).toEqual(oldSentinel);
});
it("should set event.target for new and old m.room.member events",
function() {
const sentinel = {
userId: userA,
membership: "join",
name: "Alice",
};
const oldSentinel = {
userId: userA,
membership: "join",
name: "Old Alice",
};
room.currentState.getSentinelMember.mockImplementation(function(uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
room.oldState.getSentinelMember.mockImplementation(function(uid) {
if (uid === userA) {
return oldSentinel;
}
return null;
});
const newEv = utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, event: true,
}) as MatrixEvent;
const oldEv = utils.mkMembership({
room: roomId, mship: "ban", user: userB, skey: userA, event: true,
}) as MatrixEvent;
room.addLiveEvents([newEv]);
expect(newEv.target).toEqual(sentinel);
room.addEventsToTimeline([oldEv], true, room.getLiveTimeline());
expect(oldEv.target).toEqual(oldSentinel);
it("should set event.target for new and old m.room.member events", function() {
const sentinel = {
userId: userA,
membership: "join",
name: "Alice",
};
const oldSentinel = {
userId: userA,
membership: "join",
name: "Old Alice",
};
room.currentState.getSentinelMember.mockImplementation(function(uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
room.oldState.getSentinelMember.mockImplementation(function(uid) {
if (uid === userA) {
return oldSentinel;
}
return null;
});
const newEv = utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, event: true,
}) as MatrixEvent;
const oldEv = utils.mkMembership({
room: roomId, mship: "ban", user: userB, skey: userA, event: true,
}) as MatrixEvent;
room.addLiveEvents([newEv]);
expect(newEv.target).toEqual(sentinel);
room.addEventsToTimeline([oldEv], true, room.getLiveTimeline());
expect(oldEv.target).toEqual(oldSentinel);
});
it("should call setStateEvents on the right RoomState with the right " +
"forwardLooking value for old events", function() {
@@ -406,7 +484,7 @@ describe("Room", function() {
let events = null;
beforeEach(function() {
room = new Room(roomId, null, null, { timelineSupport: timelineSupport });
room = new Room(roomId, new TestClient(userA).client, userA, { timelineSupport: timelineSupport });
// set events each time to avoid resusing Event objects (which
// doesn't work because they get frozen)
events = [
@@ -469,8 +547,7 @@ describe("Room", function() {
expect(callCount).toEqual(1);
});
it("should " + (timelineSupport ? "remember" : "forget") +
" old timelines", function() {
it("should " + (timelineSupport ? "remember" : "forget") + " old timelines", function() {
room.addLiveEvents([events[0]]);
expect(room.timeline.length).toEqual(1);
const firstLiveTimeline = room.getLiveTimeline();
@@ -486,7 +563,7 @@ describe("Room", function() {
describe("compareEventOrdering", function() {
beforeEach(function() {
room = new Room(roomId, null, null, { timelineSupport: true });
room = new Room(roomId, new TestClient(userA).client, userA, { timelineSupport: true });
});
const events: MatrixEvent[] = [
@@ -673,7 +750,7 @@ describe("Room", function() {
beforeEach(function() {
// no mocking
room = new Room(roomId, null, userA);
room = new Room(roomId, new TestClient(userA).client, userA);
});
describe("Room.recalculate => Stripped State Events", function() {
@@ -1259,6 +1336,7 @@ describe("Room", function() {
const client = (new TestClient(
"@alice:example.com", "alicedevice",
)).client;
client.supportsExperimentalThreads = () => true;
const room = new Room(roomId, client, userA, {
pendingEventOrdering: PendingEventOrdering.Detached,
});
@@ -1285,7 +1363,7 @@ describe("Room", function() {
it("should add pending events to the timeline if " +
"pendingEventOrdering == 'chronological'", function() {
room = new Room(roomId, null, userA, {
const room = new Room(roomId, new TestClient(userA).client, userA, {
pendingEventOrdering: PendingEventOrdering.Chronological,
});
const eventA = utils.mkMessage({
@@ -1504,7 +1582,7 @@ describe("Room", function() {
describe("guessDMUserId", function() {
it("should return first hero id", function() {
const room = new Room(roomId, null, userA);
const room = new Room(roomId, new TestClient(userA).client, userA);
room.setSummary({
'm.heroes': [userB],
'm.joined_member_count': 1,
@@ -1513,7 +1591,7 @@ describe("Room", function() {
expect(room.guessDMUserId()).toEqual(userB);
});
it("should return first member that isn't self", function() {
const room = new Room(roomId, null, userA);
const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([utils.mkMembership({
user: userB,
mship: "join",
@@ -1523,7 +1601,7 @@ describe("Room", function() {
expect(room.guessDMUserId()).toEqual(userB);
});
it("should return self if only member present", function() {
const room = new Room(roomId, null, userA);
const room = new Room(roomId, new TestClient(userA).client, userA);
expect(room.guessDMUserId()).toEqual(userA);
});
});
@@ -1542,12 +1620,12 @@ describe("Room", function() {
describe("getDefaultRoomName", function() {
it("should return 'Empty room' if a user is the only member", function() {
const room = new Room(roomId, null, userA);
const room = new Room(roomId, new TestClient(userA).client, userA);
expect(room.getDefaultRoomName(userA)).toEqual("Empty room");
});
it("should return a display name if one other member is in the room", function() {
const room = new Room(roomId, null, userA);
const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
@@ -1562,7 +1640,7 @@ describe("Room", function() {
});
it("should return a display name if one other member is banned", function() {
const room = new Room(roomId, null, userA);
const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
@@ -1577,7 +1655,7 @@ describe("Room", function() {
});
it("should return a display name if one other member is invited", function() {
const room = new Room(roomId, null, userA);
const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
@@ -1592,7 +1670,7 @@ describe("Room", function() {
});
it("should return 'Empty room (was User B)' if User B left the room", function() {
const room = new Room(roomId, null, userA);
const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
@@ -1607,7 +1685,7 @@ describe("Room", function() {
});
it("should return 'User B and User C' if in a room with two other users", function() {
const room = new Room(roomId, null, userA);
const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
@@ -1626,7 +1704,7 @@ describe("Room", function() {
});
it("should return 'User B and 2 others' if in a room with three other users", function() {
const room = new Room(roomId, null, userA);
const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
@@ -1651,7 +1729,7 @@ describe("Room", function() {
describe("io.element.functional_users", function() {
it("should return a display name (default behaviour) if no one is marked as a functional member", function() {
const room = new Room(roomId, null, userA);
const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
@@ -1673,7 +1751,7 @@ describe("Room", function() {
});
it("should return a display name (default behaviour) if service members is a number (invalid)", function() {
const room = new Room(roomId, null, userA);
const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
@@ -1697,7 +1775,7 @@ describe("Room", function() {
});
it("should return a display name (default behaviour) if service members is a string (invalid)", function() {
const room = new Room(roomId, null, userA);
const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
@@ -1719,7 +1797,7 @@ describe("Room", function() {
});
it("should return 'Empty room' if the only other member is a functional member", function() {
const room = new Room(roomId, null, userA);
const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
@@ -1741,7 +1819,7 @@ describe("Room", function() {
});
it("should return 'User B' if User B is the only other member who isn't a functional member", function() {
const room = new Room(roomId, null, userA);
const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
@@ -1767,7 +1845,7 @@ describe("Room", function() {
});
it("should return 'Empty room' if all other members are functional members", function() {
const room = new Room(roomId, null, userA);
const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
@@ -1793,7 +1871,7 @@ describe("Room", function() {
});
it("should not break if an unjoined user is marked as a service user", function() {
const room = new Room(roomId, null, userA);
const room = new Room(roomId, new TestClient(userA).client, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
@@ -1821,6 +1899,7 @@ describe("Room", function() {
"@alice:example.com", "alicedevice",
)).client;
room = new Room(roomId, client, userA);
client.getRoom = () => room;
});
it("allow create threads without a root event", function() {
@@ -1858,71 +1937,162 @@ describe("Room", function() {
expect(() => room.createThread(rootEvent, [])).not.toThrow();
});
it("Edits update the lastReply event", async () => {
room.client.supportsExperimentalThreads = () => true;
const randomMessage = mkMessage();
const threadRoot = mkMessage();
const threadResponse = mkThreadResponse(threadRoot);
threadResponse.localTimestamp += 1000;
const threadResponseEdit = mkEdit(threadResponse);
threadResponseEdit.localTimestamp += 2000;
room.client.fetchRoomEvent = (eventId: string) => Promise.resolve({
...threadRoot.event,
unsigned: {
"age": 123,
"m.relations": {
"m.thread": {
latest_event: threadResponse.event,
count: 2,
current_user_participated: true,
},
},
},
});
room.addLiveEvents([randomMessage, threadRoot, threadResponse]);
const thread = await emitPromise(room, ThreadEvent.New);
expect(thread.replyToEvent).toBe(threadResponse);
expect(thread.replyToEvent.getContent().body).toBe(threadResponse.getContent().body);
room.addLiveEvents([threadResponseEdit]);
await emitPromise(thread, ThreadEvent.Update);
expect(thread.replyToEvent.getContent().body).toBe(threadResponseEdit.getContent()["m.new_content"].body);
});
it("Redactions to thread responses decrement the length", async () => {
room.client.supportsExperimentalThreads = () => true;
const threadRoot = mkMessage();
const threadResponse1 = mkThreadResponse(threadRoot);
threadResponse1.localTimestamp += 1000;
const threadResponse2 = mkThreadResponse(threadRoot);
threadResponse2.localTimestamp += 2000;
room.client.fetchRoomEvent = (eventId: string) => Promise.resolve({
...threadRoot.event,
unsigned: {
"age": 123,
"m.relations": {
"m.thread": {
latest_event: threadResponse2.event,
count: 2,
current_user_participated: true,
},
},
},
});
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2]);
const thread = await emitPromise(room, ThreadEvent.New);
expect(thread).toHaveLength(2);
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
const threadResponse1Redaction = mkRedaction(threadResponse1);
room.addLiveEvents([threadResponse1Redaction]);
await emitPromise(thread, ThreadEvent.Update);
expect(thread).toHaveLength(1);
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
});
it("Redactions to reactions in threads do not decrement the length", async () => {
room.client.supportsExperimentalThreads = () => true;
const threadRoot = mkMessage();
const threadResponse1 = mkThreadResponse(threadRoot);
threadResponse1.localTimestamp += 1000;
const threadResponse2 = mkThreadResponse(threadRoot);
threadResponse2.localTimestamp += 2000;
const threadResponse2Reaction = mkReaction(threadResponse2);
room.client.fetchRoomEvent = (eventId: string) => Promise.resolve({
...threadRoot.event,
unsigned: {
"age": 123,
"m.relations": {
"m.thread": {
latest_event: threadResponse2.event,
count: 2,
current_user_participated: true,
},
},
},
});
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2, threadResponse2Reaction]);
const thread = await emitPromise(room, ThreadEvent.New);
expect(thread).toHaveLength(2);
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
const threadResponse2ReactionRedaction = mkRedaction(threadResponse2Reaction);
room.addLiveEvents([threadResponse2ReactionRedaction]);
await emitPromise(thread, ThreadEvent.Update);
expect(thread).toHaveLength(2);
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
});
it("Redacting the lastEvent finds a new lastEvent", async () => {
room.client.supportsExperimentalThreads = () => true;
const threadRoot = mkMessage();
const threadResponse1 = mkThreadResponse(threadRoot);
threadResponse1.localTimestamp += 1000;
const threadResponse2 = mkThreadResponse(threadRoot);
threadResponse2.localTimestamp += 2000;
room.client.fetchRoomEvent = (eventId: string) => Promise.resolve({
...threadRoot.event,
unsigned: {
"age": 123,
"m.relations": {
"m.thread": {
latest_event: threadResponse2.event,
count: 2,
current_user_participated: true,
},
},
},
});
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2]);
const thread = await emitPromise(room, ThreadEvent.New);
expect(thread).toHaveLength(2);
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
const threadResponse2Redaction = mkRedaction(threadResponse2);
room.addLiveEvents([threadResponse2Redaction]);
await emitPromise(thread, ThreadEvent.Update);
expect(thread).toHaveLength(1);
expect(thread.replyToEvent.getId()).toBe(threadResponse1.getId());
const threadResponse1Redaction = mkRedaction(threadResponse1);
room.addLiveEvents([threadResponse1Redaction]);
await emitPromise(thread, ThreadEvent.Update);
expect(thread).toHaveLength(0);
expect(thread.replyToEvent.getId()).toBe(threadRoot.getId());
});
});
describe("eventShouldLiveIn", () => {
const room = new Room(roomId, null, userA);
const mkMessage = () => utils.mkMessage({
event: true,
user: userA,
room: roomId,
}) as MatrixEvent;
const mkReply = (target: MatrixEvent) => utils.mkEvent({
event: true,
type: EventType.RoomMessage,
user: userA,
room: roomId,
content: {
"body": "Reply :: " + Math.random(),
"m.relates_to": {
"m.in_reply_to": {
"event_id": target.getId(),
},
},
},
}) as MatrixEvent;
const mkThreadResponse = (root: MatrixEvent) => utils.mkEvent({
event: true,
type: EventType.RoomMessage,
user: userA,
room: roomId,
content: {
"body": "Thread response :: " + Math.random(),
"m.relates_to": {
"event_id": root.getId(),
"m.in_reply_to": {
"event_id": root.getId(),
},
"rel_type": "m.thread",
},
},
}) as MatrixEvent;
const mkReaction = (target: MatrixEvent) => utils.mkEvent({
event: true,
type: EventType.Reaction,
user: userA,
room: roomId,
content: {
"m.relates_to": {
"rel_type": RelationType.Annotation,
"event_id": target.getId(),
"key": Math.random().toString(),
},
},
}) as MatrixEvent;
const mkRedaction = (target: MatrixEvent) => utils.mkEvent({
event: true,
type: EventType.RoomRedaction,
user: userA,
room: roomId,
redacts: target.getId(),
content: {},
}) as MatrixEvent;
const client = new TestClient(userA).client;
client.supportsExperimentalThreads = () => true;
const room = new Room(roomId, client, userA);
it("thread root and its relations&redactions should be in both", () => {
const randomMessage = mkMessage();
+11 -67
View File
@@ -3771,9 +3771,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
txnId = this.makeTxnId();
}
// we always construct a MatrixEvent when sending because the store and
// scheduler use them. We'll extract the params back out if it turns out
// the client has no scheduler or store.
// We always construct a MatrixEvent when sending because the store and scheduler use them.
// We'll extract the params back out if it turns out the client has no scheduler or store.
const localEvent = new MatrixEvent(Object.assign(eventObject, {
event_id: "~" + roomId + ":" + txnId,
user_id: this.credentials.userId,
@@ -3808,9 +3807,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
localEvent.setStatus(EventStatus.SENDING);
// add this event immediately to the local store as 'sending'.
if (room) {
room.addPendingEvent(localEvent, txnId);
}
room?.addPendingEvent(localEvent, txnId);
// addPendingEvent can change the state to NOT_SENT if it believes
// that there's other events that have failed. We won't bother to
@@ -5179,7 +5176,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
room.currentState.setUnknownStateEvents(stateEvents);
}
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(room, matrixEvents);
const [timelineEvents, threadedEvents] = room.partitionThreadedEvents(matrixEvents);
room.addEventsToTimeline(timelineEvents, true, room.getLiveTimeline());
await this.processThreadEvents(room, threadedEvents, true);
@@ -5281,7 +5278,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// functions contiguously, so we have to jump through some hoops to get our target event in it.
// XXX: workaround for https://github.com/vector-im/element-meta/issues/150
if (Thread.hasServerSideSupport && event.isRelation(THREAD_RELATION_TYPE.name)) {
const [, threadedEvents] = this.partitionThreadedEvents(timelineSet.room, events);
const [, threadedEvents] = timelineSet.room.partitionThreadedEvents(events);
const thread = await timelineSet.room.createThreadFetchRoot(event.threadRootId, threadedEvents, true);
let nextBatch: string;
@@ -5316,7 +5313,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
timeline.getState(EventTimeline.FORWARDS).paginationToken = res.end;
}
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(timelineSet.room, events);
const [timelineEvents, threadedEvents] = timelineSet.room.partitionThreadedEvents(events);
timelineSet.addEventsToTimeline(timelineEvents, true, timeline, res.start);
// The target event is not in a thread but process the contextual events, so we can show any threads around it.
await this.processThreadEvents(timelineSet.room, threadedEvents, true);
@@ -5446,10 +5443,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
matrixEvents[i] = event;
}
// No need to partition events for threads here, everything lives
// in the notification timeline set
const timelineSet = eventTimeline.getTimelineSet();
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(timelineSet.room, matrixEvents);
timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
await this.processThreadEvents(timelineSet.room, threadedEvents, backwards);
timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
// if we've hit the end of the timeline, we need to stop trying to
// paginate. We need to keep the 'forwards' token though, to make sure
@@ -5484,7 +5481,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const matrixEvents = res.chunk.map(this.getEventMapper());
const timelineSet = eventTimeline.getTimelineSet();
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(timelineSet.room, matrixEvents);
const [timelineEvents, threadedEvents] = timelineSet.room.partitionThreadedEvents(matrixEvents);
timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
await this.processThreadEvents(room, threadedEvents, backwards);
@@ -8852,57 +8849,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
});
}
/**
* Given some events, find the IDs of all the thread roots that are
* referred to by them.
*/
private findThreadRoots(events: MatrixEvent[]): Set<string> {
const threadRoots = new Set<string>();
for (const event of events) {
if (event.isThreadRelation) {
threadRoots.add(event.relationEventId);
}
}
return threadRoots;
}
public partitionThreadedEvents(room: Room, events: MatrixEvent[]): [
timelineEvents: MatrixEvent[],
threadedEvents: MatrixEvent[],
] {
// Indices to the events array, for readability
const ROOM = 0;
const THREAD = 1;
if (this.supportsExperimentalThreads()) {
const threadRoots = this.findThreadRoots(events);
return events.reduce((memo, event: MatrixEvent) => {
const {
shouldLiveInRoom,
shouldLiveInThread,
threadId,
} = room.eventShouldLiveIn(event, events, threadRoots);
if (shouldLiveInRoom) {
memo[ROOM].push(event);
}
if (shouldLiveInThread) {
event.setThreadId(threadId);
memo[THREAD].push(event);
}
return memo;
}, [[], []]);
} else {
// When `experimentalThreadSupport` is disabled
// treat all events as timelineEvents
return [
events,
[],
];
}
}
/**
* @experimental
*/
@@ -8911,9 +8857,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
threadedEvents: MatrixEvent[],
toStartOfTimeline: boolean,
): Promise<void> {
for (const event of threadedEvents) {
await room.addThreadedEvent(event, toStartOfTimeline);
}
await room.processThreadedEvents(threadedEvents, toStartOfTimeline);
}
/**
+6 -2
View File
@@ -45,8 +45,9 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
event.setUnsigned({ ...event.getUnsigned(), ...plainOldJsObject.unsigned });
}
if (room?.threads.has(event.getId())) {
event.setThread(room.threads.get(event.getId()));
const thread = room?.findThreadForEvent(event);
if (thread) {
event.setThread(thread);
}
if (event.isEncrypted()) {
@@ -65,6 +66,9 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
MatrixEventEvent.Replaced,
MatrixEventEvent.VisibilityChange,
]);
room?.reEmitter.reEmit(event, [
MatrixEventEvent.BeforeRedaction,
]);
}
return event;
}
+2 -2
View File
@@ -775,7 +775,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
}
public getAllRelationsEventForEvent(eventId: string): MatrixEvent[] {
const relationsForEvent = this.relations[eventId] || {};
const relationsForEvent = this.relations?.[eventId] || {};
const events = [];
for (const relationsRecord of Object.values(relationsForEvent)) {
for (const relations of Object.values(relationsRecord)) {
@@ -852,7 +852,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
}
let relationsWithEventType = relationsWithRelType[eventType];
let relatesToEvent;
let relatesToEvent: MatrixEvent;
if (!relationsWithEventType) {
relationsWithEventType = relationsWithRelType[eventType] = new Relations(
relationType,
-5
View File
@@ -119,11 +119,6 @@ export interface IEventRelation {
key?: string;
}
export interface IVisibilityEventRelation extends IEventRelation {
visibility: "visible" | "hidden";
reason?: string;
}
/**
* When an event is a visibility change event, as per MSC3531,
* the visibility change implied by the event.
+1 -1
View File
@@ -331,7 +331,7 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
// the all-knowning server tells us that the event at some point had
// this timestamp for its replacement, so any following replacement should definitely not be less
const replaceRelation = this.targetEvent.getServerAggregatedRelation<IAggregatedRelation>(RelationType.Replace);
const minTs = replaceRelation && replaceRelation.origin_server_ts;
const minTs = replaceRelation?.origin_server_ts;
const lastReplacement = this.getRelations().reduce((last, event) => {
if (event.getSender() !== this.targetEvent.getSender()) {
+157 -46
View File
@@ -22,8 +22,8 @@ import { EventTimelineSet, DuplicateStrategy } from "./event-timeline-set";
import { Direction, EventTimeline } from "./event-timeline";
import { getHttpUriForMxc } from "../content-repo";
import * as utils from "../utils";
import { normalize } from "../utils";
import { IEvent, IThreadBundledRelationship, MatrixEvent } from "./event";
import { defer, normalize } from "../utils";
import { IEvent, IThreadBundledRelationship, MatrixEvent, MatrixEventEvent, MatrixEventHandlerMap } from "./event";
import { EventStatus } from "./event-status";
import { RoomMember } from "./room-member";
import { IRoomSummary, RoomSummary } from "./room-summary";
@@ -171,7 +171,8 @@ type EmittedEvents = RoomEvent
| ThreadEvent.Update
| ThreadEvent.NewReply
| RoomEvent.Timeline
| RoomEvent.TimelineReset;
| RoomEvent.TimelineReset
| MatrixEventEvent.BeforeRedaction;
export type RoomEventHandlerMap = {
[RoomEvent.MyMembership]: (room: Room, membership: string, prevMembership?: string) => void;
@@ -188,10 +189,10 @@ export type RoomEventHandlerMap = {
oldStatus?: EventStatus,
) => void;
[ThreadEvent.New]: (thread: Thread, toStartOfTimeline: boolean) => void;
} & ThreadHandlerMap;
} & ThreadHandlerMap & MatrixEventHandlerMap;
export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap> {
private readonly reEmitter: TypedReEmitter<EmittedEvents, RoomEventHandlerMap>;
public readonly reEmitter: TypedReEmitter<EmittedEvents, RoomEventHandlerMap>;
private txnToEvent: Record<string, MatrixEvent> = {}; // Pending in-flight requests { string: MatrixEvent }
// receipts should clobber based on receipt_type and user_id pairs hence
// the form of this structure. This is sub-optimal for the exposed APIs
@@ -213,6 +214,8 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
private getTypeWarning = false;
private getVersionWarning = false;
private membersPromise?: Promise<boolean>;
// Map from threadId to pending Thread instance created by createThreadFetchRoot
private threadPromises = new Map<string, Promise<Thread>>();
// XXX: These should be read-only
/**
@@ -381,7 +384,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
return this.threadTimelineSetsPromise;
}
if (this.client?.supportsExperimentalThreads) {
if (this.client?.supportsExperimentalThreads()) {
try {
this.threadTimelineSetsPromise = Promise.all([
this.createThreadTimelineSet(),
@@ -1567,6 +1570,13 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
shouldLiveInThread: boolean;
threadId?: string;
} {
if (!this.client.supportsExperimentalThreads()) {
return {
shouldLiveInRoom: true,
shouldLiveInThread: false,
};
}
// A thread root is always shown in both timelines
if (event.isThreadRoot || roots?.has(event.getId())) {
return {
@@ -1581,7 +1591,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
return {
shouldLiveInRoom: false,
shouldLiveInThread: true,
threadId: event.relationEventId,
threadId: event.threadRootId,
};
}
@@ -1630,21 +1640,23 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
threadId: string,
events?: MatrixEvent[],
toStartOfTimeline?: boolean,
): Promise<Thread> {
): Promise<Thread | null> {
let thread = this.getThread(threadId);
if (!thread) {
const deferred = defer<Thread | null>();
this.threadPromises.set(threadId, deferred.promise);
let rootEvent = this.findEventById(threadId);
// If the rootEvent does not exist in the local stores, then fetch it from the server.
try {
const eventData = await this.client.fetchRoomEvent(this.roomId, threadId);
if (!rootEvent) {
rootEvent = new MatrixEvent(eventData);
} else {
rootEvent.setUnsigned(eventData.unsigned);
}
const mapper = this.client.getEventMapper();
rootEvent = mapper(eventData); // will merge with existing event object if such is known
} catch (e) {
logger.error("Failed to fetch thread root to construct thread with", e);
} finally {
this.threadPromises.delete(threadId);
// The root event might be not be visible to the person requesting it.
// If it wasn't fetched successfully the thread will work in "limited" mode and won't
// benefit from all the APIs a homeserver can provide to enhance the thread experience
@@ -1652,26 +1664,53 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
if (thread) {
rootEvent.setThread(thread);
}
deferred.resolve(thread);
}
}
return thread;
}
/**
* Add an event to a thread's timeline. Will fire "Thread.update"
* @experimental
*/
public async addThreadedEvent(event: MatrixEvent, toStartOfTimeline: boolean): Promise<void> {
this.applyRedaction(event);
let thread = this.findThreadForEvent(event);
if (thread) {
await thread.addEvent(event, toStartOfTimeline);
} else {
thread = await this.createThreadFetchRoot(event.threadRootId, [event], toStartOfTimeline);
private async addThreadedEvents(events: MatrixEvent[], threadId: string, toStartOfTimeline = false): Promise<void> {
let thread = this.getThread(threadId);
if (this.threadPromises.has(threadId)) {
thread = await this.threadPromises.get(threadId);
}
this.emit(ThreadEvent.Update, thread);
events = events.filter(e => e.getId() !== threadId); // filter out any root events
if (thread) {
for (const event of events) {
await thread.addEvent(event, toStartOfTimeline);
}
} else {
thread = await this.createThreadFetchRoot(threadId, events, toStartOfTimeline);
}
if (thread) {
this.emit(ThreadEvent.Update, thread);
}
}
/**
* Adds events to a thread's timeline. Will fire "Thread.update"
* @experimental
*/
public async processThreadedEvents(events: MatrixEvent[], toStartOfTimeline: boolean): Promise<unknown> {
events.forEach(this.applyRedaction);
const eventsByThread: { [threadId: string]: MatrixEvent[] } = {};
for (const event of events) {
const { threadId } = this.eventShouldLiveIn(event);
if (!eventsByThread[threadId]) {
eventsByThread[threadId] = [];
}
eventsByThread[threadId].push(event);
}
return Promise.all(Object.entries(eventsByThread).map(([threadId, events]) => (
this.addThreadedEvents(events, threadId, toStartOfTimeline)
)));
}
public createThread(
@@ -1728,7 +1767,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
}
}
private applyRedaction(event: MatrixEvent): void {
private applyRedaction = (event: MatrixEvent): void => {
if (event.isRedaction()) {
const redactId = event.event.redacts;
@@ -1738,7 +1777,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
redactedEvent.makeRedacted(event);
// If this is in the current state, replace it with the redacted version
if (redactedEvent.getStateKey()) {
if (redactedEvent.isState()) {
const currentStateEvent = this.currentState.getStateEvents(
redactedEvent.getType(),
redactedEvent.getStateKey(),
@@ -1772,19 +1811,9 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
// clients can say "so and so redacted an event" if they wish to. Also
// this may be needed to trigger an update.
}
}
};
/**
* Add an event to the end of this room's live timelines. Will fire
* "Room.timeline".
*
* @param {MatrixEvent} event Event to be added
* @param {string?} duplicateStrategy 'ignore' or 'replace'
* @param {boolean} fromCache whether the sync response came from cache
* @fires module:client~MatrixClient#event:"Room.timeline"
* @private
*/
private addLiveEvent(event: MatrixEvent, duplicateStrategy?: DuplicateStrategy, fromCache = false): void {
private processLiveEvent(event: MatrixEvent): void {
this.applyRedaction(event);
// Implement MSC3531: hiding messages.
@@ -1801,10 +1830,21 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
if (existingEvent) {
// remote echo of an event we sent earlier
this.handleRemoteEcho(event, existingEvent);
return;
}
}
}
/**
* Add an event to the end of this room's live timelines. Will fire
* "Room.timeline".
*
* @param {MatrixEvent} event Event to be added
* @param {string?} duplicateStrategy 'ignore' or 'replace'
* @param {boolean} fromCache whether the sync response came from cache
* @fires module:client~MatrixClient#event:"Room.timeline"
* @private
*/
private addLiveEvent(event: MatrixEvent, duplicateStrategy: DuplicateStrategy, fromCache = false): void {
// add to our timeline sets
for (let i = 0; i < this.timelineSets.length; i++) {
this.timelineSets[i].addLiveEvent(event, duplicateStrategy, fromCache);
@@ -1998,10 +2038,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
const newEventId = remoteEvent.getId();
const oldStatus = localEvent.status;
logger.debug(
`Got remote echo for event ${oldEventId} -> ${newEventId} ` +
`old status ${oldStatus}`,
);
logger.debug(`Got remote echo for event ${oldEventId} -> ${newEventId} old status ${oldStatus}`);
// no longer pending
delete this.txnToEvent[remoteEvent.getUnsigned().transaction_id];
@@ -2167,10 +2204,84 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
}
}
const threadRoots = this.findThreadRoots(events);
const threadInfos = events.map(e => this.eventShouldLiveIn(e, events, threadRoots));
const eventsByThread: { [threadId: string]: MatrixEvent[] } = {};
for (let i = 0; i < events.length; i++) {
// TODO: We should have a filter to say "only add state event types X Y Z to the timeline".
this.addLiveEvent(events[i], duplicateStrategy, fromCache);
this.processLiveEvent(events[i]);
const {
shouldLiveInRoom,
shouldLiveInThread,
threadId,
} = threadInfos[i];
if (shouldLiveInThread) {
if (!eventsByThread[threadId]) {
eventsByThread[threadId] = [];
}
eventsByThread[threadId].push(events[i]);
}
if (shouldLiveInRoom) {
this.addLiveEvent(events[i], duplicateStrategy, fromCache);
}
}
Object.entries(eventsByThread).forEach(([threadId, threadEvents]) => {
this.addThreadedEvents(threadEvents, threadId, false);
});
}
public partitionThreadedEvents(events: MatrixEvent[]): [
timelineEvents: MatrixEvent[],
threadedEvents: MatrixEvent[],
] {
// Indices to the events array, for readability
const ROOM = 0;
const THREAD = 1;
if (this.client.supportsExperimentalThreads()) {
const threadRoots = this.findThreadRoots(events);
return events.reduce((memo, event: MatrixEvent) => {
const {
shouldLiveInRoom,
shouldLiveInThread,
threadId,
} = this.eventShouldLiveIn(event, events, threadRoots);
if (shouldLiveInRoom) {
memo[ROOM].push(event);
}
if (shouldLiveInThread) {
event.setThreadId(threadId);
memo[THREAD].push(event);
}
return memo;
}, [[], []]);
} else {
// When `experimentalThreadSupport` is disabled treat all events as timelineEvents
return [
events,
[],
];
}
}
/**
* Given some events, find the IDs of all the thread roots that are referred to by them.
*/
private findThreadRoots(events: MatrixEvent[]): Set<string> {
const threadRoots = new Set<string>();
for (const event of events) {
if (event.isThreadRelation) {
threadRoots.add(event.relationEventId);
}
}
return threadRoots;
}
/**
+48 -37
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixClient, RelationType, RoomEvent } from "../matrix";
import { MatrixClient, MatrixEventEvent, RelationType, RoomEvent } from "../matrix";
import { TypedReEmitter } from "../ReEmitter";
import { IRelationsRequestOpts } from "../@types/requests";
import { IThreadBundledRelationship, MatrixEvent } from "./event";
@@ -94,15 +94,16 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
RoomEvent.TimelineReset,
]);
this.room.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction);
this.room.on(RoomEvent.LocalEchoUpdated, this.onEcho);
this.timelineSet.on(RoomEvent.Timeline, this.onEcho);
// 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
this.id = rootEvent?.getId() ?? opts?.initialEvents?.find(event => event.isThreadRelation)?.relationEventId;
this.initialiseThread(this.rootEvent);
opts?.initialEvents?.forEach(event => this.addEvent(event, false));
this.room.on(RoomEvent.LocalEchoUpdated, this.onEcho);
this.room.on(RoomEvent.Timeline, this.onEcho);
}
public static setServerSideSupport(hasServerSideSupport: boolean, useStable: boolean): void {
@@ -114,10 +115,50 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
}
}
private onEcho = (event: MatrixEvent) => {
if (this.timelineSet.eventIdToTimeline(event.getId())) {
private onBeforeRedaction = (event: MatrixEvent) => {
if (event?.isRelation(THREAD_RELATION_TYPE.name) &&
this.room.eventShouldLiveIn(event).threadId === this.id
) {
this.replyCount--;
this.emit(ThreadEvent.Update, this);
}
if (this.lastEvent?.getId() === event.getId()) {
const events = [...this.timelineSet.getLiveTimeline().getEvents()].reverse();
this.lastEvent = events.find(e => (
!e.isRedacted() &&
e.getId() !== event.getId() &&
e.isRelation(THREAD_RELATION_TYPE.name)
)) ?? this.rootEvent;
this.emit(ThreadEvent.NewReply, this, this.lastEvent);
}
};
private onEcho = (event: MatrixEvent) => {
if (event.threadRootId !== this.id) return; // ignore echoes for other timelines
if (this.lastEvent === event) return;
// There is a risk that the `localTimestamp` approximation will not be accurate
// when threads are used over federation. That could result in the reply
// count value drifting away from the value returned by the server
const isThreadReply = event.isRelation(THREAD_RELATION_TYPE.name);
if (!this.lastEvent || (isThreadReply
&& (event.getId() !== this.lastEvent.getId())
&& (event.localTimestamp > this.lastEvent.localTimestamp))
) {
this.lastEvent = event;
if (this.lastEvent.getId() !== this.id) {
// This counting only works when server side support is enabled as we started the counting
// from the value returned within the bundled relationship
if (Thread.hasServerSideSupport) {
this.replyCount++;
}
this.emit(ThreadEvent.NewReply, this, event);
}
}
this.emit(ThreadEvent.Update, this);
};
public get roomState(): RoomState {
@@ -125,15 +166,6 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
}
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,
@@ -177,33 +209,12 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
this._currentUserParticipated = true;
}
const isThreadReply = event.getRelation()?.rel_type === THREAD_RELATION_TYPE.name;
// If no thread support exists we want to count all thread relation
// added as a reply. We can't rely on the bundled relationships count
if (!Thread.hasServerSideSupport && isThreadReply) {
if (!Thread.hasServerSideSupport && event.isRelation(THREAD_RELATION_TYPE.name)) {
this.replyCount++;
}
// There is a risk that the `localTimestamp` approximation will not be accurate
// when threads are used over federation. That could results in the reply
// count value drifting away from the value returned by the server
if (!this.lastEvent || (isThreadReply
&& (event.getId() !== this.lastEvent.getId())
&& (event.localTimestamp > this.lastEvent.localTimestamp))
) {
this.lastEvent = event;
if (this.lastEvent.getId() !== this.id) {
// This counting only works when server side support is enabled
// as we started the counting from the value returned in the
// bundled relationship
if (Thread.hasServerSideSupport) {
this.replyCount++;
}
this.emit(ThreadEvent.NewReply, this, event);
}
}
this.emit(ThreadEvent.Update, this);
}
+1 -28
View File
@@ -1640,36 +1640,9 @@ export class SyncApi {
// if the timeline has any state events in it.
// This also needs to be done before running push rules on the events as they need
// to be decorated with sender etc.
const [mainTimelineEvents, threadedEvents] = this.client.partitionThreadedEvents(room, timelineEventList || []);
room.addLiveEvents(mainTimelineEvents, null, fromCache);
await this.processThreadEvents(room, threadedEvents, false);
room.addLiveEvents(timelineEventList || [], null, fromCache);
}
/**
* @experimental
*/
private processThreadEvents(
room: Room,
threadedEvents: MatrixEvent[],
toStartOfTimeline: boolean,
): Promise<void> {
return this.client.processThreadEvents(room, threadedEvents, toStartOfTimeline);
}
// extractRelatedEvents(event: MatrixEvent, events: MatrixEvent[], relatedEvents: MatrixEvent[] = []): MatrixEvent[] {
// relatedEvents.push(event);
// const parentEventId = event.getAssociatedId();
// const parentEventIndex = events.findIndex(event => event.getId() === parentEventId);
// if (parentEventIndex > -1) {
// const [relatedEvent] = events.splice(parentEventIndex, 1);
// return this.extractRelatedEvents(relatedEvent, events, relatedEvents);
// } else {
// return relatedEvents;
// }
// }
/**
* Takes a list of timelineEvents and adds and adds to notifEvents
* as appropriate.