You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-07-31 15:24:23 +03:00
Fix thread & main timeline partitioning logic (#2264)
This commit is contained in:
committed by
GitHub
parent
4360ae7ff8
commit
d6f1c6cfdc
@ -556,9 +556,11 @@ describe("MatrixClient", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("partitionThreadedEvents", function() {
|
describe("partitionThreadedEvents", function() {
|
||||||
|
const 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(events);
|
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
|
||||||
expect(timeline).toEqual([]);
|
expect(timeline).toEqual([]);
|
||||||
expect(threaded).toEqual([]);
|
expect(threaded).toEqual([]);
|
||||||
});
|
});
|
||||||
@ -566,24 +568,24 @@ describe("MatrixClient", function() {
|
|||||||
it("copies pre-thread in-timeline vote events onto both timelines", function() {
|
it("copies pre-thread in-timeline vote events onto both timelines", function() {
|
||||||
client.clientOpts = { experimentalThreadSupport: true };
|
client.clientOpts = { experimentalThreadSupport: true };
|
||||||
|
|
||||||
const eventMessageInThread = buildEventMessageInThread();
|
|
||||||
const eventPollResponseReference = buildEventPollResponseReference();
|
const eventPollResponseReference = buildEventPollResponseReference();
|
||||||
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
||||||
|
const eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot);
|
||||||
|
|
||||||
const events = [
|
const events = [
|
||||||
|
eventPollStartThreadRoot,
|
||||||
eventMessageInThread,
|
eventMessageInThread,
|
||||||
eventPollResponseReference,
|
eventPollResponseReference,
|
||||||
eventPollStartThreadRoot,
|
|
||||||
];
|
];
|
||||||
// Vote has no threadId yet
|
// Vote has no threadId yet
|
||||||
expect(eventPollResponseReference.threadId).toBeFalsy();
|
expect(eventPollResponseReference.threadId).toBeFalsy();
|
||||||
|
|
||||||
const [timeline, threaded] = client.partitionThreadedEvents(events);
|
const [timeline, threaded] = client.partitionThreadedEvents(room, 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
|
||||||
eventPollResponseReference,
|
|
||||||
eventPollStartThreadRoot,
|
eventPollStartThreadRoot,
|
||||||
|
eventPollResponseReference,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// The vote event has been copied into the thread
|
// The vote event has been copied into the thread
|
||||||
@ -592,33 +594,34 @@ describe("MatrixClient", function() {
|
|||||||
expect(eventRefWithThreadId.threadId).toBeTruthy();
|
expect(eventRefWithThreadId.threadId).toBeTruthy();
|
||||||
|
|
||||||
expect(threaded).toEqual([
|
expect(threaded).toEqual([
|
||||||
|
eventPollStartThreadRoot,
|
||||||
eventMessageInThread,
|
eventMessageInThread,
|
||||||
eventRefWithThreadId,
|
eventRefWithThreadId,
|
||||||
// Thread does not see thread root
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("copies pre-thread in-timeline reactions onto both timelines", function() {
|
it("copies pre-thread in-timeline reactions onto both timelines", function() {
|
||||||
client.clientOpts = { experimentalThreadSupport: true };
|
client.clientOpts = { experimentalThreadSupport: true };
|
||||||
|
|
||||||
const eventMessageInThread = buildEventMessageInThread();
|
|
||||||
const eventReaction = buildEventReaction();
|
|
||||||
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
||||||
|
const eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot);
|
||||||
|
const eventReaction = buildEventReaction(eventPollStartThreadRoot);
|
||||||
|
|
||||||
const events = [
|
const events = [
|
||||||
|
eventPollStartThreadRoot,
|
||||||
eventMessageInThread,
|
eventMessageInThread,
|
||||||
eventReaction,
|
eventReaction,
|
||||||
eventPollStartThreadRoot,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const [timeline, threaded] = client.partitionThreadedEvents(events);
|
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
|
||||||
|
|
||||||
expect(timeline).toEqual([
|
expect(timeline).toEqual([
|
||||||
eventReaction,
|
|
||||||
eventPollStartThreadRoot,
|
eventPollStartThreadRoot,
|
||||||
|
eventReaction,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(threaded).toEqual([
|
expect(threaded).toEqual([
|
||||||
|
eventPollStartThreadRoot,
|
||||||
eventMessageInThread,
|
eventMessageInThread,
|
||||||
withThreadId(eventReaction, eventPollStartThreadRoot.getId()),
|
withThreadId(eventReaction, eventPollStartThreadRoot.getId()),
|
||||||
]);
|
]);
|
||||||
@ -628,23 +631,24 @@ describe("MatrixClient", function() {
|
|||||||
client.clientOpts = { experimentalThreadSupport: true };
|
client.clientOpts = { experimentalThreadSupport: true };
|
||||||
|
|
||||||
const eventPollResponseReference = buildEventPollResponseReference();
|
const eventPollResponseReference = buildEventPollResponseReference();
|
||||||
const eventMessageInThread = buildEventMessageInThread();
|
|
||||||
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
||||||
|
const eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot);
|
||||||
|
|
||||||
const events = [
|
const events = [
|
||||||
|
eventPollStartThreadRoot,
|
||||||
eventPollResponseReference,
|
eventPollResponseReference,
|
||||||
eventMessageInThread,
|
eventMessageInThread,
|
||||||
eventPollStartThreadRoot,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const [timeline, threaded] = client.partitionThreadedEvents(events);
|
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
|
||||||
|
|
||||||
expect(timeline).toEqual([
|
expect(timeline).toEqual([
|
||||||
eventPollResponseReference,
|
|
||||||
eventPollStartThreadRoot,
|
eventPollStartThreadRoot,
|
||||||
|
eventPollResponseReference,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(threaded).toEqual([
|
expect(threaded).toEqual([
|
||||||
|
eventPollStartThreadRoot,
|
||||||
withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()),
|
withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()),
|
||||||
eventMessageInThread,
|
eventMessageInThread,
|
||||||
]);
|
]);
|
||||||
@ -653,26 +657,27 @@ describe("MatrixClient", function() {
|
|||||||
it("copies post-thread in-timeline reactions onto both timelines", function() {
|
it("copies post-thread in-timeline reactions onto both timelines", function() {
|
||||||
client.clientOpts = { experimentalThreadSupport: true };
|
client.clientOpts = { experimentalThreadSupport: true };
|
||||||
|
|
||||||
const eventReaction = buildEventReaction();
|
|
||||||
const eventMessageInThread = buildEventMessageInThread();
|
|
||||||
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
||||||
|
const eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot);
|
||||||
|
const eventReaction = buildEventReaction(eventPollStartThreadRoot);
|
||||||
|
|
||||||
const events = [
|
const events = [
|
||||||
eventReaction,
|
|
||||||
eventMessageInThread,
|
|
||||||
eventPollStartThreadRoot,
|
eventPollStartThreadRoot,
|
||||||
|
eventMessageInThread,
|
||||||
|
eventReaction,
|
||||||
];
|
];
|
||||||
|
|
||||||
const [timeline, threaded] = client.partitionThreadedEvents(events);
|
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
|
||||||
|
|
||||||
expect(timeline).toEqual([
|
expect(timeline).toEqual([
|
||||||
eventReaction,
|
|
||||||
eventPollStartThreadRoot,
|
eventPollStartThreadRoot,
|
||||||
|
eventReaction,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(threaded).toEqual([
|
expect(threaded).toEqual([
|
||||||
withThreadId(eventReaction, eventPollStartThreadRoot.getId()),
|
eventPollStartThreadRoot,
|
||||||
eventMessageInThread,
|
eventMessageInThread,
|
||||||
|
withThreadId(eventReaction, eventPollStartThreadRoot.getId()),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -680,9 +685,9 @@ describe("MatrixClient", function() {
|
|||||||
client.clientOpts = { experimentalThreadSupport: true };
|
client.clientOpts = { experimentalThreadSupport: true };
|
||||||
// This is based on recording the events in a real room:
|
// This is based on recording the events in a real room:
|
||||||
|
|
||||||
const eventMessageInThread = buildEventMessageInThread();
|
|
||||||
const eventPollResponseReference = buildEventPollResponseReference();
|
|
||||||
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
||||||
|
const eventPollResponseReference = buildEventPollResponseReference();
|
||||||
|
const eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot);
|
||||||
const eventRoomName = buildEventRoomName();
|
const eventRoomName = buildEventRoomName();
|
||||||
const eventEncryption = buildEventEncryption();
|
const eventEncryption = buildEventEncryption();
|
||||||
const eventGuestAccess = buildEventGuestAccess();
|
const eventGuestAccess = buildEventGuestAccess();
|
||||||
@ -693,9 +698,9 @@ describe("MatrixClient", function() {
|
|||||||
const eventCreate = buildEventCreate();
|
const eventCreate = buildEventCreate();
|
||||||
|
|
||||||
const events = [
|
const events = [
|
||||||
eventMessageInThread,
|
|
||||||
eventPollResponseReference,
|
|
||||||
eventPollStartThreadRoot,
|
eventPollStartThreadRoot,
|
||||||
|
eventPollResponseReference,
|
||||||
|
eventMessageInThread,
|
||||||
eventRoomName,
|
eventRoomName,
|
||||||
eventEncryption,
|
eventEncryption,
|
||||||
eventGuestAccess,
|
eventGuestAccess,
|
||||||
@ -705,12 +710,12 @@ describe("MatrixClient", function() {
|
|||||||
eventMember,
|
eventMember,
|
||||||
eventCreate,
|
eventCreate,
|
||||||
];
|
];
|
||||||
const [timeline, threaded] = client.partitionThreadedEvents(events);
|
const [timeline, threaded] = client.partitionThreadedEvents(room, 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
|
||||||
eventPollResponseReference,
|
|
||||||
eventPollStartThreadRoot,
|
eventPollStartThreadRoot,
|
||||||
|
eventPollResponseReference,
|
||||||
eventRoomName,
|
eventRoomName,
|
||||||
eventEncryption,
|
eventEncryption,
|
||||||
eventGuestAccess,
|
eventGuestAccess,
|
||||||
@ -721,11 +726,95 @@ describe("MatrixClient", function() {
|
|||||||
eventCreate,
|
eventCreate,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Thread should contain only stuff that happened in the thread -
|
// Thread should contain only stuff that happened in the thread - no room state events
|
||||||
// no thread root, and no room state events
|
|
||||||
expect(threaded).toEqual([
|
expect(threaded).toEqual([
|
||||||
eventMessageInThread,
|
eventPollStartThreadRoot,
|
||||||
withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()),
|
withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()),
|
||||||
|
eventMessageInThread,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends redactions of reactions to thread responses to thread timeline only", () => {
|
||||||
|
client.clientOpts = { experimentalThreadSupport: true };
|
||||||
|
|
||||||
|
const threadRootEvent = buildEventPollStartThreadRoot();
|
||||||
|
const eventMessageInThread = buildEventMessageInThread(threadRootEvent);
|
||||||
|
const threadedReaction = buildEventReaction(eventMessageInThread);
|
||||||
|
const threadedReactionRedaction = buildEventRedaction(threadedReaction);
|
||||||
|
|
||||||
|
const events = [
|
||||||
|
threadRootEvent,
|
||||||
|
eventMessageInThread,
|
||||||
|
threadedReaction,
|
||||||
|
threadedReactionRedaction,
|
||||||
|
];
|
||||||
|
|
||||||
|
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
|
||||||
|
|
||||||
|
expect(timeline).toEqual([
|
||||||
|
threadRootEvent,
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(threaded).toEqual([
|
||||||
|
threadRootEvent,
|
||||||
|
eventMessageInThread,
|
||||||
|
threadedReaction,
|
||||||
|
threadedReactionRedaction,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends reply to reply to thread root outside of thread to main timeline only", () => {
|
||||||
|
client.clientOpts = { experimentalThreadSupport: true };
|
||||||
|
|
||||||
|
const threadRootEvent = buildEventPollStartThreadRoot();
|
||||||
|
const eventMessageInThread = buildEventMessageInThread(threadRootEvent);
|
||||||
|
const directReplyToThreadRoot = buildEventReply(threadRootEvent);
|
||||||
|
const replyToReply = buildEventReply(directReplyToThreadRoot);
|
||||||
|
|
||||||
|
const events = [
|
||||||
|
threadRootEvent,
|
||||||
|
eventMessageInThread,
|
||||||
|
directReplyToThreadRoot,
|
||||||
|
replyToReply,
|
||||||
|
];
|
||||||
|
|
||||||
|
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
|
||||||
|
|
||||||
|
expect(timeline).toEqual([
|
||||||
|
threadRootEvent,
|
||||||
|
directReplyToThreadRoot,
|
||||||
|
replyToReply,
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(threaded).toEqual([
|
||||||
|
threadRootEvent,
|
||||||
|
eventMessageInThread,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends reply to thread responses to thread timeline only", () => {
|
||||||
|
client.clientOpts = { experimentalThreadSupport: true };
|
||||||
|
|
||||||
|
const threadRootEvent = buildEventPollStartThreadRoot();
|
||||||
|
const eventMessageInThread = buildEventMessageInThread(threadRootEvent);
|
||||||
|
const replyToThreadResponse = buildEventReply(eventMessageInThread);
|
||||||
|
|
||||||
|
const events = [
|
||||||
|
threadRootEvent,
|
||||||
|
eventMessageInThread,
|
||||||
|
replyToThreadResponse,
|
||||||
|
];
|
||||||
|
|
||||||
|
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
|
||||||
|
|
||||||
|
expect(timeline).toEqual([
|
||||||
|
threadRootEvent,
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(threaded).toEqual([
|
||||||
|
threadRootEvent,
|
||||||
|
eventMessageInThread,
|
||||||
|
replyToThreadResponse,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -737,16 +826,16 @@ function withThreadId(event, newThreadId) {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildEventMessageInThread = () => new MatrixEvent({
|
const buildEventMessageInThread = (root) => new MatrixEvent({
|
||||||
"age": 80098509,
|
"age": 80098509,
|
||||||
"content": {
|
"content": {
|
||||||
"algorithm": "m.megolm.v1.aes-sha2",
|
"algorithm": "m.megolm.v1.aes-sha2",
|
||||||
"ciphertext": "ENCRYPTEDSTUFF",
|
"ciphertext": "ENCRYPTEDSTUFF",
|
||||||
"device_id": "XISFUZSKHH",
|
"device_id": "XISFUZSKHH",
|
||||||
"m.relates_to": {
|
"m.relates_to": {
|
||||||
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
|
"event_id": root.getId(),
|
||||||
"m.in_reply_to": {
|
"m.in_reply_to": {
|
||||||
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
|
"event_id": root.getId(),
|
||||||
},
|
},
|
||||||
"rel_type": "m.thread",
|
"rel_type": "m.thread",
|
||||||
},
|
},
|
||||||
@ -784,10 +873,10 @@ const buildEventPollResponseReference = () => new MatrixEvent({
|
|||||||
"user_id": "@andybalaam-test1:matrix.org",
|
"user_id": "@andybalaam-test1:matrix.org",
|
||||||
});
|
});
|
||||||
|
|
||||||
const buildEventReaction = () => new MatrixEvent({
|
const buildEventReaction = (event) => new MatrixEvent({
|
||||||
"content": {
|
"content": {
|
||||||
"m.relates_to": {
|
"m.relates_to": {
|
||||||
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
|
"event_id": event.getId(),
|
||||||
"key": "🤗",
|
"key": "🤗",
|
||||||
"rel_type": "m.annotation",
|
"rel_type": "m.annotation",
|
||||||
},
|
},
|
||||||
@ -803,6 +892,22 @@ const buildEventReaction = () => new MatrixEvent({
|
|||||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const buildEventRedaction = (event) => new MatrixEvent({
|
||||||
|
"content": {
|
||||||
|
|
||||||
|
},
|
||||||
|
"origin_server_ts": 1643977249239,
|
||||||
|
"sender": "@andybalaam-test1:matrix.org",
|
||||||
|
"redacts": event.getId(),
|
||||||
|
"type": "m.room.redaction",
|
||||||
|
"unsigned": {
|
||||||
|
"age": 22597,
|
||||||
|
"transaction_id": "m1643977249073.17",
|
||||||
|
},
|
||||||
|
"event_id": "$86B2b-x3LgE4DlV4y24b7UHnt72LIA3rzjvMysTtAfB",
|
||||||
|
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||||
|
});
|
||||||
|
|
||||||
const buildEventPollStartThreadRoot = () => new MatrixEvent({
|
const buildEventPollStartThreadRoot = () => new MatrixEvent({
|
||||||
"age": 80108647,
|
"age": 80108647,
|
||||||
"content": {
|
"content": {
|
||||||
@ -821,6 +926,29 @@ const buildEventPollStartThreadRoot = () => new MatrixEvent({
|
|||||||
"user_id": "@andybalaam-test1:matrix.org",
|
"user_id": "@andybalaam-test1:matrix.org",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const buildEventReply = (target) => new MatrixEvent({
|
||||||
|
"age": 80098509,
|
||||||
|
"content": {
|
||||||
|
"algorithm": "m.megolm.v1.aes-sha2",
|
||||||
|
"ciphertext": "ENCRYPTEDSTUFF",
|
||||||
|
"device_id": "XISFUZSKHH",
|
||||||
|
"m.relates_to": {
|
||||||
|
"m.in_reply_to": {
|
||||||
|
"event_id": target.getId(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
|
||||||
|
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
|
||||||
|
},
|
||||||
|
"event_id": target.getId() + Math.random(),
|
||||||
|
"origin_server_ts": 1643815466378,
|
||||||
|
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||||
|
"sender": "@andybalaam-test1:matrix.org",
|
||||||
|
"type": "m.room.encrypted",
|
||||||
|
"unsigned": { "age": 80098509 },
|
||||||
|
"user_id": "@andybalaam-test1:matrix.org",
|
||||||
|
});
|
||||||
|
|
||||||
const buildEventRoomName = () => new MatrixEvent({
|
const buildEventRoomName = () => new MatrixEvent({
|
||||||
"age": 80123249,
|
"age": 80123249,
|
||||||
"content": {
|
"content": {
|
||||||
|
@ -735,8 +735,7 @@ describe("MatrixClient syncing", function() {
|
|||||||
expect(tok).toEqual("pagTok");
|
expect(tok).toEqual("pagTok");
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// first flush the filter request; this will make syncLeftRooms
|
// first flush the filter request; this will make syncLeftRooms make its /sync call
|
||||||
// make its /sync call
|
|
||||||
httpBackend.flush("/filter").then(function() {
|
httpBackend.flush("/filter").then(function() {
|
||||||
return httpBackend.flushAllExpected();
|
return httpBackend.flushAllExpected();
|
||||||
}),
|
}),
|
||||||
|
@ -1,369 +0,0 @@
|
|||||||
// load olm before the sdk if possible
|
|
||||||
import '../olm-loader';
|
|
||||||
|
|
||||||
import { logger } from '../../src/logger';
|
|
||||||
import { MatrixEvent } from "../../src/models/event";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a promise that is resolved when the client next emits a
|
|
||||||
* SYNCING event.
|
|
||||||
* @param {Object} client The client
|
|
||||||
* @param {Number=} count Number of syncs to wait for (default 1)
|
|
||||||
* @return {Promise} Resolves once the client has emitted a SYNCING event
|
|
||||||
*/
|
|
||||||
export function syncPromise(client, count) {
|
|
||||||
if (count === undefined) {
|
|
||||||
count = 1;
|
|
||||||
}
|
|
||||||
if (count <= 0) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
const p = new Promise((resolve, reject) => {
|
|
||||||
const cb = (state) => {
|
|
||||||
logger.log(`${Date.now()} syncPromise(${count}): ${state}`);
|
|
||||||
if (state === 'SYNCING') {
|
|
||||||
resolve();
|
|
||||||
} else {
|
|
||||||
client.once('sync', cb);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
client.once('sync', cb);
|
|
||||||
});
|
|
||||||
|
|
||||||
return p.then(() => {
|
|
||||||
return syncPromise(client, count-1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a spy for an object and automatically spy its methods.
|
|
||||||
* @param {*} constr The class constructor (used with 'new')
|
|
||||||
* @param {string} name The name of the class
|
|
||||||
* @return {Object} An instantiated object with spied methods/properties.
|
|
||||||
*/
|
|
||||||
export function mock(constr, name) {
|
|
||||||
// Based on
|
|
||||||
// http://eclipsesource.com/blogs/2014/03/27/mocks-in-jasmine-tests/
|
|
||||||
const HelperConstr = new Function(); // jshint ignore:line
|
|
||||||
HelperConstr.prototype = constr.prototype;
|
|
||||||
const result = new HelperConstr();
|
|
||||||
result.toString = function() {
|
|
||||||
return "mock" + (name ? " of " + name : "");
|
|
||||||
};
|
|
||||||
for (const key of Object.getOwnPropertyNames(constr.prototype)) { // eslint-disable-line guard-for-in
|
|
||||||
try {
|
|
||||||
if (constr.prototype[key] instanceof Function) {
|
|
||||||
result[key] = jest.fn();
|
|
||||||
}
|
|
||||||
} catch (ex) {
|
|
||||||
// Direct access to some non-function fields of DOM prototypes may
|
|
||||||
// cause exceptions.
|
|
||||||
// Overwriting will not work either in that case.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an Event.
|
|
||||||
* @param {Object} opts Values for the event.
|
|
||||||
* @param {string} opts.type The event.type
|
|
||||||
* @param {string} opts.room The event.room_id
|
|
||||||
* @param {string} opts.sender The event.sender
|
|
||||||
* @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.
|
|
||||||
* @return {Object} a JSON object representing this event.
|
|
||||||
*/
|
|
||||||
export function mkEvent(opts) {
|
|
||||||
if (!opts.type || !opts.content) {
|
|
||||||
throw new Error("Missing .type or .content =>" + JSON.stringify(opts));
|
|
||||||
}
|
|
||||||
const event = {
|
|
||||||
type: opts.type,
|
|
||||||
room_id: opts.room,
|
|
||||||
sender: opts.sender || opts.user, // opts.user for backwards-compat
|
|
||||||
content: opts.content,
|
|
||||||
unsigned: opts.unsigned || {},
|
|
||||||
event_id: "$" + Math.random() + "-" + Math.random(),
|
|
||||||
};
|
|
||||||
if (opts.skey !== undefined) {
|
|
||||||
event.state_key = opts.skey;
|
|
||||||
} else if (["m.room.name", "m.room.topic", "m.room.create", "m.room.join_rules",
|
|
||||||
"m.room.power_levels", "m.room.topic",
|
|
||||||
"com.example.state"].includes(opts.type)) {
|
|
||||||
event.state_key = "";
|
|
||||||
}
|
|
||||||
return opts.event ? new MatrixEvent(event) : event;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an m.presence event.
|
|
||||||
* @param {Object} opts Values for the presence.
|
|
||||||
* @return {Object|MatrixEvent} The event
|
|
||||||
*/
|
|
||||||
export function mkPresence(opts) {
|
|
||||||
if (!opts.user) {
|
|
||||||
throw new Error("Missing user");
|
|
||||||
}
|
|
||||||
const event = {
|
|
||||||
event_id: "$" + Math.random() + "-" + Math.random(),
|
|
||||||
type: "m.presence",
|
|
||||||
sender: opts.sender || opts.user, // opts.user for backwards-compat
|
|
||||||
content: {
|
|
||||||
avatar_url: opts.url,
|
|
||||||
displayname: opts.name,
|
|
||||||
last_active_ago: opts.ago,
|
|
||||||
presence: opts.presence || "offline",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return opts.event ? new MatrixEvent(event) : event;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an m.room.member event.
|
|
||||||
* @param {Object} opts Values for the membership.
|
|
||||||
* @param {string} opts.room The room ID for the event.
|
|
||||||
* @param {string} opts.mship The content.membership for the event.
|
|
||||||
* @param {string} opts.sender The sender user ID for the event.
|
|
||||||
* @param {string} opts.skey The target user ID for the event if applicable
|
|
||||||
* e.g. for invites/bans.
|
|
||||||
* @param {string} opts.name The content.displayname for the event.
|
|
||||||
* @param {string} opts.url The content.avatar_url for the event.
|
|
||||||
* @param {boolean} opts.event True to make a MatrixEvent.
|
|
||||||
* @return {Object|MatrixEvent} The event
|
|
||||||
*/
|
|
||||||
export function mkMembership(opts) {
|
|
||||||
opts.type = "m.room.member";
|
|
||||||
if (!opts.skey) {
|
|
||||||
opts.skey = opts.sender || opts.user;
|
|
||||||
}
|
|
||||||
if (!opts.mship) {
|
|
||||||
throw new Error("Missing .mship => " + JSON.stringify(opts));
|
|
||||||
}
|
|
||||||
opts.content = {
|
|
||||||
membership: opts.mship,
|
|
||||||
};
|
|
||||||
if (opts.name) {
|
|
||||||
opts.content.displayname = opts.name;
|
|
||||||
}
|
|
||||||
if (opts.url) {
|
|
||||||
opts.content.avatar_url = opts.url;
|
|
||||||
}
|
|
||||||
return mkEvent(opts);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an m.room.message event.
|
|
||||||
* @param {Object} opts Values for the message
|
|
||||||
* @param {string} opts.room The room ID for the event.
|
|
||||||
* @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.
|
|
||||||
* @return {Object|MatrixEvent} The event
|
|
||||||
*/
|
|
||||||
export function mkMessage(opts) {
|
|
||||||
opts.type = "m.room.message";
|
|
||||||
if (!opts.msg) {
|
|
||||||
opts.msg = "Random->" + Math.random();
|
|
||||||
}
|
|
||||||
if (!opts.room || !opts.user) {
|
|
||||||
throw new Error("Missing .room or .user from %s", opts);
|
|
||||||
}
|
|
||||||
opts.content = {
|
|
||||||
msgtype: "m.text",
|
|
||||||
body: opts.msg,
|
|
||||||
};
|
|
||||||
return mkEvent(opts);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A mock implementation of webstorage
|
|
||||||
*
|
|
||||||
* @constructor
|
|
||||||
*/
|
|
||||||
export function MockStorageApi() {
|
|
||||||
this.data = {};
|
|
||||||
}
|
|
||||||
MockStorageApi.prototype = {
|
|
||||||
get length() {
|
|
||||||
return Object.keys(this.data).length;
|
|
||||||
},
|
|
||||||
key: function(i) {
|
|
||||||
return Object.keys(this.data)[i];
|
|
||||||
},
|
|
||||||
setItem: function(k, v) {
|
|
||||||
this.data[k] = v;
|
|
||||||
},
|
|
||||||
getItem: function(k) {
|
|
||||||
return this.data[k] || null;
|
|
||||||
},
|
|
||||||
removeItem: function(k) {
|
|
||||||
delete this.data[k];
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If an event is being decrypted, wait for it to finish being decrypted.
|
|
||||||
*
|
|
||||||
* @param {MatrixEvent} event
|
|
||||||
* @returns {Promise} promise which resolves (to `event`) when the event has been decrypted
|
|
||||||
*/
|
|
||||||
export function awaitDecryption(event) {
|
|
||||||
// An event is not always decrypted ahead of time
|
|
||||||
// getClearContent is a good signal to know whether an event has been decrypted
|
|
||||||
// already
|
|
||||||
if (event.getClearContent() !== null) {
|
|
||||||
return event;
|
|
||||||
} else {
|
|
||||||
logger.log(`${Date.now()} event ${event.getId()} is being decrypted; waiting`);
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
event.once('Event.decrypted', (ev) => {
|
|
||||||
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
|
|
||||||
resolve(ev);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function HttpResponse(
|
|
||||||
httpLookups, acceptKeepalives, ignoreUnhandledSync,
|
|
||||||
) {
|
|
||||||
this.httpLookups = httpLookups;
|
|
||||||
this.acceptKeepalives = acceptKeepalives === undefined ? true : acceptKeepalives;
|
|
||||||
this.ignoreUnhandledSync = ignoreUnhandledSync;
|
|
||||||
this.pendingLookup = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
HttpResponse.prototype.request = function(
|
|
||||||
cb, method, path, qp, data, prefix,
|
|
||||||
) {
|
|
||||||
if (path === HttpResponse.KEEP_ALIVE_PATH && this.acceptKeepalives) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
const next = this.httpLookups.shift();
|
|
||||||
const logLine = (
|
|
||||||
"MatrixClient[UT] RECV " + method + " " + path + " " +
|
|
||||||
"EXPECT " + (next ? next.method : next) + " " + (next ? next.path : next)
|
|
||||||
);
|
|
||||||
logger.log(logLine);
|
|
||||||
|
|
||||||
if (!next) { // no more things to return
|
|
||||||
if (method === "GET" && path === "/sync" && this.ignoreUnhandledSync) {
|
|
||||||
logger.log("MatrixClient[UT] Ignoring.");
|
|
||||||
return new Promise(() => {});
|
|
||||||
}
|
|
||||||
if (this.pendingLookup) {
|
|
||||||
if (this.pendingLookup.method === method
|
|
||||||
&& this.pendingLookup.path === path) {
|
|
||||||
return this.pendingLookup.promise;
|
|
||||||
}
|
|
||||||
// >1 pending thing, and they are different, whine.
|
|
||||||
expect(false).toBe(
|
|
||||||
true, ">1 pending request. You should probably handle them. " +
|
|
||||||
"PENDING: " + JSON.stringify(this.pendingLookup) + " JUST GOT: " +
|
|
||||||
method + " " + path,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.pendingLookup = {
|
|
||||||
promise: new Promise(() => {}),
|
|
||||||
method: method,
|
|
||||||
path: path,
|
|
||||||
};
|
|
||||||
return this.pendingLookup.promise;
|
|
||||||
}
|
|
||||||
if (next.path === path && next.method === method) {
|
|
||||||
logger.log(
|
|
||||||
"MatrixClient[UT] Matched. Returning " +
|
|
||||||
(next.error ? "BAD" : "GOOD") + " response",
|
|
||||||
);
|
|
||||||
if (next.expectBody) {
|
|
||||||
expect(next.expectBody).toEqual(data);
|
|
||||||
}
|
|
||||||
if (next.expectQueryParams) {
|
|
||||||
Object.keys(next.expectQueryParams).forEach(function(k) {
|
|
||||||
expect(qp[k]).toEqual(next.expectQueryParams[k]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (next.thenCall) {
|
|
||||||
process.nextTick(next.thenCall, 0); // next tick so we return first.
|
|
||||||
}
|
|
||||||
|
|
||||||
if (next.error) {
|
|
||||||
return Promise.reject({
|
|
||||||
errcode: next.error.errcode,
|
|
||||||
httpStatus: next.error.httpStatus,
|
|
||||||
name: next.error.errcode,
|
|
||||||
message: "Expected testing error",
|
|
||||||
data: next.error,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return Promise.resolve(next.data);
|
|
||||||
} else if (method === "GET" && path === "/sync" && this.ignoreUnhandledSync) {
|
|
||||||
logger.log("MatrixClient[UT] Ignoring.");
|
|
||||||
this.httpLookups.unshift(next);
|
|
||||||
return new Promise(() => {});
|
|
||||||
}
|
|
||||||
expect(true).toBe(false, "Expected different request. " + logLine);
|
|
||||||
return new Promise(() => {});
|
|
||||||
};
|
|
||||||
|
|
||||||
HttpResponse.KEEP_ALIVE_PATH = "/_matrix/client/versions";
|
|
||||||
|
|
||||||
HttpResponse.PUSH_RULES_RESPONSE = {
|
|
||||||
method: "GET",
|
|
||||||
path: "/pushrules/",
|
|
||||||
data: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
HttpResponse.PUSH_RULES_RESPONSE = {
|
|
||||||
method: "GET",
|
|
||||||
path: "/pushrules/",
|
|
||||||
data: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
HttpResponse.USER_ID = "@alice:bar";
|
|
||||||
|
|
||||||
HttpResponse.filterResponse = function(userId) {
|
|
||||||
const filterPath = "/user/" + encodeURIComponent(userId) + "/filter";
|
|
||||||
return {
|
|
||||||
method: "POST",
|
|
||||||
path: filterPath,
|
|
||||||
data: { filter_id: "f1lt3r" },
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
HttpResponse.SYNC_DATA = {
|
|
||||||
next_batch: "s_5_3",
|
|
||||||
presence: { events: [] },
|
|
||||||
rooms: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
HttpResponse.SYNC_RESPONSE = {
|
|
||||||
method: "GET",
|
|
||||||
path: "/sync",
|
|
||||||
data: HttpResponse.SYNC_DATA,
|
|
||||||
};
|
|
||||||
|
|
||||||
HttpResponse.defaultResponses = function(userId) {
|
|
||||||
return [
|
|
||||||
HttpResponse.PUSH_RULES_RESPONSE,
|
|
||||||
HttpResponse.filterResponse(userId),
|
|
||||||
HttpResponse.SYNC_RESPONSE,
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
export function setHttpResponses(
|
|
||||||
httpBackend, responses,
|
|
||||||
) {
|
|
||||||
responses.forEach(response => {
|
|
||||||
httpBackend
|
|
||||||
.when(response.method, response.path)
|
|
||||||
.respond(200, response.data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export const emitPromise = (e, k) => new Promise(r => e.once(k, r));
|
|
282
spec/test-utils/test-utils.ts
Normal file
282
spec/test-utils/test-utils.ts
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
// eslint-disable-next-line no-restricted-imports
|
||||||
|
import EventEmitter from "events";
|
||||||
|
|
||||||
|
// load olm before the sdk if possible
|
||||||
|
import '../olm-loader';
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a promise that is resolved when the client next emits a
|
||||||
|
* SYNCING event.
|
||||||
|
* @param {Object} client The client
|
||||||
|
* @param {Number=} count Number of syncs to wait for (default 1)
|
||||||
|
* @return {Promise} Resolves once the client has emitted a SYNCING event
|
||||||
|
*/
|
||||||
|
export function syncPromise(client: MatrixClient, count = 1): Promise<void> {
|
||||||
|
if (count <= 0) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
const p = new Promise<void>((resolve) => {
|
||||||
|
const cb = (state: SyncState) => {
|
||||||
|
logger.log(`${Date.now()} syncPromise(${count}): ${state}`);
|
||||||
|
if (state === SyncState.Syncing) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
client.once(ClientEvent.Sync, cb);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
client.once(ClientEvent.Sync, cb);
|
||||||
|
});
|
||||||
|
|
||||||
|
return p.then(() => {
|
||||||
|
return syncPromise(client, count - 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a spy for an object and automatically spy its methods.
|
||||||
|
* @param {*} constr The class constructor (used with 'new')
|
||||||
|
* @param {string} name The name of the class
|
||||||
|
* @return {Object} An instantiated object with spied methods/properties.
|
||||||
|
*/
|
||||||
|
export function mock<T>(constr: { new(...args: any[]): T }, name: string): T {
|
||||||
|
// Based on http://eclipsesource.com/blogs/2014/03/27/mocks-in-jasmine-tests/
|
||||||
|
const HelperConstr = new Function(); // jshint ignore:line
|
||||||
|
HelperConstr.prototype = constr.prototype;
|
||||||
|
// @ts-ignore
|
||||||
|
const result = new HelperConstr();
|
||||||
|
result.toString = function() {
|
||||||
|
return "mock" + (name ? " of " + name : "");
|
||||||
|
};
|
||||||
|
for (const key of Object.getOwnPropertyNames(constr.prototype)) { // eslint-disable-line guard-for-in
|
||||||
|
try {
|
||||||
|
if (constr.prototype[key] instanceof Function) {
|
||||||
|
result[key] = jest.fn();
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
// Direct access to some non-function fields of DOM prototypes may
|
||||||
|
// cause exceptions.
|
||||||
|
// Overwriting will not work either in that case.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IEventOpts {
|
||||||
|
type: EventType | string;
|
||||||
|
room: string;
|
||||||
|
sender?: string;
|
||||||
|
skey?: string;
|
||||||
|
content: IContent;
|
||||||
|
event?: boolean;
|
||||||
|
user?: string;
|
||||||
|
unsigned?: IUnsigned;
|
||||||
|
redacts?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an Event.
|
||||||
|
* @param {Object} opts Values for the event.
|
||||||
|
* @param {string} opts.type The event.type
|
||||||
|
* @param {string} opts.room The event.room_id
|
||||||
|
* @param {string} opts.sender The event.sender
|
||||||
|
* @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.
|
||||||
|
* @return {Object} a JSON object representing this event.
|
||||||
|
*/
|
||||||
|
export function mkEvent(opts: IEventOpts): object | MatrixEvent {
|
||||||
|
if (!opts.type || !opts.content) {
|
||||||
|
throw new Error("Missing .type or .content =>" + JSON.stringify(opts));
|
||||||
|
}
|
||||||
|
const event: Partial<IEvent> = {
|
||||||
|
type: opts.type as string,
|
||||||
|
room_id: opts.room,
|
||||||
|
sender: opts.sender || opts.user, // opts.user for backwards-compat
|
||||||
|
content: opts.content,
|
||||||
|
unsigned: opts.unsigned || {},
|
||||||
|
event_id: "$" + Math.random() + "-" + Math.random(),
|
||||||
|
redacts: opts.redacts,
|
||||||
|
};
|
||||||
|
if (opts.skey !== undefined) {
|
||||||
|
event.state_key = opts.skey;
|
||||||
|
} else if ([
|
||||||
|
EventType.RoomName,
|
||||||
|
EventType.RoomTopic,
|
||||||
|
EventType.RoomCreate,
|
||||||
|
EventType.RoomJoinRules,
|
||||||
|
EventType.RoomPowerLevels,
|
||||||
|
EventType.RoomTopic,
|
||||||
|
"com.example.state",
|
||||||
|
].includes(opts.type)) {
|
||||||
|
event.state_key = "";
|
||||||
|
}
|
||||||
|
return opts.event ? new MatrixEvent(event) : event;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPresenceOpts {
|
||||||
|
user?: string;
|
||||||
|
sender?: string;
|
||||||
|
url: string;
|
||||||
|
name: string;
|
||||||
|
ago: number;
|
||||||
|
presence?: string;
|
||||||
|
event?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an m.presence event.
|
||||||
|
* @param {Object} opts Values for the presence.
|
||||||
|
* @return {Object|MatrixEvent} The event
|
||||||
|
*/
|
||||||
|
export function mkPresence(opts: IPresenceOpts): object | MatrixEvent {
|
||||||
|
const event = {
|
||||||
|
event_id: "$" + Math.random() + "-" + Math.random(),
|
||||||
|
type: "m.presence",
|
||||||
|
sender: opts.sender || opts.user, // opts.user for backwards-compat
|
||||||
|
content: {
|
||||||
|
avatar_url: opts.url,
|
||||||
|
displayname: opts.name,
|
||||||
|
last_active_ago: opts.ago,
|
||||||
|
presence: opts.presence || "offline",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return opts.event ? new MatrixEvent(event) : event;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IMembershipOpts {
|
||||||
|
room: string;
|
||||||
|
mship: string;
|
||||||
|
sender?: string;
|
||||||
|
user?: string;
|
||||||
|
skey?: string;
|
||||||
|
name?: string;
|
||||||
|
url?: string;
|
||||||
|
event?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an m.room.member event.
|
||||||
|
* @param {Object} opts Values for the membership.
|
||||||
|
* @param {string} opts.room The room ID for the event.
|
||||||
|
* @param {string} opts.mship The content.membership for the event.
|
||||||
|
* @param {string} opts.sender The sender user ID for the event.
|
||||||
|
* @param {string} opts.skey The target user ID for the event if applicable
|
||||||
|
* e.g. for invites/bans.
|
||||||
|
* @param {string} opts.name The content.displayname for the event.
|
||||||
|
* @param {string} opts.url The content.avatar_url for the event.
|
||||||
|
* @param {boolean} opts.event True to make a MatrixEvent.
|
||||||
|
* @return {Object|MatrixEvent} The event
|
||||||
|
*/
|
||||||
|
export function mkMembership(opts: IMembershipOpts): object | MatrixEvent {
|
||||||
|
const eventOpts: IEventOpts = {
|
||||||
|
...opts,
|
||||||
|
type: EventType.RoomMember,
|
||||||
|
content: {
|
||||||
|
membership: opts.mship,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!opts.skey) {
|
||||||
|
eventOpts.skey = opts.sender || opts.user;
|
||||||
|
}
|
||||||
|
if (opts.name) {
|
||||||
|
eventOpts.content.displayname = opts.name;
|
||||||
|
}
|
||||||
|
if (opts.url) {
|
||||||
|
eventOpts.content.avatar_url = opts.url;
|
||||||
|
}
|
||||||
|
return mkEvent(eventOpts);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IMessageOpts {
|
||||||
|
room: string;
|
||||||
|
user: string;
|
||||||
|
msg?: string;
|
||||||
|
event?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an m.room.message event.
|
||||||
|
* @param {Object} opts Values for the message
|
||||||
|
* @param {string} opts.room The room ID for the event.
|
||||||
|
* @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.
|
||||||
|
* @return {Object|MatrixEvent} The event
|
||||||
|
*/
|
||||||
|
export function mkMessage(opts: IMessageOpts): object | MatrixEvent {
|
||||||
|
const eventOpts: IEventOpts = {
|
||||||
|
...opts,
|
||||||
|
type: EventType.RoomMessage,
|
||||||
|
content: {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: opts.msg,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!eventOpts.content.body) {
|
||||||
|
eventOpts.content.body = "Random->" + Math.random();
|
||||||
|
}
|
||||||
|
return mkEvent(eventOpts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A mock implementation of webstorage
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
export class MockStorageApi {
|
||||||
|
private data: Record<string, any> = {};
|
||||||
|
|
||||||
|
public get length() {
|
||||||
|
return Object.keys(this.data).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public key(i: number): any {
|
||||||
|
return Object.keys(this.data)[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
public setItem(k: string, v: any): void {
|
||||||
|
this.data[k] = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getItem(k: string): any {
|
||||||
|
return this.data[k] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeItem(k: string): void {
|
||||||
|
delete this.data[k];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If an event is being decrypted, wait for it to finish being decrypted.
|
||||||
|
*
|
||||||
|
* @param {MatrixEvent} event
|
||||||
|
* @returns {Promise} promise which resolves (to `event`) when the event has been decrypted
|
||||||
|
*/
|
||||||
|
export async function awaitDecryption(event: MatrixEvent): Promise<MatrixEvent> {
|
||||||
|
// An event is not always decrypted ahead of time
|
||||||
|
// getClearContent is a good signal to know whether an event has been decrypted
|
||||||
|
// already
|
||||||
|
if (event.getClearContent() !== null) {
|
||||||
|
return event;
|
||||||
|
} else {
|
||||||
|
logger.log(`${Date.now()} event ${event.getId()} is being decrypted; waiting`);
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
event.once(MatrixEventEvent.Decrypted, (ev) => {
|
||||||
|
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
|
||||||
|
resolve(ev);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const emitPromise = (e: EventEmitter, k: string): Promise<any> => new Promise(r => e.once(k, r));
|
@ -20,11 +20,33 @@ import anotherjson from 'another-json';
|
|||||||
|
|
||||||
import * as olmlib from "../../../src/crypto/olmlib";
|
import * as olmlib from "../../../src/crypto/olmlib";
|
||||||
import { TestClient } from '../../TestClient';
|
import { TestClient } from '../../TestClient';
|
||||||
import { HttpResponse, setHttpResponses } from '../../test-utils/test-utils';
|
|
||||||
import { resetCrossSigningKeys } from "./crypto-utils";
|
import { resetCrossSigningKeys } from "./crypto-utils";
|
||||||
import { MatrixError } from '../../../src/http-api';
|
import { MatrixError } from '../../../src/http-api';
|
||||||
import { logger } from '../../../src/logger';
|
import { logger } from '../../../src/logger';
|
||||||
|
|
||||||
|
const PUSH_RULES_RESPONSE = {
|
||||||
|
method: "GET",
|
||||||
|
path: "/pushrules/",
|
||||||
|
data: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterResponse = function(userId) {
|
||||||
|
const filterPath = "/user/" + encodeURIComponent(userId) + "/filter";
|
||||||
|
return {
|
||||||
|
method: "POST",
|
||||||
|
path: filterPath,
|
||||||
|
data: { filter_id: "f1lt3r" },
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function setHttpResponses(httpBackend, responses) {
|
||||||
|
responses.forEach(response => {
|
||||||
|
httpBackend
|
||||||
|
.when(response.method, response.path)
|
||||||
|
.respond(200, response.data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function makeTestClient(userInfo, options, keys) {
|
async function makeTestClient(userInfo, options, keys) {
|
||||||
if (!keys) keys = {};
|
if (!keys) keys = {};
|
||||||
|
|
||||||
@ -237,7 +259,7 @@ describe("Cross Signing", function() {
|
|||||||
|
|
||||||
// feed sync result that includes master key, ssk, device key
|
// feed sync result that includes master key, ssk, device key
|
||||||
const responses = [
|
const responses = [
|
||||||
HttpResponse.PUSH_RULES_RESPONSE,
|
PUSH_RULES_RESPONSE,
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
path: "/keys/upload",
|
path: "/keys/upload",
|
||||||
@ -248,7 +270,7 @@ describe("Cross Signing", function() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
HttpResponse.filterResponse("@alice:example.com"),
|
filterResponse("@alice:example.com"),
|
||||||
{
|
{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
path: "/sync",
|
path: "/sync",
|
||||||
@ -493,7 +515,7 @@ describe("Cross Signing", function() {
|
|||||||
// - master key signed by her usk (pretend that it was signed by another
|
// - master key signed by her usk (pretend that it was signed by another
|
||||||
// of Alice's devices)
|
// of Alice's devices)
|
||||||
const responses = [
|
const responses = [
|
||||||
HttpResponse.PUSH_RULES_RESPONSE,
|
PUSH_RULES_RESPONSE,
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
path: "/keys/upload",
|
path: "/keys/upload",
|
||||||
@ -504,7 +526,7 @@ describe("Cross Signing", function() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
HttpResponse.filterResponse("@alice:example.com"),
|
filterResponse("@alice:example.com"),
|
||||||
{
|
{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
path: "/sync",
|
path: "/sync",
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
MatrixEvent,
|
||||||
RelationType,
|
RelationType,
|
||||||
} from "../../src";
|
} from "../../src";
|
||||||
import { FilterComponent } from "../../src/filter-component";
|
import { FilterComponent } from "../../src/filter-component";
|
||||||
@ -13,7 +14,7 @@ describe("Filter Component", function() {
|
|||||||
content: { },
|
content: { },
|
||||||
room: 'roomId',
|
room: 'roomId',
|
||||||
event: true,
|
event: true,
|
||||||
});
|
}) as MatrixEvent;
|
||||||
|
|
||||||
const checkResult = filter.check(event);
|
const checkResult = filter.check(event);
|
||||||
|
|
||||||
@ -27,7 +28,7 @@ describe("Filter Component", function() {
|
|||||||
content: { },
|
content: { },
|
||||||
room: 'roomId',
|
room: 'roomId',
|
||||||
event: true,
|
event: true,
|
||||||
});
|
}) as MatrixEvent;
|
||||||
|
|
||||||
const checkResult = filter.check(event);
|
const checkResult = filter.check(event);
|
||||||
|
|
||||||
@ -54,7 +55,7 @@ describe("Filter Component", function() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
}) as MatrixEvent;
|
||||||
|
|
||||||
expect(filter.check(threadRootNotParticipated)).toBe(false);
|
expect(filter.check(threadRootNotParticipated)).toBe(false);
|
||||||
});
|
});
|
||||||
@ -79,7 +80,7 @@ describe("Filter Component", function() {
|
|||||||
user: '@someone-else:server.org',
|
user: '@someone-else:server.org',
|
||||||
room: 'roomId',
|
room: 'roomId',
|
||||||
event: true,
|
event: true,
|
||||||
});
|
}) as MatrixEvent;
|
||||||
|
|
||||||
expect(filter.check(threadRootParticipated)).toBe(true);
|
expect(filter.check(threadRootParticipated)).toBe(true);
|
||||||
});
|
});
|
||||||
@ -99,7 +100,7 @@ describe("Filter Component", function() {
|
|||||||
[RelationType.Reference]: {},
|
[RelationType.Reference]: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
}) as MatrixEvent;
|
||||||
|
|
||||||
expect(filter.check(referenceRelationEvent)).toBe(false);
|
expect(filter.check(referenceRelationEvent)).toBe(false);
|
||||||
});
|
});
|
||||||
@ -122,7 +123,7 @@ describe("Filter Component", function() {
|
|||||||
},
|
},
|
||||||
room: 'roomId',
|
room: 'roomId',
|
||||||
event: true,
|
event: true,
|
||||||
});
|
}) as MatrixEvent;
|
||||||
|
|
||||||
const eventWithMultipleRelations = mkEvent({
|
const eventWithMultipleRelations = mkEvent({
|
||||||
"type": "m.room.message",
|
"type": "m.room.message",
|
||||||
@ -147,7 +148,7 @@ describe("Filter Component", function() {
|
|||||||
},
|
},
|
||||||
"room": 'roomId',
|
"room": 'roomId',
|
||||||
"event": true,
|
"event": true,
|
||||||
});
|
}) as MatrixEvent;
|
||||||
|
|
||||||
const noMatchEvent = mkEvent({
|
const noMatchEvent = mkEvent({
|
||||||
"type": "m.room.message",
|
"type": "m.room.message",
|
||||||
@ -159,7 +160,7 @@ describe("Filter Component", function() {
|
|||||||
},
|
},
|
||||||
"room": 'roomId',
|
"room": 'roomId',
|
||||||
"event": true,
|
"event": true,
|
||||||
});
|
}) as MatrixEvent;
|
||||||
|
|
||||||
expect(filter.check(threadRootEvent)).toBe(true);
|
expect(filter.check(threadRootEvent)).toBe(true);
|
||||||
expect(filter.check(eventWithMultipleRelations)).toBe(true);
|
expect(filter.check(eventWithMultipleRelations)).toBe(true);
|
||||||
|
@ -32,6 +32,7 @@ import { Preset } from "../../src/@types/partials";
|
|||||||
import * as testUtils from "../test-utils/test-utils";
|
import * as testUtils from "../test-utils/test-utils";
|
||||||
import { makeBeaconInfoContent } from "../../src/content-helpers";
|
import { makeBeaconInfoContent } from "../../src/content-helpers";
|
||||||
import { M_BEACON_INFO } from "../../src/@types/beacon";
|
import { M_BEACON_INFO } from "../../src/@types/beacon";
|
||||||
|
import { Room } from "../../src";
|
||||||
|
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
|
|
||||||
@ -957,6 +958,7 @@ describe("MatrixClient", function() {
|
|||||||
it("partitions root events to room timeline and thread timeline", () => {
|
it("partitions root events to room timeline and thread timeline", () => {
|
||||||
const supportsExperimentalThreads = client.supportsExperimentalThreads;
|
const supportsExperimentalThreads = client.supportsExperimentalThreads;
|
||||||
client.supportsExperimentalThreads = () => true;
|
client.supportsExperimentalThreads = () => true;
|
||||||
|
const room = new Room("!room1:matrix.org", client, userId);
|
||||||
|
|
||||||
const rootEvent = new MatrixEvent({
|
const rootEvent = new MatrixEvent({
|
||||||
"content": {},
|
"content": {},
|
||||||
@ -979,9 +981,9 @@ describe("MatrixClient", function() {
|
|||||||
|
|
||||||
expect(rootEvent.isThreadRoot).toBe(true);
|
expect(rootEvent.isThreadRoot).toBe(true);
|
||||||
|
|
||||||
const [room, threads] = client.partitionThreadedEvents([rootEvent]);
|
const [roomEvents, threadEvents] = client.partitionThreadedEvents(room, [rootEvent]);
|
||||||
expect(room).toHaveLength(1);
|
expect(roomEvents).toHaveLength(1);
|
||||||
expect(threads).toHaveLength(1);
|
expect(threadEvents).toHaveLength(1);
|
||||||
|
|
||||||
// Restore method
|
// Restore method
|
||||||
client.supportsExperimentalThreads = supportsExperimentalThreads;
|
client.supportsExperimentalThreads = supportsExperimentalThreads;
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -5179,7 +5179,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
room.currentState.setUnknownStateEvents(stateEvents);
|
room.currentState.setUnknownStateEvents(stateEvents);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(matrixEvents);
|
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(room, 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 +5281,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(events);
|
const [, threadedEvents] = this.partitionThreadedEvents(timelineSet.room, 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 +5316,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(events);
|
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(timelineSet.room, 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);
|
||||||
@ -5446,9 +5446,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
matrixEvents[i] = event;
|
matrixEvents[i] = event;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(matrixEvents);
|
|
||||||
|
|
||||||
const timelineSet = eventTimeline.getTimelineSet();
|
const timelineSet = eventTimeline.getTimelineSet();
|
||||||
|
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(timelineSet.room, 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,10 +5483,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
const token = res.end;
|
const token = res.end;
|
||||||
const matrixEvents = res.chunk.map(this.getEventMapper());
|
const matrixEvents = res.chunk.map(this.getEventMapper());
|
||||||
|
|
||||||
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(matrixEvents);
|
const timelineSet = eventTimeline.getTimelineSet();
|
||||||
|
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(timelineSet.room, matrixEvents);
|
||||||
eventTimeline.getTimelineSet()
|
timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
|
||||||
.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
|
|
||||||
await this.processThreadEvents(room, threadedEvents, backwards);
|
await this.processThreadEvents(room, threadedEvents, backwards);
|
||||||
|
|
||||||
// if we've hit the end of the timeline, we need to stop trying to
|
// if we've hit the end of the timeline, we need to stop trying to
|
||||||
@ -8868,60 +8866,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
return threadRoots;
|
return threadRoots;
|
||||||
}
|
}
|
||||||
|
|
||||||
private eventShouldLiveIn(event: MatrixEvent, room: Room, events: MatrixEvent[], roots: Set<string>): {
|
public partitionThreadedEvents(room: Room, events: MatrixEvent[]): [
|
||||||
shouldLiveInRoom: boolean;
|
|
||||||
shouldLiveInThread: boolean;
|
|
||||||
threadId?: string;
|
|
||||||
} {
|
|
||||||
if (event.isThreadRoot) {
|
|
||||||
return {
|
|
||||||
shouldLiveInRoom: true,
|
|
||||||
shouldLiveInThread: true,
|
|
||||||
threadId: event.getId(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// A thread relation is always only shown in a thread
|
|
||||||
if (event.isThreadRelation) {
|
|
||||||
return {
|
|
||||||
shouldLiveInRoom: false,
|
|
||||||
shouldLiveInThread: true,
|
|
||||||
threadId: event.relationEventId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const parentEventId = event.getAssociatedId();
|
|
||||||
const parentEvent = room?.findEventById(parentEventId) ?? events.find((mxEv: MatrixEvent) => (
|
|
||||||
mxEv.getId() === parentEventId
|
|
||||||
));
|
|
||||||
|
|
||||||
// A reaction targeting the thread root needs to be routed to both the main timeline and the associated thread
|
|
||||||
const targetingThreadRoot = parentEvent?.isThreadRoot || roots.has(event.relationEventId);
|
|
||||||
if (targetingThreadRoot) {
|
|
||||||
return {
|
|
||||||
shouldLiveInRoom: true,
|
|
||||||
shouldLiveInThread: true,
|
|
||||||
threadId: event.relationEventId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the parent event also has an associated ID we want to re-run the
|
|
||||||
// computation for that parent event.
|
|
||||||
// In the case of the redaction of a reaction that targets a root event
|
|
||||||
// we want that redaction to be pushed to both timeline
|
|
||||||
if (parentEvent?.getAssociatedId()) {
|
|
||||||
return this.eventShouldLiveIn(parentEvent, room, events, roots);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We've exhausted all scenarios, can safely assume that this event
|
|
||||||
// should live in the room timeline
|
|
||||||
return {
|
|
||||||
shouldLiveInRoom: true,
|
|
||||||
shouldLiveInThread: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public partitionThreadedEvents(events: MatrixEvent[]): [
|
|
||||||
timelineEvents: MatrixEvent[],
|
timelineEvents: MatrixEvent[],
|
||||||
threadedEvents: MatrixEvent[],
|
threadedEvents: MatrixEvent[],
|
||||||
] {
|
] {
|
||||||
@ -8931,13 +8876,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
if (this.supportsExperimentalThreads()) {
|
if (this.supportsExperimentalThreads()) {
|
||||||
const threadRoots = this.findThreadRoots(events);
|
const threadRoots = this.findThreadRoots(events);
|
||||||
return events.reduce((memo, event: MatrixEvent) => {
|
return events.reduce((memo, event: MatrixEvent) => {
|
||||||
const room = this.getRoom(event.getRoomId());
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
shouldLiveInRoom,
|
shouldLiveInRoom,
|
||||||
shouldLiveInThread,
|
shouldLiveInThread,
|
||||||
threadId,
|
threadId,
|
||||||
} = this.eventShouldLiveIn(event, room, events, threadRoots);
|
} = room.eventShouldLiveIn(event, events, threadRoots);
|
||||||
|
|
||||||
if (shouldLiveInRoom) {
|
if (shouldLiveInRoom) {
|
||||||
memo[ROOM].push(event);
|
memo[ROOM].push(event);
|
||||||
|
@ -1562,19 +1562,68 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public findThreadForEvent(event: MatrixEvent): Thread | null {
|
public eventShouldLiveIn(event: MatrixEvent, events?: MatrixEvent[], roots?: Set<string>): {
|
||||||
if (!event) {
|
shouldLiveInRoom: boolean;
|
||||||
return null;
|
shouldLiveInThread: boolean;
|
||||||
|
threadId?: string;
|
||||||
|
} {
|
||||||
|
// A thread root is always shown in both timelines
|
||||||
|
if (event.isThreadRoot || roots?.has(event.getId())) {
|
||||||
|
return {
|
||||||
|
shouldLiveInRoom: true,
|
||||||
|
shouldLiveInThread: true,
|
||||||
|
threadId: event.getId(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A thread relation is always only shown in a thread
|
||||||
if (event.isThreadRelation) {
|
if (event.isThreadRelation) {
|
||||||
return this.threads.get(event.threadRootId);
|
return {
|
||||||
} else if (event.isThreadRoot) {
|
shouldLiveInRoom: false,
|
||||||
return this.threads.get(event.getId());
|
shouldLiveInThread: true,
|
||||||
} else {
|
threadId: event.relationEventId,
|
||||||
const parentEvent = this.findEventById(event.getAssociatedId());
|
};
|
||||||
return this.findThreadForEvent(parentEvent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const parentEventId = event.getAssociatedId();
|
||||||
|
const parentEvent = this.findEventById(parentEventId) ?? events?.find(e => e.getId() === parentEventId);
|
||||||
|
|
||||||
|
// Treat relations and redactions as extensions of their parents so evaluate parentEvent instead
|
||||||
|
if (parentEvent && (event.isRelation() || event.isRedaction())) {
|
||||||
|
return this.eventShouldLiveIn(parentEvent, events, roots);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edge case where we know the event is a relation but don't have the parentEvent
|
||||||
|
if (roots?.has(event.relationEventId)) {
|
||||||
|
return {
|
||||||
|
shouldLiveInRoom: true,
|
||||||
|
shouldLiveInThread: true,
|
||||||
|
threadId: event.relationEventId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// A reply directly to a thread response is shown as part of the thread only, this is to provide a better
|
||||||
|
// experience when communicating with users using clients without full threads support
|
||||||
|
if (parentEvent?.isThreadRelation) {
|
||||||
|
return {
|
||||||
|
shouldLiveInRoom: false,
|
||||||
|
shouldLiveInThread: true,
|
||||||
|
threadId: parentEvent.threadRootId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// We've exhausted all scenarios, can safely assume that this event should live in the room timeline only
|
||||||
|
return {
|
||||||
|
shouldLiveInRoom: true,
|
||||||
|
shouldLiveInThread: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public findThreadForEvent(event?: MatrixEvent): Thread | null {
|
||||||
|
if (!event) return null;
|
||||||
|
|
||||||
|
const { threadId } = this.eventShouldLiveIn(event);
|
||||||
|
return threadId ? this.getThread(threadId) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createThreadFetchRoot(
|
public async createThreadFetchRoot(
|
||||||
@ -1895,14 +1944,6 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private shouldAddEventToMainTimeline(thread: Thread, event: MatrixEvent): boolean {
|
|
||||||
if (!thread) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return !event.isThreadRelation && thread.id === event.getAssociatedId();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to aggregate the local echo for a relation, and also
|
* Used to aggregate the local echo for a relation, and also
|
||||||
* for re-applying a relation after it's redaction has been cancelled,
|
* for re-applying a relation after it's redaction has been cancelled,
|
||||||
@ -1914,10 +1955,11 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
* @param {module:models/event.MatrixEvent} event the relation event that needs to be aggregated.
|
* @param {module:models/event.MatrixEvent} event the relation event that needs to be aggregated.
|
||||||
*/
|
*/
|
||||||
private aggregateNonLiveRelation(event: MatrixEvent): void {
|
private aggregateNonLiveRelation(event: MatrixEvent): void {
|
||||||
const thread = this.findThreadForEvent(event);
|
const { shouldLiveInRoom, threadId } = this.eventShouldLiveIn(event);
|
||||||
|
const thread = this.getThread(threadId);
|
||||||
thread?.timelineSet.aggregateRelations(event);
|
thread?.timelineSet.aggregateRelations(event);
|
||||||
|
|
||||||
if (this.shouldAddEventToMainTimeline(thread, event)) {
|
if (shouldLiveInRoom) {
|
||||||
// TODO: We should consider whether this means it would be a better
|
// TODO: We should consider whether this means it would be a better
|
||||||
// design to lift the relations handling up to the room instead.
|
// design to lift the relations handling up to the room instead.
|
||||||
for (let i = 0; i < this.timelineSets.length; i++) {
|
for (let i = 0; i < this.timelineSets.length; i++) {
|
||||||
@ -1973,10 +2015,11 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
// any, which is good, because we don't want to try decoding it again).
|
// any, which is good, because we don't want to try decoding it again).
|
||||||
localEvent.handleRemoteEcho(remoteEvent.event);
|
localEvent.handleRemoteEcho(remoteEvent.event);
|
||||||
|
|
||||||
const thread = this.findThreadForEvent(remoteEvent);
|
const { shouldLiveInRoom, threadId } = this.eventShouldLiveIn(remoteEvent);
|
||||||
|
const thread = this.getThread(threadId);
|
||||||
thread?.timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId);
|
thread?.timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId);
|
||||||
|
|
||||||
if (this.shouldAddEventToMainTimeline(thread, remoteEvent)) {
|
if (shouldLiveInRoom) {
|
||||||
for (let i = 0; i < this.timelineSets.length; i++) {
|
for (let i = 0; i < this.timelineSets.length; i++) {
|
||||||
const timelineSet = this.timelineSets[i];
|
const timelineSet = this.timelineSets[i];
|
||||||
|
|
||||||
@ -2042,10 +2085,11 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
// update the event id
|
// update the event id
|
||||||
event.replaceLocalEventId(newEventId);
|
event.replaceLocalEventId(newEventId);
|
||||||
|
|
||||||
const thread = this.findThreadForEvent(event);
|
const { shouldLiveInRoom, threadId } = this.eventShouldLiveIn(event);
|
||||||
|
const thread = this.getThread(threadId);
|
||||||
thread?.timelineSet.replaceEventId(oldEventId, newEventId);
|
thread?.timelineSet.replaceEventId(oldEventId, newEventId);
|
||||||
|
|
||||||
if (this.shouldAddEventToMainTimeline(thread, event)) {
|
if (shouldLiveInRoom) {
|
||||||
// if the event was already in the timeline (which will be the case if
|
// if the event was already in the timeline (which will be the case if
|
||||||
// opts.pendingEventOrdering==chronological), we need to update the
|
// opts.pendingEventOrdering==chronological), we need to update the
|
||||||
// timeline map.
|
// timeline map.
|
||||||
@ -2105,13 +2149,12 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
* @throws If <code>duplicateStrategy</code> is not falsey, 'replace' or 'ignore'.
|
* @throws If <code>duplicateStrategy</code> is not falsey, 'replace' or 'ignore'.
|
||||||
*/
|
*/
|
||||||
public addLiveEvents(events: MatrixEvent[], duplicateStrategy?: DuplicateStrategy, fromCache = false): void {
|
public addLiveEvents(events: MatrixEvent[], duplicateStrategy?: DuplicateStrategy, fromCache = false): void {
|
||||||
let i;
|
|
||||||
if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) {
|
if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) {
|
||||||
throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'");
|
throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'");
|
||||||
}
|
}
|
||||||
|
|
||||||
// sanity check that the live timeline is still live
|
// sanity check that the live timeline is still live
|
||||||
for (i = 0; i < this.timelineSets.length; i++) {
|
for (let i = 0; i < this.timelineSets.length; i++) {
|
||||||
const liveTimeline = this.timelineSets[i].getLiveTimeline();
|
const liveTimeline = this.timelineSets[i].getLiveTimeline();
|
||||||
if (liveTimeline.getPaginationToken(EventTimeline.FORWARDS)) {
|
if (liveTimeline.getPaginationToken(EventTimeline.FORWARDS)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@ -2120,21 +2163,13 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (liveTimeline.getNeighbouringTimeline(EventTimeline.FORWARDS)) {
|
if (liveTimeline.getNeighbouringTimeline(EventTimeline.FORWARDS)) {
|
||||||
throw new Error(
|
throw new Error(`live timeline ${i} is no longer live - it has a neighbouring timeline`);
|
||||||
"live timeline " + i + " is no longer live - " +
|
|
||||||
"it has a neighbouring timeline",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (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
|
// TODO: We should have a filter to say "only add state event types X Y Z to the timeline".
|
||||||
// types X Y Z to the timeline".
|
|
||||||
this.addLiveEvent(events[i], duplicateStrategy, fromCache);
|
this.addLiveEvent(events[i], duplicateStrategy, fromCache);
|
||||||
const thread = this.findThreadForEvent(events[i]);
|
|
||||||
if (thread) {
|
|
||||||
thread.addEvent(events[i], true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
62
src/sync.ts
62
src/sync.ts
@ -276,15 +276,13 @@ export class SyncApi {
|
|||||||
return client.http.authedRequest<any>( // TODO types
|
return client.http.authedRequest<any>( // TODO types
|
||||||
undefined, Method.Get, "/sync", qps as any, undefined, localTimeoutMs,
|
undefined, Method.Get, "/sync", qps as any, undefined, localTimeoutMs,
|
||||||
);
|
);
|
||||||
}).then((data) => {
|
}).then(async (data) => {
|
||||||
let leaveRooms = [];
|
let leaveRooms = [];
|
||||||
if (data.rooms?.leave) {
|
if (data.rooms?.leave) {
|
||||||
leaveRooms = this.mapSyncResponseToRoomArray(data.rooms.leave);
|
leaveRooms = this.mapSyncResponseToRoomArray(data.rooms.leave);
|
||||||
}
|
}
|
||||||
const rooms = [];
|
return Promise.all(leaveRooms.map(async (leaveObj) => {
|
||||||
leaveRooms.forEach(async (leaveObj) => {
|
|
||||||
const room = leaveObj.room;
|
const room = leaveObj.room;
|
||||||
rooms.push(room);
|
|
||||||
if (!leaveObj.isBrandNewRoom) {
|
if (!leaveObj.isBrandNewRoom) {
|
||||||
// the intention behind syncLeftRooms is to add in rooms which were
|
// the intention behind syncLeftRooms is to add in rooms which were
|
||||||
// *omitted* from the initial /sync. Rooms the user were joined to
|
// *omitted* from the initial /sync. Rooms the user were joined to
|
||||||
@ -298,25 +296,22 @@ export class SyncApi {
|
|||||||
}
|
}
|
||||||
leaveObj.timeline = leaveObj.timeline || {};
|
leaveObj.timeline = leaveObj.timeline || {};
|
||||||
const events = this.mapSyncEventsFormat(leaveObj.timeline, room);
|
const events = this.mapSyncEventsFormat(leaveObj.timeline, room);
|
||||||
const [timelineEvents, threadedEvents] = this.client.partitionThreadedEvents(events);
|
|
||||||
|
|
||||||
const stateEvents = this.mapSyncEventsFormat(leaveObj.state, room);
|
const stateEvents = this.mapSyncEventsFormat(leaveObj.state, room);
|
||||||
|
|
||||||
// set the back-pagination token. Do this *before* adding any
|
// set the back-pagination token. Do this *before* adding any
|
||||||
// events so that clients can start back-paginating.
|
// events so that clients can start back-paginating.
|
||||||
room.getLiveTimeline().setPaginationToken(leaveObj.timeline.prev_batch,
|
room.getLiveTimeline().setPaginationToken(leaveObj.timeline.prev_batch, EventTimeline.BACKWARDS);
|
||||||
EventTimeline.BACKWARDS);
|
|
||||||
|
|
||||||
this.processRoomEvents(room, stateEvents, timelineEvents);
|
await this.processRoomEvents(room, stateEvents, events);
|
||||||
await this.processThreadEvents(room, threadedEvents, false);
|
|
||||||
|
|
||||||
room.recalculate();
|
room.recalculate();
|
||||||
client.store.storeRoom(room);
|
client.store.storeRoom(room);
|
||||||
client.emit(ClientEvent.Room, room);
|
client.emit(ClientEvent.Room, room);
|
||||||
|
|
||||||
this.processEventsForNotifs(room, events);
|
this.processEventsForNotifs(room, events);
|
||||||
});
|
return room;
|
||||||
return rooms;
|
}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -759,7 +754,7 @@ export class SyncApi {
|
|||||||
try {
|
try {
|
||||||
await this.processSyncResponse(syncEventData, data);
|
await this.processSyncResponse(syncEventData, data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error("Error processing cached sync", e.stack || e);
|
logger.error("Error processing cached sync", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't emit a prepared if we've bailed because the store is invalid:
|
// Don't emit a prepared if we've bailed because the store is invalid:
|
||||||
@ -834,7 +829,7 @@ export class SyncApi {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
// log the exception with stack if we have it, else fall back
|
// log the exception with stack if we have it, else fall back
|
||||||
// to the plain description
|
// to the plain description
|
||||||
logger.error("Caught /sync error", e.stack || e);
|
logger.error("Caught /sync error", e);
|
||||||
|
|
||||||
// Emit the exception for client handling
|
// Emit the exception for client handling
|
||||||
this.client.emit(ClientEvent.SyncUnexpectedError, e);
|
this.client.emit(ClientEvent.SyncUnexpectedError, e);
|
||||||
@ -1087,9 +1082,7 @@ export class SyncApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handle to-device events
|
// handle to-device events
|
||||||
if (data.to_device && Array.isArray(data.to_device.events) &&
|
if (Array.isArray(data.to_device?.events) && data.to_device.events.length > 0) {
|
||||||
data.to_device.events.length > 0
|
|
||||||
) {
|
|
||||||
const cancelledKeyVerificationTxns = [];
|
const cancelledKeyVerificationTxns = [];
|
||||||
data.to_device.events
|
data.to_device.events
|
||||||
.map(client.getEventMapper())
|
.map(client.getEventMapper())
|
||||||
@ -1163,11 +1156,11 @@ export class SyncApi {
|
|||||||
this.notifEvents = [];
|
this.notifEvents = [];
|
||||||
|
|
||||||
// Handle invites
|
// Handle invites
|
||||||
inviteRooms.forEach((inviteObj) => {
|
await utils.promiseMapSeries(inviteRooms, async (inviteObj) => {
|
||||||
const room = inviteObj.room;
|
const room = inviteObj.room;
|
||||||
const stateEvents = this.mapSyncEventsFormat(inviteObj.invite_state, room);
|
const stateEvents = this.mapSyncEventsFormat(inviteObj.invite_state, room);
|
||||||
|
|
||||||
this.processRoomEvents(room, stateEvents);
|
await this.processRoomEvents(room, stateEvents);
|
||||||
if (inviteObj.isBrandNewRoom) {
|
if (inviteObj.isBrandNewRoom) {
|
||||||
room.recalculate();
|
room.recalculate();
|
||||||
client.store.storeRoom(room);
|
client.store.storeRoom(room);
|
||||||
@ -1274,10 +1267,7 @@ export class SyncApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [timelineEvents, threadedEvents] = this.client.partitionThreadedEvents(events);
|
await this.processRoomEvents(room, stateEvents, events, syncEventData.fromCache);
|
||||||
|
|
||||||
this.processRoomEvents(room, stateEvents, timelineEvents, syncEventData.fromCache);
|
|
||||||
await this.processThreadEvents(room, threadedEvents, false);
|
|
||||||
|
|
||||||
// set summary after processing events,
|
// set summary after processing events,
|
||||||
// because it will trigger a name calculation
|
// because it will trigger a name calculation
|
||||||
@ -1318,8 +1308,7 @@ export class SyncApi {
|
|||||||
};
|
};
|
||||||
|
|
||||||
await utils.promiseMapSeries(stateEvents, processRoomEvent);
|
await utils.promiseMapSeries(stateEvents, processRoomEvent);
|
||||||
await utils.promiseMapSeries(timelineEvents, processRoomEvent);
|
await utils.promiseMapSeries(events, processRoomEvent);
|
||||||
await utils.promiseMapSeries(threadedEvents, processRoomEvent);
|
|
||||||
ephemeralEvents.forEach(function(e) {
|
ephemeralEvents.forEach(function(e) {
|
||||||
client.emit(ClientEvent.Event, e);
|
client.emit(ClientEvent.Event, e);
|
||||||
});
|
});
|
||||||
@ -1336,16 +1325,13 @@ export class SyncApi {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Handle leaves (e.g. kicked rooms)
|
// Handle leaves (e.g. kicked rooms)
|
||||||
leaveRooms.forEach(async (leaveObj) => {
|
await utils.promiseMapSeries(leaveRooms, async (leaveObj) => {
|
||||||
const room = leaveObj.room;
|
const room = leaveObj.room;
|
||||||
const stateEvents = this.mapSyncEventsFormat(leaveObj.state, room);
|
const stateEvents = this.mapSyncEventsFormat(leaveObj.state, room);
|
||||||
const events = this.mapSyncEventsFormat(leaveObj.timeline, room);
|
const events = this.mapSyncEventsFormat(leaveObj.timeline, room);
|
||||||
const accountDataEvents = this.mapSyncEventsFormat(leaveObj.account_data);
|
const accountDataEvents = this.mapSyncEventsFormat(leaveObj.account_data);
|
||||||
|
|
||||||
const [timelineEvents, threadedEvents] = this.client.partitionThreadedEvents(events);
|
await this.processRoomEvents(room, stateEvents, events);
|
||||||
|
|
||||||
this.processRoomEvents(room, stateEvents, timelineEvents);
|
|
||||||
await this.processThreadEvents(room, threadedEvents, false);
|
|
||||||
room.addAccountData(accountDataEvents);
|
room.addAccountData(accountDataEvents);
|
||||||
|
|
||||||
room.recalculate();
|
room.recalculate();
|
||||||
@ -1359,10 +1345,7 @@ export class SyncApi {
|
|||||||
stateEvents.forEach(function(e) {
|
stateEvents.forEach(function(e) {
|
||||||
client.emit(ClientEvent.Event, e);
|
client.emit(ClientEvent.Event, e);
|
||||||
});
|
});
|
||||||
timelineEvents.forEach(function(e) {
|
events.forEach(function(e) {
|
||||||
client.emit(ClientEvent.Event, e);
|
|
||||||
});
|
|
||||||
threadedEvents.forEach(function(e) {
|
|
||||||
client.emit(ClientEvent.Event, e);
|
client.emit(ClientEvent.Event, e);
|
||||||
});
|
});
|
||||||
accountDataEvents.forEach(function(e) {
|
accountDataEvents.forEach(function(e) {
|
||||||
@ -1592,16 +1575,16 @@ export class SyncApi {
|
|||||||
* @param {Room} room
|
* @param {Room} room
|
||||||
* @param {MatrixEvent[]} stateEventList A list of state events. This is the state
|
* @param {MatrixEvent[]} stateEventList A list of state events. This is the state
|
||||||
* at the *START* of the timeline list if it is supplied.
|
* at the *START* of the timeline list if it is supplied.
|
||||||
* @param {MatrixEvent[]} [timelineEventList] A list of timeline events. Lower index
|
* @param {MatrixEvent[]} [timelineEventList] A list of timeline events, including threaded. Lower index
|
||||||
* @param {boolean} fromCache whether the sync response came from cache
|
* @param {boolean} fromCache whether the sync response came from cache
|
||||||
* is earlier in time. Higher index is later.
|
* is earlier in time. Higher index is later.
|
||||||
*/
|
*/
|
||||||
private processRoomEvents(
|
private async processRoomEvents(
|
||||||
room: Room,
|
room: Room,
|
||||||
stateEventList: MatrixEvent[],
|
stateEventList: MatrixEvent[],
|
||||||
timelineEventList?: MatrixEvent[],
|
timelineEventList?: MatrixEvent[],
|
||||||
fromCache = false,
|
fromCache = false,
|
||||||
): void {
|
): Promise<void> {
|
||||||
// If there are no events in the timeline yet, initialise it with
|
// If there are no events in the timeline yet, initialise it with
|
||||||
// the given state events
|
// the given state events
|
||||||
const liveTimeline = room.getLiveTimeline();
|
const liveTimeline = room.getLiveTimeline();
|
||||||
@ -1651,11 +1634,14 @@ export class SyncApi {
|
|||||||
room.oldState.setStateEvents(stateEventList || []);
|
room.oldState.setStateEvents(stateEventList || []);
|
||||||
room.currentState.setStateEvents(stateEventList || []);
|
room.currentState.setStateEvents(stateEventList || []);
|
||||||
}
|
}
|
||||||
// execute the timeline events. This will continue to diverge the current state
|
|
||||||
|
// Execute the timeline events. This will continue to diverge the current state
|
||||||
// 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.
|
||||||
room.addLiveEvents(timelineEventList || [], null, fromCache);
|
const [mainTimelineEvents, threadedEvents] = this.client.partitionThreadedEvents(room, timelineEventList || []);
|
||||||
|
room.addLiveEvents(mainTimelineEvents, null, fromCache);
|
||||||
|
await this.processThreadEvents(room, threadedEvents, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Reference in New Issue
Block a user