You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-07-30 04:23:07 +03:00
Fix handling of threaded messages around edits & echoes (#2267)
This commit is contained in:
committed by
GitHub
parent
3322b47b6d
commit
dde4285cdf
@ -145,12 +145,14 @@ describe("MatrixClient", function() {
|
|||||||
describe("joinRoom", function() {
|
describe("joinRoom", function() {
|
||||||
it("should no-op if you've already joined a room", function() {
|
it("should no-op if you've already joined a room", function() {
|
||||||
const roomId = "!foo:bar";
|
const roomId = "!foo:bar";
|
||||||
const room = new Room(roomId, userId);
|
const room = new Room(roomId, client, userId);
|
||||||
|
client.fetchRoomEvent = () => Promise.resolve({});
|
||||||
room.addLiveEvents([
|
room.addLiveEvents([
|
||||||
utils.mkMembership({
|
utils.mkMembership({
|
||||||
user: userId, room: roomId, mship: "join", event: true,
|
user: userId, room: roomId, mship: "join", event: true,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
httpBackend.verifyNoOutstandingRequests();
|
||||||
store.storeRoom(room);
|
store.storeRoom(room);
|
||||||
client.joinRoom(roomId);
|
client.joinRoom(roomId);
|
||||||
httpBackend.verifyNoOutstandingRequests();
|
httpBackend.verifyNoOutstandingRequests();
|
||||||
@ -556,11 +558,14 @@ describe("MatrixClient", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("partitionThreadedEvents", 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() {
|
it("returns empty arrays when given an empty arrays", function() {
|
||||||
const events = [];
|
const events = [];
|
||||||
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
|
const [timeline, threaded] = room.partitionThreadedEvents(events);
|
||||||
expect(timeline).toEqual([]);
|
expect(timeline).toEqual([]);
|
||||||
expect(threaded).toEqual([]);
|
expect(threaded).toEqual([]);
|
||||||
});
|
});
|
||||||
@ -580,7 +585,7 @@ describe("MatrixClient", function() {
|
|||||||
// Vote has no threadId yet
|
// Vote has no threadId yet
|
||||||
expect(eventPollResponseReference.threadId).toBeFalsy();
|
expect(eventPollResponseReference.threadId).toBeFalsy();
|
||||||
|
|
||||||
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
|
const [timeline, threaded] = room.partitionThreadedEvents(events);
|
||||||
|
|
||||||
expect(timeline).toEqual([
|
expect(timeline).toEqual([
|
||||||
// The message that was sent in a thread is missing
|
// The message that was sent in a thread is missing
|
||||||
@ -613,7 +618,7 @@ describe("MatrixClient", function() {
|
|||||||
eventReaction,
|
eventReaction,
|
||||||
];
|
];
|
||||||
|
|
||||||
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
|
const [timeline, threaded] = room.partitionThreadedEvents(events);
|
||||||
|
|
||||||
expect(timeline).toEqual([
|
expect(timeline).toEqual([
|
||||||
eventPollStartThreadRoot,
|
eventPollStartThreadRoot,
|
||||||
@ -640,7 +645,7 @@ describe("MatrixClient", function() {
|
|||||||
eventMessageInThread,
|
eventMessageInThread,
|
||||||
];
|
];
|
||||||
|
|
||||||
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
|
const [timeline, threaded] = room.partitionThreadedEvents(events);
|
||||||
|
|
||||||
expect(timeline).toEqual([
|
expect(timeline).toEqual([
|
||||||
eventPollStartThreadRoot,
|
eventPollStartThreadRoot,
|
||||||
@ -667,7 +672,7 @@ describe("MatrixClient", function() {
|
|||||||
eventReaction,
|
eventReaction,
|
||||||
];
|
];
|
||||||
|
|
||||||
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
|
const [timeline, threaded] = room.partitionThreadedEvents(events);
|
||||||
|
|
||||||
expect(timeline).toEqual([
|
expect(timeline).toEqual([
|
||||||
eventPollStartThreadRoot,
|
eventPollStartThreadRoot,
|
||||||
@ -710,7 +715,7 @@ describe("MatrixClient", function() {
|
|||||||
eventMember,
|
eventMember,
|
||||||
eventCreate,
|
eventCreate,
|
||||||
];
|
];
|
||||||
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
|
const [timeline, threaded] = room.partitionThreadedEvents(events);
|
||||||
|
|
||||||
expect(timeline).toEqual([
|
expect(timeline).toEqual([
|
||||||
// The message that was sent in a thread is missing
|
// The message that was sent in a thread is missing
|
||||||
@ -749,7 +754,7 @@ describe("MatrixClient", function() {
|
|||||||
threadedReactionRedaction,
|
threadedReactionRedaction,
|
||||||
];
|
];
|
||||||
|
|
||||||
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
|
const [timeline, threaded] = room.partitionThreadedEvents(events);
|
||||||
|
|
||||||
expect(timeline).toEqual([
|
expect(timeline).toEqual([
|
||||||
threadRootEvent,
|
threadRootEvent,
|
||||||
@ -778,7 +783,7 @@ describe("MatrixClient", function() {
|
|||||||
replyToReply,
|
replyToReply,
|
||||||
];
|
];
|
||||||
|
|
||||||
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
|
const [timeline, threaded] = room.partitionThreadedEvents(events);
|
||||||
|
|
||||||
expect(timeline).toEqual([
|
expect(timeline).toEqual([
|
||||||
threadRootEvent,
|
threadRootEvent,
|
||||||
@ -805,7 +810,7 @@ describe("MatrixClient", function() {
|
|||||||
replyToThreadResponse,
|
replyToThreadResponse,
|
||||||
];
|
];
|
||||||
|
|
||||||
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
|
const [timeline, threaded] = room.partitionThreadedEvents(events);
|
||||||
|
|
||||||
expect(timeline).toEqual([
|
expect(timeline).toEqual([
|
||||||
threadRootEvent,
|
threadRootEvent,
|
||||||
|
@ -101,6 +101,7 @@ export function mkEvent(opts: IEventOpts): object | MatrixEvent {
|
|||||||
content: opts.content,
|
content: opts.content,
|
||||||
unsigned: opts.unsigned || {},
|
unsigned: opts.unsigned || {},
|
||||||
event_id: "$" + Math.random() + "-" + Math.random(),
|
event_id: "$" + Math.random() + "-" + Math.random(),
|
||||||
|
txn_id: "~" + Math.random(),
|
||||||
redacts: opts.redacts,
|
redacts: opts.redacts,
|
||||||
};
|
};
|
||||||
if (opts.skey !== undefined) {
|
if (opts.skey !== undefined) {
|
||||||
|
@ -981,7 +981,7 @@ describe("MatrixClient", function() {
|
|||||||
|
|
||||||
expect(rootEvent.isThreadRoot).toBe(true);
|
expect(rootEvent.isThreadRoot).toBe(true);
|
||||||
|
|
||||||
const [roomEvents, threadEvents] = client.partitionThreadedEvents(room, [rootEvent]);
|
const [roomEvents, threadEvents] = room.partitionThreadedEvents([rootEvent]);
|
||||||
expect(roomEvents).toHaveLength(1);
|
expect(roomEvents).toHaveLength(1);
|
||||||
expect(threadEvents).toHaveLength(1);
|
expect(threadEvents).toHaveLength(1);
|
||||||
|
|
||||||
|
@ -35,6 +35,8 @@ 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 { emitPromise } from "../test-utils/test-utils";
|
||||||
|
import { ThreadEvent } from "../../src/models/thread";
|
||||||
|
|
||||||
describe("Room", function() {
|
describe("Room", function() {
|
||||||
const roomId = "!foo:bar";
|
const roomId = "!foo:bar";
|
||||||
@ -44,8 +46,86 @@ describe("Room", function() {
|
|||||||
const userD = "@dorothy:bar";
|
const userD = "@dorothy:bar";
|
||||||
let room;
|
let room;
|
||||||
|
|
||||||
|
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 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(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}) 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;
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
room = new Room(roomId, null, userA);
|
room = new Room(roomId, new TestClient(userA, "device").client, userA);
|
||||||
// mock RoomStates
|
// mock RoomStates
|
||||||
room.oldState = room.getLiveTimeline().startState = utils.mock(RoomState, "oldState");
|
room.oldState = room.getLiveTimeline().startState = utils.mock(RoomState, "oldState");
|
||||||
room.currentState = room.getLiveTimeline().endState = utils.mock(RoomState, "currentState");
|
room.currentState = room.getLiveTimeline().endState = utils.mock(RoomState, "currentState");
|
||||||
@ -157,19 +237,18 @@ describe("Room", function() {
|
|||||||
expect(room.timeline[0]).toEqual(events[0]);
|
expect(room.timeline[0]).toEqual(events[0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should emit 'Room.timeline' events",
|
it("should emit 'Room.timeline' events", function() {
|
||||||
function() {
|
let callCount = 0;
|
||||||
let callCount = 0;
|
room.on("Room.timeline", function(event, emitRoom, toStart) {
|
||||||
room.on("Room.timeline", function(event, emitRoom, toStart) {
|
callCount += 1;
|
||||||
callCount += 1;
|
expect(room.timeline.length).toEqual(callCount);
|
||||||
expect(room.timeline.length).toEqual(callCount);
|
expect(event).toEqual(events[callCount - 1]);
|
||||||
expect(event).toEqual(events[callCount - 1]);
|
expect(emitRoom).toEqual(room);
|
||||||
expect(emitRoom).toEqual(room);
|
expect(toStart).toBeFalsy();
|
||||||
expect(toStart).toBeFalsy();
|
|
||||||
});
|
|
||||||
room.addLiveEvents(events);
|
|
||||||
expect(callCount).toEqual(2);
|
|
||||||
});
|
});
|
||||||
|
room.addLiveEvents(events);
|
||||||
|
expect(callCount).toEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
it("should call setStateEvents on the right RoomState with the right forwardLooking value for new events",
|
it("should call setStateEvents on the right RoomState with the right forwardLooking value for new events",
|
||||||
function() {
|
function() {
|
||||||
@ -338,42 +417,41 @@ describe("Room", function() {
|
|||||||
expect(oldEv.sender).toEqual(oldSentinel);
|
expect(oldEv.sender).toEqual(oldSentinel);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should set event.target for new and old m.room.member events",
|
it("should set event.target for new and old m.room.member events", function() {
|
||||||
function() {
|
const sentinel = {
|
||||||
const sentinel = {
|
userId: userA,
|
||||||
userId: userA,
|
membership: "join",
|
||||||
membership: "join",
|
name: "Alice",
|
||||||
name: "Alice",
|
};
|
||||||
};
|
const oldSentinel = {
|
||||||
const oldSentinel = {
|
userId: userA,
|
||||||
userId: userA,
|
membership: "join",
|
||||||
membership: "join",
|
name: "Old Alice",
|
||||||
name: "Old Alice",
|
};
|
||||||
};
|
room.currentState.getSentinelMember.mockImplementation(function(uid) {
|
||||||
room.currentState.getSentinelMember.mockImplementation(function(uid) {
|
if (uid === userA) {
|
||||||
if (uid === userA) {
|
return sentinel;
|
||||||
return sentinel;
|
}
|
||||||
}
|
return null;
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
|
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 " +
|
it("should call setStateEvents on the right RoomState with the right " +
|
||||||
"forwardLooking value for old events", function() {
|
"forwardLooking value for old events", function() {
|
||||||
@ -406,7 +484,7 @@ describe("Room", function() {
|
|||||||
let events = null;
|
let events = null;
|
||||||
|
|
||||||
beforeEach(function() {
|
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
|
// set events each time to avoid resusing Event objects (which
|
||||||
// doesn't work because they get frozen)
|
// doesn't work because they get frozen)
|
||||||
events = [
|
events = [
|
||||||
@ -469,8 +547,7 @@ describe("Room", function() {
|
|||||||
expect(callCount).toEqual(1);
|
expect(callCount).toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should " + (timelineSupport ? "remember" : "forget") +
|
it("should " + (timelineSupport ? "remember" : "forget") + " old timelines", function() {
|
||||||
" old timelines", function() {
|
|
||||||
room.addLiveEvents([events[0]]);
|
room.addLiveEvents([events[0]]);
|
||||||
expect(room.timeline.length).toEqual(1);
|
expect(room.timeline.length).toEqual(1);
|
||||||
const firstLiveTimeline = room.getLiveTimeline();
|
const firstLiveTimeline = room.getLiveTimeline();
|
||||||
@ -486,7 +563,7 @@ describe("Room", function() {
|
|||||||
|
|
||||||
describe("compareEventOrdering", function() {
|
describe("compareEventOrdering", function() {
|
||||||
beforeEach(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[] = [
|
const events: MatrixEvent[] = [
|
||||||
@ -673,7 +750,7 @@ describe("Room", function() {
|
|||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
// no mocking
|
// no mocking
|
||||||
room = new Room(roomId, null, userA);
|
room = new Room(roomId, new TestClient(userA).client, userA);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Room.recalculate => Stripped State Events", function() {
|
describe("Room.recalculate => Stripped State Events", function() {
|
||||||
@ -1259,6 +1336,7 @@ describe("Room", function() {
|
|||||||
const client = (new TestClient(
|
const client = (new TestClient(
|
||||||
"@alice:example.com", "alicedevice",
|
"@alice:example.com", "alicedevice",
|
||||||
)).client;
|
)).client;
|
||||||
|
client.supportsExperimentalThreads = () => true;
|
||||||
const room = new Room(roomId, client, userA, {
|
const room = new Room(roomId, client, userA, {
|
||||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||||
});
|
});
|
||||||
@ -1285,7 +1363,7 @@ describe("Room", function() {
|
|||||||
|
|
||||||
it("should add pending events to the timeline if " +
|
it("should add pending events to the timeline if " +
|
||||||
"pendingEventOrdering == 'chronological'", function() {
|
"pendingEventOrdering == 'chronological'", function() {
|
||||||
room = new Room(roomId, null, userA, {
|
const room = new Room(roomId, new TestClient(userA).client, userA, {
|
||||||
pendingEventOrdering: PendingEventOrdering.Chronological,
|
pendingEventOrdering: PendingEventOrdering.Chronological,
|
||||||
});
|
});
|
||||||
const eventA = utils.mkMessage({
|
const eventA = utils.mkMessage({
|
||||||
@ -1504,7 +1582,7 @@ describe("Room", function() {
|
|||||||
|
|
||||||
describe("guessDMUserId", function() {
|
describe("guessDMUserId", function() {
|
||||||
it("should return first hero id", 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({
|
room.setSummary({
|
||||||
'm.heroes': [userB],
|
'm.heroes': [userB],
|
||||||
'm.joined_member_count': 1,
|
'm.joined_member_count': 1,
|
||||||
@ -1513,7 +1591,7 @@ describe("Room", function() {
|
|||||||
expect(room.guessDMUserId()).toEqual(userB);
|
expect(room.guessDMUserId()).toEqual(userB);
|
||||||
});
|
});
|
||||||
it("should return first member that isn't self", function() {
|
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({
|
room.addLiveEvents([utils.mkMembership({
|
||||||
user: userB,
|
user: userB,
|
||||||
mship: "join",
|
mship: "join",
|
||||||
@ -1523,7 +1601,7 @@ describe("Room", function() {
|
|||||||
expect(room.guessDMUserId()).toEqual(userB);
|
expect(room.guessDMUserId()).toEqual(userB);
|
||||||
});
|
});
|
||||||
it("should return self if only member present", function() {
|
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);
|
expect(room.guessDMUserId()).toEqual(userA);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -1542,12 +1620,12 @@ describe("Room", function() {
|
|||||||
|
|
||||||
describe("getDefaultRoomName", function() {
|
describe("getDefaultRoomName", function() {
|
||||||
it("should return 'Empty room' if a user is the only member", 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");
|
expect(room.getDefaultRoomName(userA)).toEqual("Empty room");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return a display name if one other member is in the room", function() {
|
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([
|
room.addLiveEvents([
|
||||||
utils.mkMembership({
|
utils.mkMembership({
|
||||||
user: userA, mship: "join",
|
user: userA, mship: "join",
|
||||||
@ -1562,7 +1640,7 @@ describe("Room", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return a display name if one other member is banned", 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([
|
room.addLiveEvents([
|
||||||
utils.mkMembership({
|
utils.mkMembership({
|
||||||
user: userA, mship: "join",
|
user: userA, mship: "join",
|
||||||
@ -1577,7 +1655,7 @@ describe("Room", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return a display name if one other member is invited", 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([
|
room.addLiveEvents([
|
||||||
utils.mkMembership({
|
utils.mkMembership({
|
||||||
user: userA, mship: "join",
|
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() {
|
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([
|
room.addLiveEvents([
|
||||||
utils.mkMembership({
|
utils.mkMembership({
|
||||||
user: userA, mship: "join",
|
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() {
|
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([
|
room.addLiveEvents([
|
||||||
utils.mkMembership({
|
utils.mkMembership({
|
||||||
user: userA, mship: "join",
|
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() {
|
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([
|
room.addLiveEvents([
|
||||||
utils.mkMembership({
|
utils.mkMembership({
|
||||||
user: userA, mship: "join",
|
user: userA, mship: "join",
|
||||||
@ -1651,7 +1729,7 @@ describe("Room", function() {
|
|||||||
|
|
||||||
describe("io.element.functional_users", 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() {
|
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([
|
room.addLiveEvents([
|
||||||
utils.mkMembership({
|
utils.mkMembership({
|
||||||
user: userA, mship: "join",
|
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() {
|
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([
|
room.addLiveEvents([
|
||||||
utils.mkMembership({
|
utils.mkMembership({
|
||||||
user: userA, mship: "join",
|
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() {
|
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([
|
room.addLiveEvents([
|
||||||
utils.mkMembership({
|
utils.mkMembership({
|
||||||
user: userA, mship: "join",
|
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() {
|
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([
|
room.addLiveEvents([
|
||||||
utils.mkMembership({
|
utils.mkMembership({
|
||||||
user: userA, mship: "join",
|
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() {
|
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([
|
room.addLiveEvents([
|
||||||
utils.mkMembership({
|
utils.mkMembership({
|
||||||
user: userA, mship: "join",
|
user: userA, mship: "join",
|
||||||
@ -1767,7 +1845,7 @@ describe("Room", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return 'Empty room' if all other members are functional members", 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([
|
room.addLiveEvents([
|
||||||
utils.mkMembership({
|
utils.mkMembership({
|
||||||
user: userA, mship: "join",
|
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() {
|
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([
|
room.addLiveEvents([
|
||||||
utils.mkMembership({
|
utils.mkMembership({
|
||||||
user: userA, mship: "join",
|
user: userA, mship: "join",
|
||||||
@ -1858,71 +1936,51 @@ describe("Room", function() {
|
|||||||
|
|
||||||
expect(() => room.createThread(rootEvent, [])).not.toThrow();
|
expect(() => room.createThread(rootEvent, [])).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Edits update the lastReply event", async () => {
|
||||||
|
const client = (new TestClient(
|
||||||
|
"@alice:example.com", "alicedevice",
|
||||||
|
)).client;
|
||||||
|
client.supportsExperimentalThreads = () => true;
|
||||||
|
room = new Room(roomId, client, userA);
|
||||||
|
|
||||||
|
const randomMessage = mkMessage();
|
||||||
|
const threadRoot = mkMessage();
|
||||||
|
const threadResponse = mkThreadResponse(threadRoot);
|
||||||
|
threadResponse.localTimestamp += 1000;
|
||||||
|
const threadResponseEdit = mkEdit(threadResponse);
|
||||||
|
threadResponseEdit.localTimestamp += 2000;
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("eventShouldLiveIn", () => {
|
describe("eventShouldLiveIn", () => {
|
||||||
const room = new Room(roomId, null, userA);
|
const client = new TestClient(userA).client;
|
||||||
|
client.supportsExperimentalThreads = () => true;
|
||||||
const mkMessage = () => utils.mkMessage({
|
const room = new Room(roomId, client, userA);
|
||||||
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;
|
|
||||||
|
|
||||||
it("thread root and its relations&redactions should be in both", () => {
|
it("thread root and its relations&redactions should be in both", () => {
|
||||||
const randomMessage = mkMessage();
|
const randomMessage = mkMessage();
|
||||||
|
@ -3771,9 +3771,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
txnId = this.makeTxnId();
|
txnId = this.makeTxnId();
|
||||||
}
|
}
|
||||||
|
|
||||||
// we always construct a MatrixEvent when sending because the store and
|
// We always construct a MatrixEvent when sending because the store and scheduler use them.
|
||||||
// scheduler use them. We'll extract the params back out if it turns out
|
// We'll extract the params back out if it turns out the client has no scheduler or store.
|
||||||
// the client has no scheduler or store.
|
|
||||||
const localEvent = new MatrixEvent(Object.assign(eventObject, {
|
const localEvent = new MatrixEvent(Object.assign(eventObject, {
|
||||||
event_id: "~" + roomId + ":" + txnId,
|
event_id: "~" + roomId + ":" + txnId,
|
||||||
user_id: this.credentials.userId,
|
user_id: this.credentials.userId,
|
||||||
@ -3808,9 +3807,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
localEvent.setStatus(EventStatus.SENDING);
|
localEvent.setStatus(EventStatus.SENDING);
|
||||||
|
|
||||||
// add this event immediately to the local store as '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
|
// addPendingEvent can change the state to NOT_SENT if it believes
|
||||||
// that there's other events that have failed. We won't bother to
|
// 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);
|
room.currentState.setUnknownStateEvents(stateEvents);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(room, matrixEvents);
|
const [timelineEvents, threadedEvents] = room.partitionThreadedEvents(matrixEvents);
|
||||||
|
|
||||||
room.addEventsToTimeline(timelineEvents, true, room.getLiveTimeline());
|
room.addEventsToTimeline(timelineEvents, true, room.getLiveTimeline());
|
||||||
await this.processThreadEvents(room, threadedEvents, true);
|
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.
|
// 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
|
// XXX: workaround for https://github.com/vector-im/element-meta/issues/150
|
||||||
if (Thread.hasServerSideSupport && event.isRelation(THREAD_RELATION_TYPE.name)) {
|
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);
|
const thread = await timelineSet.room.createThreadFetchRoot(event.threadRootId, threadedEvents, true);
|
||||||
|
|
||||||
let nextBatch: string;
|
let nextBatch: string;
|
||||||
@ -5316,7 +5313,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
timeline.getState(EventTimeline.FORWARDS).paginationToken = res.end;
|
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);
|
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.
|
// 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);
|
await this.processThreadEvents(timelineSet.room, threadedEvents, true);
|
||||||
@ -5447,7 +5444,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
}
|
}
|
||||||
|
|
||||||
const timelineSet = eventTimeline.getTimelineSet();
|
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);
|
timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
|
||||||
await this.processThreadEvents(timelineSet.room, threadedEvents, backwards);
|
await this.processThreadEvents(timelineSet.room, threadedEvents, backwards);
|
||||||
|
|
||||||
@ -5484,7 +5481,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
const matrixEvents = res.chunk.map(this.getEventMapper());
|
const matrixEvents = res.chunk.map(this.getEventMapper());
|
||||||
|
|
||||||
const timelineSet = eventTimeline.getTimelineSet();
|
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);
|
timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
|
||||||
await this.processThreadEvents(room, threadedEvents, backwards);
|
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
|
* @experimental
|
||||||
*/
|
*/
|
||||||
@ -8911,9 +8857,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
threadedEvents: MatrixEvent[],
|
threadedEvents: MatrixEvent[],
|
||||||
toStartOfTimeline: boolean,
|
toStartOfTimeline: boolean,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
for (const event of threadedEvents) {
|
await room.processThreadedEvents(threadedEvents, toStartOfTimeline);
|
||||||
await room.addThreadedEvent(event, toStartOfTimeline);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -45,8 +45,9 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
|
|||||||
event.setUnsigned({ ...event.getUnsigned(), ...plainOldJsObject.unsigned });
|
event.setUnsigned({ ...event.getUnsigned(), ...plainOldJsObject.unsigned });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (room?.threads.has(event.getId())) {
|
const thread = room?.findThreadForEvent(event);
|
||||||
event.setThread(room.threads.get(event.getId()));
|
if (thread) {
|
||||||
|
event.setThread(thread);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.isEncrypted()) {
|
if (event.isEncrypted()) {
|
||||||
|
@ -775,7 +775,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getAllRelationsEventForEvent(eventId: string): MatrixEvent[] {
|
public getAllRelationsEventForEvent(eventId: string): MatrixEvent[] {
|
||||||
const relationsForEvent = this.relations[eventId] || {};
|
const relationsForEvent = this.relations?.[eventId] || {};
|
||||||
const events = [];
|
const events = [];
|
||||||
for (const relationsRecord of Object.values(relationsForEvent)) {
|
for (const relationsRecord of Object.values(relationsForEvent)) {
|
||||||
for (const relations of Object.values(relationsRecord)) {
|
for (const relations of Object.values(relationsRecord)) {
|
||||||
@ -852,7 +852,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
|||||||
}
|
}
|
||||||
let relationsWithEventType = relationsWithRelType[eventType];
|
let relationsWithEventType = relationsWithRelType[eventType];
|
||||||
|
|
||||||
let relatesToEvent;
|
let relatesToEvent: MatrixEvent;
|
||||||
if (!relationsWithEventType) {
|
if (!relationsWithEventType) {
|
||||||
relationsWithEventType = relationsWithRelType[eventType] = new Relations(
|
relationsWithEventType = relationsWithRelType[eventType] = new Relations(
|
||||||
relationType,
|
relationType,
|
||||||
|
@ -119,11 +119,6 @@ export interface IEventRelation {
|
|||||||
key?: string;
|
key?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IVisibilityEventRelation extends IEventRelation {
|
|
||||||
visibility: "visible" | "hidden";
|
|
||||||
reason?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When an event is a visibility change event, as per MSC3531,
|
* When an event is a visibility change event, as per MSC3531,
|
||||||
* the visibility change implied by the event.
|
* the visibility change implied by the event.
|
||||||
|
@ -331,7 +331,7 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
|
|||||||
// the all-knowning server tells us that the event at some point had
|
// 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
|
// this timestamp for its replacement, so any following replacement should definitely not be less
|
||||||
const replaceRelation = this.targetEvent.getServerAggregatedRelation<IAggregatedRelation>(RelationType.Replace);
|
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) => {
|
const lastReplacement = this.getRelations().reduce((last, event) => {
|
||||||
if (event.getSender() !== this.targetEvent.getSender()) {
|
if (event.getSender() !== this.targetEvent.getSender()) {
|
||||||
|
@ -22,7 +22,7 @@ import { EventTimelineSet, DuplicateStrategy } from "./event-timeline-set";
|
|||||||
import { Direction, EventTimeline } from "./event-timeline";
|
import { Direction, EventTimeline } from "./event-timeline";
|
||||||
import { getHttpUriForMxc } from "../content-repo";
|
import { getHttpUriForMxc } from "../content-repo";
|
||||||
import * as utils from "../utils";
|
import * as utils from "../utils";
|
||||||
import { normalize } from "../utils";
|
import { defer, normalize } from "../utils";
|
||||||
import { IEvent, IThreadBundledRelationship, MatrixEvent } from "./event";
|
import { IEvent, IThreadBundledRelationship, MatrixEvent } from "./event";
|
||||||
import { EventStatus } from "./event-status";
|
import { EventStatus } from "./event-status";
|
||||||
import { RoomMember } from "./room-member";
|
import { RoomMember } from "./room-member";
|
||||||
@ -213,6 +213,8 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
private getTypeWarning = false;
|
private getTypeWarning = false;
|
||||||
private getVersionWarning = false;
|
private getVersionWarning = false;
|
||||||
private membersPromise?: Promise<boolean>;
|
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
|
// XXX: These should be read-only
|
||||||
/**
|
/**
|
||||||
@ -1567,6 +1569,13 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
shouldLiveInThread: boolean;
|
shouldLiveInThread: boolean;
|
||||||
threadId?: string;
|
threadId?: string;
|
||||||
} {
|
} {
|
||||||
|
if (!this.client.supportsExperimentalThreads()) {
|
||||||
|
return {
|
||||||
|
shouldLiveInRoom: true,
|
||||||
|
shouldLiveInThread: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// A thread root is always shown in both timelines
|
// A thread root is always shown in both timelines
|
||||||
if (event.isThreadRoot || roots?.has(event.getId())) {
|
if (event.isThreadRoot || roots?.has(event.getId())) {
|
||||||
return {
|
return {
|
||||||
@ -1581,7 +1590,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
return {
|
return {
|
||||||
shouldLiveInRoom: false,
|
shouldLiveInRoom: false,
|
||||||
shouldLiveInThread: true,
|
shouldLiveInThread: true,
|
||||||
threadId: event.relationEventId,
|
threadId: event.threadRootId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1630,21 +1639,23 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
threadId: string,
|
threadId: string,
|
||||||
events?: MatrixEvent[],
|
events?: MatrixEvent[],
|
||||||
toStartOfTimeline?: boolean,
|
toStartOfTimeline?: boolean,
|
||||||
): Promise<Thread> {
|
): Promise<Thread | null> {
|
||||||
let thread = this.getThread(threadId);
|
let thread = this.getThread(threadId);
|
||||||
|
|
||||||
if (!thread) {
|
if (!thread) {
|
||||||
|
const deferred = defer<Thread | null>();
|
||||||
|
this.threadPromises.set(threadId, deferred.promise);
|
||||||
|
|
||||||
let rootEvent = this.findEventById(threadId);
|
let rootEvent = this.findEventById(threadId);
|
||||||
// If the rootEvent does not exist in the local stores, then fetch it from the server.
|
// If the rootEvent does not exist in the local stores, then fetch it from the server.
|
||||||
try {
|
try {
|
||||||
const eventData = await this.client.fetchRoomEvent(this.roomId, threadId);
|
const eventData = await this.client.fetchRoomEvent(this.roomId, threadId);
|
||||||
|
const mapper = this.client.getEventMapper();
|
||||||
if (!rootEvent) {
|
rootEvent = mapper(eventData); // will merge with existing event object if such is known
|
||||||
rootEvent = new MatrixEvent(eventData);
|
} catch (e) {
|
||||||
} else {
|
logger.error("Failed to fetch thread root to construct thread with", e);
|
||||||
rootEvent.setUnsigned(eventData.unsigned);
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
|
this.threadPromises.delete(threadId);
|
||||||
// The root event might be not be visible to the person requesting it.
|
// 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
|
// 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
|
// benefit from all the APIs a homeserver can provide to enhance the thread experience
|
||||||
@ -1652,26 +1663,51 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
if (thread) {
|
if (thread) {
|
||||||
rootEvent.setThread(thread);
|
rootEvent.setThread(thread);
|
||||||
}
|
}
|
||||||
|
deferred.resolve(thread);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return thread;
|
return thread;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private async addThreadedEvents(events: MatrixEvent[], threadId: string, toStartOfTimeline = false): Promise<void> {
|
||||||
* Add an event to a thread's timeline. Will fire "Thread.update"
|
let thread = this.getThread(threadId);
|
||||||
* @experimental
|
if (this.threadPromises.has(threadId)) {
|
||||||
*/
|
thread = await this.threadPromises.get(threadId);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.emit(ThreadEvent.Update, thread);
|
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(
|
public createThread(
|
||||||
@ -1728,7 +1764,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private 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;
|
||||||
|
|
||||||
@ -1738,7 +1774,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
redactedEvent.makeRedacted(event);
|
redactedEvent.makeRedacted(event);
|
||||||
|
|
||||||
// If this is in the current state, replace it with the redacted version
|
// If this is in the current state, replace it with the redacted version
|
||||||
if (redactedEvent.getStateKey()) {
|
if (redactedEvent.isState()) {
|
||||||
const currentStateEvent = this.currentState.getStateEvents(
|
const currentStateEvent = this.currentState.getStateEvents(
|
||||||
redactedEvent.getType(),
|
redactedEvent.getType(),
|
||||||
redactedEvent.getStateKey(),
|
redactedEvent.getStateKey(),
|
||||||
@ -1772,19 +1808,9 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
// clients can say "so and so redacted an event" if they wish to. Also
|
// clients can say "so and so redacted an event" if they wish to. Also
|
||||||
// this may be needed to trigger an update.
|
// this may be needed to trigger an update.
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
private processLiveEvent(event: MatrixEvent): Promise<void> {
|
||||||
* 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 {
|
|
||||||
this.applyRedaction(event);
|
this.applyRedaction(event);
|
||||||
|
|
||||||
// Implement MSC3531: hiding messages.
|
// Implement MSC3531: hiding messages.
|
||||||
@ -1804,7 +1830,19 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
return;
|
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
|
// add to our timeline sets
|
||||||
for (let i = 0; i < this.timelineSets.length; i++) {
|
for (let i = 0; i < this.timelineSets.length; i++) {
|
||||||
this.timelineSets[i].addLiveEvent(event, duplicateStrategy, fromCache);
|
this.timelineSets[i].addLiveEvent(event, duplicateStrategy, fromCache);
|
||||||
@ -1998,10 +2036,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
const newEventId = remoteEvent.getId();
|
const newEventId = remoteEvent.getId();
|
||||||
const oldStatus = localEvent.status;
|
const oldStatus = localEvent.status;
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(`Got remote echo for event ${oldEventId} -> ${newEventId} old status ${oldStatus}`);
|
||||||
`Got remote echo for event ${oldEventId} -> ${newEventId} ` +
|
|
||||||
`old status ${oldStatus}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// no longer pending
|
// no longer pending
|
||||||
delete this.txnToEvent[remoteEvent.getUnsigned().transaction_id];
|
delete this.txnToEvent[remoteEvent.getUnsigned().transaction_id];
|
||||||
@ -2167,10 +2202,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++) {
|
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".
|
// 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -94,15 +94,15 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
|||||||
RoomEvent.TimelineReset,
|
RoomEvent.TimelineReset,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
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,
|
// 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
|
||||||
this.id = rootEvent?.getId() ?? opts?.initialEvents?.find(event => event.isThreadRelation)?.relationEventId;
|
this.id = rootEvent?.getId() ?? opts?.initialEvents?.find(event => event.isThreadRelation)?.relationEventId;
|
||||||
this.initialiseThread(this.rootEvent);
|
this.initialiseThread(this.rootEvent);
|
||||||
|
|
||||||
opts?.initialEvents?.forEach(event => this.addEvent(event, false));
|
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 {
|
public static setServerSideSupport(hasServerSideSupport: boolean, useStable: boolean): void {
|
||||||
@ -115,6 +115,26 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private onEcho = (event: MatrixEvent) => {
|
private onEcho = (event: MatrixEvent) => {
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.timelineSet.eventIdToTimeline(event.getId())) {
|
if (this.timelineSet.eventIdToTimeline(event.getId())) {
|
||||||
this.emit(ThreadEvent.Update, this);
|
this.emit(ThreadEvent.Update, this);
|
||||||
}
|
}
|
||||||
@ -125,15 +145,6 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private addEventToTimeline(event: MatrixEvent, toStartOfTimeline: boolean): void {
|
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())) {
|
if (!this.findEventById(event.getId())) {
|
||||||
this.timelineSet.addEventToTimeline(
|
this.timelineSet.addEventToTimeline(
|
||||||
event,
|
event,
|
||||||
@ -177,33 +188,13 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
|||||||
this._currentUserParticipated = true;
|
this._currentUserParticipated = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isThreadReply = event.getRelation()?.rel_type === THREAD_RELATION_TYPE.name;
|
const isThreadReply = event.isRelation(THREAD_RELATION_TYPE.name);
|
||||||
// If no thread support exists we want to count all thread relation
|
// 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
|
// added as a reply. We can't rely on the bundled relationships count
|
||||||
if (!Thread.hasServerSideSupport && isThreadReply) {
|
if (!Thread.hasServerSideSupport && isThreadReply) {
|
||||||
this.replyCount++;
|
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);
|
this.emit(ThreadEvent.Update, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
29
src/sync.ts
29
src/sync.ts
@ -1635,36 +1635,9 @@ export class SyncApi {
|
|||||||
// if the timeline has any state events in it.
|
// 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
|
// This also needs to be done before running push rules on the events as they need
|
||||||
// to be decorated with sender etc.
|
// to be decorated with sender etc.
|
||||||
const [mainTimelineEvents, threadedEvents] = this.client.partitionThreadedEvents(room, timelineEventList || []);
|
room.addLiveEvents(timelineEventList || [], null, fromCache);
|
||||||
room.addLiveEvents(mainTimelineEvents, null, fromCache);
|
|
||||||
await this.processThreadEvents(room, threadedEvents, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @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
|
* Takes a list of timelineEvents and adds and adds to notifEvents
|
||||||
* as appropriate.
|
* as appropriate.
|
||||||
|
Reference in New Issue
Block a user