Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bf8ee39a3f | |||
| cc1d1d76d0 | |||
| 30fdd96168 | |||
| d473a2e095 | |||
| d398ffd6df | |||
| ffab55452a | |||
| 9120b1dfd9 | |||
| 5d4e3183aa | |||
| 07171a95c4 | |||
| 12afcd3850 | |||
| 2d23330b74 | |||
| 7faff66006 | |||
| b07457726b | |||
| 6bf8142ff6 | |||
| cf0ccaf93d | |||
| 34a3955b60 | |||
| 82122872bf | |||
| 9c242a9ce6 | |||
| 51f3fac87b | |||
| 66b98844a2 | |||
| d03db00e4c | |||
| cf8d2bf6ef | |||
| 913a0b51a6 | |||
| ede0f696ee | |||
| d05214a169 | |||
| 1e93d0b19f | |||
| 5e125e646c | |||
| 1643201363 | |||
| 8e99e084a8 | |||
| 67d362fdd9 | |||
| e66430ada6 | |||
| 52ca60142a | |||
| 7d2a5afa6d | |||
| 02ba233644 | |||
| 50493a3330 | |||
| 8434f29c54 | |||
| a34426a7f6 | |||
| 033693283d | |||
| fab7b7f26a | |||
| 230e3b4ace | |||
| a50a627300 | |||
| 80930f6690 | |||
| 652b3a9208 | |||
| 0a8b825702 | |||
| f4e2a38f4b | |||
| b3474531c9 | |||
| 16ca09eed8 | |||
| 129fb7f10f | |||
| 016e24472a | |||
| 4acb98c496 | |||
| 96d1b30012 | |||
| 6fc586598a | |||
| 2d9c938765 |
@@ -1,3 +1,51 @@
|
||||
Changes in [15.5.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.5.1) (2022-02-14)
|
||||
==================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix issue with rooms not getting marked as unread ([\#2163](https://github.com/matrix-org/matrix-js-sdk/pull/2163)). Fixes vector-im/element-web#20971.
|
||||
* Don't store streams that are only used once ([\#2157](https://github.com/matrix-org/matrix-js-sdk/pull/2157)). Fixes vector-im/element-web#20932. Contributed by @SimonBrandner.
|
||||
* Fix edge cases around RR calculations ([\#2160](https://github.com/matrix-org/matrix-js-sdk/pull/2160)). Fixes vector-im/element-web#20922.
|
||||
* Account for encryption in `maySendMessage()` ([\#2159](https://github.com/matrix-org/matrix-js-sdk/pull/2159)). Contributed by @SimonBrandner.
|
||||
* Send references to thread root to threads, even out of order ([\#2156](https://github.com/matrix-org/matrix-js-sdk/pull/2156)).
|
||||
* Fix initial sync fail when event fetching unsuccessful ([\#2150](https://github.com/matrix-org/matrix-js-sdk/pull/2150)). Fixes vector-im/element-web#20862.
|
||||
* Don't decrypt redacted messages ([\#2143](https://github.com/matrix-org/matrix-js-sdk/pull/2143)). Contributed by @SimonBrandner.
|
||||
|
||||
Changes in [15.5.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.5.1-rc.1) (2022-02-08)
|
||||
============================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix issue with rooms not getting marked as unread ([\#2163](https://github.com/matrix-org/matrix-js-sdk/pull/2163)). Fixes vector-im/element-web#20971.
|
||||
* Don't store streams that are only used once ([\#2157](https://github.com/matrix-org/matrix-js-sdk/pull/2157)). Fixes vector-im/element-web#20932. Contributed by @SimonBrandner.
|
||||
* Fix edge cases around RR calculations ([\#2160](https://github.com/matrix-org/matrix-js-sdk/pull/2160)). Fixes vector-im/element-web#20922.
|
||||
* Account for encryption in `maySendMessage()` ([\#2159](https://github.com/matrix-org/matrix-js-sdk/pull/2159)). Contributed by @SimonBrandner.
|
||||
* Send references to thread root to threads, even out of order ([\#2156](https://github.com/matrix-org/matrix-js-sdk/pull/2156)).
|
||||
* Fix initial sync fail when event fetching unsuccessful ([\#2150](https://github.com/matrix-org/matrix-js-sdk/pull/2150)). Fixes vector-im/element-web#20862.
|
||||
* Don't decrypt redacted messages ([\#2143](https://github.com/matrix-org/matrix-js-sdk/pull/2143)). Contributed by @SimonBrandner.
|
||||
|
||||
Changes in [15.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.5.0) (2022-01-31)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Support m.asset in m.location event content ([\#2109](https://github.com/matrix-org/matrix-js-sdk/pull/2109)).
|
||||
* Send extensible events structure and support on-demand parsing ([\#2091](https://github.com/matrix-org/matrix-js-sdk/pull/2091)).
|
||||
* Support cancelling events whilst they are in status = ENCRYPTING ([\#2095](https://github.com/matrix-org/matrix-js-sdk/pull/2095)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix http-api butchering idServer requests ([\#2134](https://github.com/matrix-org/matrix-js-sdk/pull/2134)). Fixes vector-im/element-web#20680.
|
||||
* Don't remove streams that still have tracks ([\#2104](https://github.com/matrix-org/matrix-js-sdk/pull/2104)).
|
||||
|
||||
Changes in [15.5.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.5.0-rc.1) (2022-01-26)
|
||||
============================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Support m.asset in m.location event content ([\#2109](https://github.com/matrix-org/matrix-js-sdk/pull/2109)).
|
||||
* Send extensible events structure and support on-demand parsing ([\#2091](https://github.com/matrix-org/matrix-js-sdk/pull/2091)).
|
||||
* Support cancelling events whilst they are in status = ENCRYPTING ([\#2095](https://github.com/matrix-org/matrix-js-sdk/pull/2095)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix http-api butchering idServer requests ([\#2134](https://github.com/matrix-org/matrix-js-sdk/pull/2134)). Fixes vector-im/element-web#20680.
|
||||
* Don't remove streams that still have tracks ([\#2104](https://github.com/matrix-org/matrix-js-sdk/pull/2104)).
|
||||
|
||||
Changes in [15.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.4.0) (2022-01-17)
|
||||
==================================================================================================
|
||||
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "15.4.0",
|
||||
"version": "15.5.1",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"scripts": {
|
||||
"prepublishOnly": "yarn build",
|
||||
@@ -56,6 +56,7 @@
|
||||
"bs58": "^4.0.1",
|
||||
"content-type": "^1.0.4",
|
||||
"loglevel": "^1.7.1",
|
||||
"matrix-events-sdk": "^0.0.1-beta.6",
|
||||
"p-retry": "^4.5.0",
|
||||
"qs": "^6.9.6",
|
||||
"request": "^2.88.2",
|
||||
|
||||
@@ -348,7 +348,7 @@ function recvMessage(httpBackend, client, sender, message) {
|
||||
return testUtils.awaitDecryption(event);
|
||||
}).then((event) => {
|
||||
expect(event.getType()).toEqual("m.room.message");
|
||||
expect(event.getContent()).toEqual({
|
||||
expect(event.getContent()).toMatchObject({
|
||||
msgtype: "m.text",
|
||||
body: "Hello, World",
|
||||
});
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import * as utils from "../test-utils";
|
||||
import { CRYPTO_ENABLED } from "../../src/client";
|
||||
import { MatrixEvent } from "../../src/models/event";
|
||||
import { Filter, MemoryStore, Room } from "../../src/matrix";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { Thread } from "../../src/models/thread";
|
||||
|
||||
describe("MatrixClient", function() {
|
||||
let client = null;
|
||||
@@ -392,6 +394,492 @@ describe("MatrixClient", function() {
|
||||
return prom;
|
||||
});
|
||||
});
|
||||
|
||||
describe("partitionThreadedEvents", function() {
|
||||
it("returns empty arrays when given an empty arrays", function() {
|
||||
const events = [];
|
||||
const [timeline, threaded] = client.partitionThreadedEvents(events);
|
||||
expect(timeline).toEqual([]);
|
||||
expect(threaded).toEqual([]);
|
||||
});
|
||||
|
||||
it("copies pre-thread in-timeline vote events onto both timelines", function() {
|
||||
client.clientOpts = { experimentalThreadSupport: true };
|
||||
const events = [
|
||||
eventMessageInThread,
|
||||
eventPollResponseReference,
|
||||
eventPollStartThreadRoot,
|
||||
];
|
||||
// Vote has no threadId yet
|
||||
expect(eventPollResponseReference.threadId).toBeFalsy();
|
||||
|
||||
const [timeline, threaded] = client.partitionThreadedEvents(events);
|
||||
|
||||
expect(timeline).toEqual([
|
||||
// The message that was sent in a thread is missing
|
||||
eventPollResponseReference,
|
||||
eventPollStartThreadRoot,
|
||||
]);
|
||||
|
||||
// The vote event has been copied into the thread
|
||||
const eventRefWithThreadId = withThreadId(
|
||||
eventPollResponseReference, eventPollStartThreadRoot.getId());
|
||||
expect(eventRefWithThreadId.threadId).toBeTruthy();
|
||||
|
||||
expect(threaded).toEqual([
|
||||
eventMessageInThread,
|
||||
eventRefWithThreadId,
|
||||
// Thread does not see thread root
|
||||
]);
|
||||
});
|
||||
|
||||
it("copies pre-thread in-timeline reactions onto both timelines", function() {
|
||||
client.clientOpts = { experimentalThreadSupport: true };
|
||||
const events = [
|
||||
eventMessageInThread,
|
||||
eventReaction,
|
||||
eventPollStartThreadRoot,
|
||||
];
|
||||
|
||||
const [timeline, threaded] = client.partitionThreadedEvents(events);
|
||||
|
||||
expect(timeline).toEqual([
|
||||
eventReaction,
|
||||
eventPollStartThreadRoot,
|
||||
]);
|
||||
|
||||
expect(threaded).toEqual([
|
||||
eventMessageInThread,
|
||||
withThreadId(eventReaction, eventPollStartThreadRoot.getId()),
|
||||
]);
|
||||
});
|
||||
|
||||
it("copies post-thread in-timeline vote events onto both timelines", function() {
|
||||
client.clientOpts = { experimentalThreadSupport: true };
|
||||
const events = [
|
||||
eventPollResponseReference,
|
||||
eventMessageInThread,
|
||||
eventPollStartThreadRoot,
|
||||
];
|
||||
|
||||
const [timeline, threaded] = client.partitionThreadedEvents(events);
|
||||
|
||||
expect(timeline).toEqual([
|
||||
eventPollResponseReference,
|
||||
eventPollStartThreadRoot,
|
||||
]);
|
||||
|
||||
expect(threaded).toEqual([
|
||||
withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()),
|
||||
eventMessageInThread,
|
||||
]);
|
||||
});
|
||||
|
||||
it("copies post-thread in-thread vote events onto both timelines", function() {
|
||||
client.clientOpts = { experimentalThreadSupport: true };
|
||||
|
||||
// Events for this test only, because we hack around with them
|
||||
const eventMessageInThread2 = new MatrixEvent({
|
||||
"age": 80098509,
|
||||
"content": {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"ciphertext": "ENCRYPTEDSTUFF",
|
||||
"device_id": "XISFUZSKHH",
|
||||
"m.relates_to": {
|
||||
"event_id": "$AAA2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
|
||||
"m.in_reply_to": {
|
||||
"event_id": "$AAA2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
|
||||
},
|
||||
"rel_type": "io.element.thread",
|
||||
},
|
||||
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
|
||||
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
|
||||
},
|
||||
"event_id": "$AAAhKIGYowtBblVLkRimeIg8TcdjETnxhDPGfi6NpDg",
|
||||
"origin_server_ts": 1643815466378,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"type": "m.room.encrypted",
|
||||
"unsigned": { "age": 80098509 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const eventPollStartThreadRoot2 = new MatrixEvent({
|
||||
"age": 80108647,
|
||||
"content": {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"ciphertext": "ENCRYPTEDSTUFF",
|
||||
"device_id": "XISFUZSKHH",
|
||||
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
|
||||
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
|
||||
},
|
||||
"event_id": "$AAA2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
|
||||
"origin_server_ts": 1643815456240,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"type": "m.room.encrypted",
|
||||
"unsigned": { "age": 80108647 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const eventPollResponseReference2 = new MatrixEvent({
|
||||
"age": 80098509,
|
||||
"content": {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"ciphertext": "ENCRYPTEDSTUFF",
|
||||
"device_id": "XISFUZSKHH",
|
||||
"m.relates_to": {
|
||||
"event_id": "$AAA2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
|
||||
"rel_type": "m.reference",
|
||||
},
|
||||
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
|
||||
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
|
||||
},
|
||||
"event_id": "$AAAvpezvsF0cKgav3g8W-uEVS4WkDHgxbJZvL3uMR1g",
|
||||
"origin_server_ts": 1643815458650,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"type": "m.room.encrypted",
|
||||
"unsigned": { "age": 80106237 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
// When we react within a thread, sometimes the thread root
|
||||
// has isThreadRelation === true, because thread is set on it,
|
||||
// but threadId is not.
|
||||
eventPollStartThreadRoot2.setThread(
|
||||
new Thread(
|
||||
eventPollStartThreadRoot2,
|
||||
{
|
||||
client,
|
||||
room: new Room(),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const events = [
|
||||
eventPollResponseReference2,
|
||||
eventMessageInThread2,
|
||||
eventPollStartThreadRoot2,
|
||||
];
|
||||
|
||||
const [timeline, threaded] = client.partitionThreadedEvents(events);
|
||||
|
||||
expect(timeline).toEqual([
|
||||
eventPollResponseReference2,
|
||||
// eventPollStartThreadRoot2,
|
||||
// This is weird: by hacking the thread root to have an inconsistency
|
||||
// between thread and threadId (which is what I have observed in the
|
||||
// wild), we have persuaded the code that the thread root is actually
|
||||
// within the thread, so it is not provided to the main timeline.
|
||||
//
|
||||
// This should go away when we fix this inconsistency. When that
|
||||
// happens, we should probably delete this test.
|
||||
]);
|
||||
|
||||
expect(threaded).toEqual([
|
||||
withThreadId(
|
||||
eventPollResponseReference2, eventPollStartThreadRoot2.getId(),
|
||||
),
|
||||
eventMessageInThread2,
|
||||
eventPollStartThreadRoot2, // See note above for why this appears here.
|
||||
]);
|
||||
});
|
||||
|
||||
it("copies post-thread in-timeline reactions onto both timelines", function() {
|
||||
client.clientOpts = { experimentalThreadSupport: true };
|
||||
const events = [
|
||||
eventReaction,
|
||||
eventMessageInThread,
|
||||
eventPollStartThreadRoot,
|
||||
];
|
||||
|
||||
const [timeline, threaded] = client.partitionThreadedEvents(events);
|
||||
|
||||
expect(timeline).toEqual([
|
||||
eventReaction,
|
||||
eventPollStartThreadRoot,
|
||||
]);
|
||||
|
||||
expect(threaded).toEqual([
|
||||
withThreadId(eventReaction, eventPollStartThreadRoot.getId()),
|
||||
eventMessageInThread,
|
||||
]);
|
||||
});
|
||||
|
||||
it("sends room state events to the main timeline only", function() {
|
||||
client.clientOpts = { experimentalThreadSupport: true };
|
||||
// This is based on recording the events in a real room:
|
||||
const events = [
|
||||
eventMessageInThread,
|
||||
eventPollResponseReference,
|
||||
eventPollStartThreadRoot,
|
||||
eventRoomName,
|
||||
eventEncryption,
|
||||
eventGuestAccess,
|
||||
eventHistoryVisibility,
|
||||
eventJoinRules,
|
||||
eventPowerLevels,
|
||||
eventMember,
|
||||
eventCreate,
|
||||
];
|
||||
const [timeline, threaded] = client.partitionThreadedEvents(events);
|
||||
|
||||
expect(timeline).toEqual([
|
||||
// The message that was sent in a thread is missing
|
||||
eventPollResponseReference,
|
||||
eventPollStartThreadRoot,
|
||||
eventRoomName,
|
||||
eventEncryption,
|
||||
eventGuestAccess,
|
||||
eventHistoryVisibility,
|
||||
eventJoinRules,
|
||||
eventPowerLevels,
|
||||
eventMember,
|
||||
eventCreate,
|
||||
]);
|
||||
|
||||
// Thread should contain only stuff that happened in the thread -
|
||||
// no thread root, and no room state events
|
||||
expect(threaded).toEqual([
|
||||
eventMessageInThread,
|
||||
withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function withThreadId(event, newThreadId) {
|
||||
const ret = event.toSnapshot();
|
||||
ret.setThreadId(newThreadId);
|
||||
return ret;
|
||||
}
|
||||
|
||||
const eventMessageInThread = new MatrixEvent({
|
||||
"age": 80098509,
|
||||
"content": {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"ciphertext": "ENCRYPTEDSTUFF",
|
||||
"device_id": "XISFUZSKHH",
|
||||
"m.relates_to": {
|
||||
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
|
||||
"m.in_reply_to": {
|
||||
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
|
||||
},
|
||||
"rel_type": "io.element.thread",
|
||||
},
|
||||
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
|
||||
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
|
||||
},
|
||||
"event_id": "$W4chKIGYowtBblVLkRimeIg8TcdjETnxhDPGfi6NpDg",
|
||||
"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 eventPollResponseReference = new MatrixEvent({
|
||||
"age": 80098509,
|
||||
"content": {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"ciphertext": "ENCRYPTEDSTUFF",
|
||||
"device_id": "XISFUZSKHH",
|
||||
"m.relates_to": {
|
||||
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
|
||||
"rel_type": "m.reference",
|
||||
},
|
||||
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
|
||||
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
|
||||
},
|
||||
"event_id": "$91JvpezvsF0cKgav3g8W-uEVS4WkDHgxbJZvL3uMR1g",
|
||||
"origin_server_ts": 1643815458650,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"type": "m.room.encrypted",
|
||||
"unsigned": { "age": 80106237 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const eventReaction = new MatrixEvent({
|
||||
"content": {
|
||||
"m.relates_to": {
|
||||
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
|
||||
"key": "🤗",
|
||||
"rel_type": "m.annotation",
|
||||
},
|
||||
},
|
||||
"origin_server_ts": 1643977249238,
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"type": "m.reaction",
|
||||
"unsigned": {
|
||||
"age": 22598,
|
||||
"transaction_id": "m1643977249073.16",
|
||||
},
|
||||
"event_id": "$86B2b-x3LgE4DlV4y24b7UHnt72LIA3rzjvMysTtAfA",
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
});
|
||||
|
||||
const eventPollStartThreadRoot = new MatrixEvent({
|
||||
"age": 80108647,
|
||||
"content": {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"ciphertext": "ENCRYPTEDSTUFF",
|
||||
"device_id": "XISFUZSKHH",
|
||||
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
|
||||
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
|
||||
},
|
||||
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
|
||||
"origin_server_ts": 1643815456240,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"type": "m.room.encrypted",
|
||||
"unsigned": { "age": 80108647 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const eventRoomName = new MatrixEvent({
|
||||
"age": 80123249,
|
||||
"content": {
|
||||
"name": "1 poll, 1 vote, 1 thread",
|
||||
},
|
||||
"event_id": "$QAdyNJtKnl1j7or2yMycbOCvb6bCgvHs5lg3ZMd5xWk",
|
||||
"origin_server_ts": 1643815441638,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"state_key": "",
|
||||
"type": "m.room.name",
|
||||
"unsigned": { "age": 80123249 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const eventEncryption = new MatrixEvent({
|
||||
"age": 80123383,
|
||||
"content": {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
},
|
||||
"event_id": "$1hGykogKQkXbHw8bVuyE3BjHnFBEJBcUWnakd0ck2K0",
|
||||
"origin_server_ts": 1643815441504,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"state_key": "",
|
||||
"type": "m.room.encryption",
|
||||
"unsigned": { "age": 80123383 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const eventGuestAccess = new MatrixEvent({
|
||||
"age": 80123473,
|
||||
"content": {
|
||||
"guest_access": "can_join",
|
||||
},
|
||||
"event_id": "$4_2n-H6K9-0nPbnjjtIue2SU44tGJsnuTmi6UuSrh-U",
|
||||
"origin_server_ts": 1643815441414,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"state_key": "",
|
||||
"type": "m.room.guest_access",
|
||||
"unsigned": { "age": 80123473 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const eventHistoryVisibility = new MatrixEvent({
|
||||
"age": 80123556,
|
||||
"content": {
|
||||
"history_visibility": "shared",
|
||||
},
|
||||
"event_id": "$W6kp44CTnvciOiHSPyhp8dh4n2v1_9kclUPddeaQj0E",
|
||||
"origin_server_ts": 1643815441331,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"state_key": "",
|
||||
"type": "m.room.history_visibility",
|
||||
"unsigned": { "age": 80123556 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const eventJoinRules = new MatrixEvent({
|
||||
"age": 80123696,
|
||||
"content": {
|
||||
"join_rule": "invite",
|
||||
},
|
||||
"event_id": "$6JDDeDp7fEc0F6YnTWMruNcKWFltR3e9wk7wWDDJrAU",
|
||||
"origin_server_ts": 1643815441191,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"state_key": "",
|
||||
"type": "m.room.join_rules",
|
||||
"unsigned": { "age": 80123696 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const eventPowerLevels = new MatrixEvent({
|
||||
"age": 80124105,
|
||||
"content": {
|
||||
"ban": 50,
|
||||
"events": {
|
||||
"m.room.avatar": 50,
|
||||
"m.room.canonical_alias": 50,
|
||||
"m.room.encryption": 100,
|
||||
"m.room.history_visibility": 100,
|
||||
"m.room.name": 50,
|
||||
"m.room.power_levels": 100,
|
||||
"m.room.server_acl": 100,
|
||||
"m.room.tombstone": 100,
|
||||
},
|
||||
"events_default": 0,
|
||||
"historical": 100,
|
||||
"invite": 0,
|
||||
"kick": 50,
|
||||
"redact": 50,
|
||||
"state_default": 50,
|
||||
"users": {
|
||||
"@andybalaam-test1:matrix.org": 100,
|
||||
},
|
||||
"users_default": 0,
|
||||
},
|
||||
"event_id": "$XZY2YgQhXskpc7gmJJG3S0VmS9_QjjCUVeeFTfgfC2E",
|
||||
"origin_server_ts": 1643815440782,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"state_key": "",
|
||||
"type": "m.room.power_levels",
|
||||
"unsigned": { "age": 80124105 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const eventMember = new MatrixEvent({
|
||||
"age": 80125279,
|
||||
"content": {
|
||||
"avatar_url": "mxc://matrix.org/aNtbVcFfwotudypZcHsIcPOc",
|
||||
"displayname": "andybalaam-test1",
|
||||
"membership": "join",
|
||||
},
|
||||
"event_id": "$Ex5eVmMs_ti784mo8bgddynbwLvy6231lCycJr7Cl9M",
|
||||
"origin_server_ts": 1643815439608,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"state_key": "@andybalaam-test1:matrix.org",
|
||||
"type": "m.room.member",
|
||||
"unsigned": { "age": 80125279 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const eventCreate = new MatrixEvent({
|
||||
"age": 80126105,
|
||||
"content": {
|
||||
"creator": "@andybalaam-test1:matrix.org",
|
||||
"room_version": "6",
|
||||
},
|
||||
"event_id": "$e7j2Gt37k5NPwB6lz2N3V9lO5pUdNK8Ai7i2FPEK-oI",
|
||||
"origin_server_ts": 1643815438782,
|
||||
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
|
||||
"sender": "@andybalaam-test1:matrix.org",
|
||||
"state_key": "",
|
||||
"type": "m.room.create",
|
||||
"unsigned": { "age": 80126105 },
|
||||
"user_id": "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
function assertObjectContains(obj, expected) {
|
||||
|
||||
@@ -365,3 +365,5 @@ export function setHttpResponses(
|
||||
.respond(200, response.data);
|
||||
});
|
||||
}
|
||||
|
||||
export const emitPromise = (e, k) => new Promise(r => e.once(k, r));
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { makeLocationContent } from "../../src/content-helpers";
|
||||
import {
|
||||
ASSET_NODE_TYPE,
|
||||
ASSET_TYPE_SELF,
|
||||
LOCATION_EVENT_TYPE,
|
||||
TIMESTAMP_NODE_TYPE,
|
||||
} from "../../src/@types/location";
|
||||
import { TEXT_NODE_TYPE } from "../../src/@types/extensible_events";
|
||||
|
||||
describe("Location", function() {
|
||||
it("should create a valid location with defaults", function() {
|
||||
const loc = makeLocationContent("txt", "geo:foo", 134235435);
|
||||
expect(loc.body).toEqual("txt");
|
||||
expect(loc.msgtype).toEqual("m.location");
|
||||
expect(loc.geo_uri).toEqual("geo:foo");
|
||||
expect(LOCATION_EVENT_TYPE.findIn(loc)).toEqual({
|
||||
uri: "geo:foo",
|
||||
description: undefined,
|
||||
});
|
||||
expect(ASSET_NODE_TYPE.findIn(loc)).toEqual({ type: ASSET_TYPE_SELF });
|
||||
expect(TEXT_NODE_TYPE.findIn(loc)).toEqual("txt");
|
||||
expect(TIMESTAMP_NODE_TYPE.findIn(loc)).toEqual(134235435);
|
||||
});
|
||||
|
||||
it("should create a valid location with explicit properties", function() {
|
||||
const loc = makeLocationContent(
|
||||
"txxt", "geo:bar", 134235436, "desc", "m.something");
|
||||
|
||||
expect(loc.body).toEqual("txxt");
|
||||
expect(loc.msgtype).toEqual("m.location");
|
||||
expect(loc.geo_uri).toEqual("geo:bar");
|
||||
expect(LOCATION_EVENT_TYPE.findIn(loc)).toEqual({
|
||||
uri: "geo:bar",
|
||||
description: "desc",
|
||||
});
|
||||
expect(ASSET_NODE_TYPE.findIn(loc)).toEqual({ type: "m.something" });
|
||||
expect(TEXT_NODE_TYPE.findIn(loc)).toEqual("txxt");
|
||||
expect(TIMESTAMP_NODE_TYPE.findIn(loc)).toEqual(134235436);
|
||||
});
|
||||
});
|
||||
@@ -11,8 +11,9 @@ import {
|
||||
UNSTABLE_MSC3089_TREE_SUBTYPE,
|
||||
} from "../../src/@types/event";
|
||||
import { MEGOLM_ALGORITHM } from "../../src/crypto/olmlib";
|
||||
import { MatrixEvent } from "../../src/models/event";
|
||||
import { EventStatus, MatrixEvent } from "../../src/models/event";
|
||||
import { Preset } from "../../src/@types/partials";
|
||||
import * as testUtils from "../test-utils";
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
@@ -713,6 +714,7 @@ describe("MatrixClient", function() {
|
||||
describe("guest rooms", function() {
|
||||
it("should only do /sync calls (without filter/pushrules)", function(done) {
|
||||
httpLookups = []; // no /pushrules or /filterw
|
||||
httpLookups.push(CAPABILITIES_RESPONSE);
|
||||
httpLookups.push({
|
||||
method: "GET",
|
||||
path: "/sync",
|
||||
@@ -867,4 +869,83 @@ describe("MatrixClient", function() {
|
||||
await client.redactEvent(roomId, eventId, txnId, { reason });
|
||||
});
|
||||
});
|
||||
|
||||
describe("cancelPendingEvent", () => {
|
||||
const roomId = "!room:server";
|
||||
const txnId = "m12345";
|
||||
|
||||
const mockRoom = {
|
||||
getMyMembership: () => "join",
|
||||
updatePendingEvent: (event, status) => event.setStatus(status),
|
||||
currentState: {
|
||||
getStateEvents: (eventType, stateKey) => {
|
||||
if (eventType === EventType.RoomCreate) {
|
||||
expect(stateKey).toEqual("");
|
||||
return new MatrixEvent({
|
||||
content: {
|
||||
[RoomCreateTypeField]: RoomType.Space,
|
||||
},
|
||||
});
|
||||
} else if (eventType === EventType.RoomEncryption) {
|
||||
expect(stateKey).toEqual("");
|
||||
return new MatrixEvent({ content: {} });
|
||||
} else {
|
||||
throw new Error("Unexpected event type or state key");
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let event;
|
||||
beforeEach(async () => {
|
||||
event = new MatrixEvent({
|
||||
event_id: "~" + roomId + ":" + txnId,
|
||||
user_id: client.credentials.userId,
|
||||
sender: client.credentials.userId,
|
||||
room_id: roomId,
|
||||
origin_server_ts: new Date().getTime(),
|
||||
});
|
||||
event.setTxnId(txnId);
|
||||
|
||||
client.getRoom = (getRoomId) => {
|
||||
expect(getRoomId).toEqual(roomId);
|
||||
return mockRoom;
|
||||
};
|
||||
client.crypto = { // mock crypto
|
||||
encryptEvent: (event, room) => new Promise(() => {}),
|
||||
};
|
||||
});
|
||||
|
||||
function assertCancelled() {
|
||||
expect(event.status).toBe(EventStatus.CANCELLED);
|
||||
expect(client.scheduler.removeEventFromQueue(event)).toBeFalsy();
|
||||
expect(httpLookups.filter(h => h.path.includes("/send/")).length).toBe(0);
|
||||
}
|
||||
|
||||
it("should cancel an event which is queued", () => {
|
||||
event.setStatus(EventStatus.QUEUED);
|
||||
client.scheduler.queueEvent(event);
|
||||
client.cancelPendingEvent(event);
|
||||
assertCancelled();
|
||||
});
|
||||
|
||||
it("should cancel an event which is encrypting", async () => {
|
||||
client.encryptAndSendEvent(null, event);
|
||||
await testUtils.emitPromise(event, "Event.status");
|
||||
client.cancelPendingEvent(event);
|
||||
assertCancelled();
|
||||
});
|
||||
|
||||
it("should cancel an event which is not sent", () => {
|
||||
event.setStatus(EventStatus.NOT_SENT);
|
||||
client.cancelPendingEvent(event);
|
||||
assertCancelled();
|
||||
});
|
||||
|
||||
it("should error when given any other event status", () => {
|
||||
event.setStatus(EventStatus.SENDING);
|
||||
expect(() => client.cancelPendingEvent(event)).toThrow("cannot cancel an event with status sending");
|
||||
expect(event.status).toBe(EventStatus.SENDING);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -120,7 +120,7 @@ describe("RoomState", function() {
|
||||
it("should return a single MatrixEvent if a state_key was specified",
|
||||
function() {
|
||||
const event = state.getStateEvents("m.room.member", userA);
|
||||
expect(event.getContent()).toEqual({
|
||||
expect(event.getContent()).toMatchObject({
|
||||
membership: "join",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1461,7 +1461,7 @@ describe("Room", function() {
|
||||
describe("maySendMessage", function() {
|
||||
it("should return false if synced membership not join",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
const room = new Room(roomId, { isRoomEncrypted: () => false }, userA);
|
||||
room.updateMyMembership("invite");
|
||||
expect(room.maySendMessage()).toEqual(false);
|
||||
room.updateMyMembership("leave");
|
||||
|
||||
@@ -177,6 +177,16 @@ export const UNSTABLE_ELEMENT_FUNCTIONAL_USERS = new UnstableValue(
|
||||
"io.element.functional_members",
|
||||
"io.element.functional_members");
|
||||
|
||||
/**
|
||||
* A type of message that affects visibility of a message,
|
||||
* as per https://github.com/matrix-org/matrix-doc/pull/3531
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export const EVENT_VISIBILITY_CHANGE_TYPE = new UnstableValue(
|
||||
"m.visibility",
|
||||
"org.matrix.msc3531.visibility");
|
||||
|
||||
export interface IEncryptedFile {
|
||||
url: string;
|
||||
mimetype?: string;
|
||||
|
||||
@@ -23,8 +23,12 @@ import { TEXT_NODE_TYPE } from "./extensible_events";
|
||||
export const LOCATION_EVENT_TYPE = new UnstableValue(
|
||||
"m.location", "org.matrix.msc3488.location");
|
||||
|
||||
export const ASSET_NODE_TYPE = new UnstableValue("m.asset", "org.matrix.msc3488.asset");
|
||||
|
||||
export const TIMESTAMP_NODE_TYPE = new UnstableValue("m.ts", "org.matrix.msc3488.ts");
|
||||
|
||||
export const ASSET_TYPE_SELF = "m.self";
|
||||
|
||||
/* From the spec at:
|
||||
* https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md
|
||||
{
|
||||
@@ -37,6 +41,9 @@ export const TIMESTAMP_NODE_TYPE = new UnstableValue("m.ts", "org.matrix.msc3488
|
||||
"uri": "geo:51.5008,0.1247;u=35",
|
||||
"description": "Matthew's whereabouts",
|
||||
},
|
||||
"m.asset": {
|
||||
"type": "m.self"
|
||||
},
|
||||
"m.text": "Matthew was at geo:51.5008,0.1247;u=35 as of Sat Nov 13 18:50:58 2021",
|
||||
"m.ts": 1636829458432,
|
||||
}
|
||||
@@ -52,6 +59,9 @@ export interface ILocationContent extends IContent {
|
||||
uri: string;
|
||||
description?: string;
|
||||
};
|
||||
[ASSET_NODE_TYPE.name]: {
|
||||
type: string;
|
||||
};
|
||||
[TEXT_NODE_TYPE.name]: string;
|
||||
[TIMESTAMP_NODE_TYPE.name]: number;
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { UnstableValue } from "../NamespacedValue";
|
||||
import { IContent } from "../models/event";
|
||||
import { TEXT_NODE_TYPE } from "./extensible_events";
|
||||
|
||||
export const POLL_START_EVENT_TYPE = new UnstableValue(
|
||||
"m.poll.start", "org.matrix.msc3381.poll.start");
|
||||
|
||||
export const POLL_RESPONSE_EVENT_TYPE = new UnstableValue(
|
||||
"m.poll.response", "org.matrix.msc3381.poll.response");
|
||||
|
||||
export const POLL_END_EVENT_TYPE = new UnstableValue(
|
||||
"m.poll.end", "org.matrix.msc3381.poll.end");
|
||||
|
||||
export const POLL_KIND_DISCLOSED = new UnstableValue(
|
||||
"m.poll.disclosed", "org.matrix.msc3381.poll.disclosed");
|
||||
|
||||
export const POLL_KIND_UNDISCLOSED = new UnstableValue(
|
||||
"m.poll.undisclosed", "org.matrix.msc3381.poll.undisclosed");
|
||||
|
||||
export interface IPollAnswer extends IContent {
|
||||
id: string;
|
||||
[TEXT_NODE_TYPE.name]: string;
|
||||
}
|
||||
|
||||
export interface IPollContent extends IContent {
|
||||
[POLL_START_EVENT_TYPE.name]: {
|
||||
kind: string; // disclosed or undisclosed (untypeable for now)
|
||||
question: {
|
||||
[TEXT_NODE_TYPE.name]: string;
|
||||
};
|
||||
answers: IPollAnswer[];
|
||||
};
|
||||
[TEXT_NODE_TYPE.name]: string;
|
||||
}
|
||||
|
||||
export interface IPollResponseContent extends IContent {
|
||||
[POLL_RESPONSE_EVENT_TYPE.name]: {
|
||||
answers: string[];
|
||||
};
|
||||
"m.relates_to": {
|
||||
"event_id": string;
|
||||
"rel_type": string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IPollEndContent extends IContent {
|
||||
[POLL_END_EVENT_TYPE.name]: {};
|
||||
"m.relates_to": {
|
||||
"event_id": string;
|
||||
"rel_type": string;
|
||||
};
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import { IContent, IEvent } from "../models/event";
|
||||
import { Preset, Visibility } from "./partials";
|
||||
import { SearchKey } from "./search";
|
||||
import { IRoomEventFilter } from "../filter";
|
||||
import { Direction } from "../models/event-timeline";
|
||||
|
||||
// allow camelcase as these are things that go onto the wire
|
||||
/* eslint-disable camelcase */
|
||||
@@ -144,6 +145,7 @@ export interface IRelationsRequestOpts {
|
||||
from?: string;
|
||||
to?: string;
|
||||
limit?: number;
|
||||
direction?: Direction;
|
||||
}
|
||||
|
||||
export interface IRelationsResponse {
|
||||
|
||||
+148
-46
@@ -20,8 +20,9 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "events";
|
||||
import { EmoteEvent, MessageEvent, NoticeEvent, IPartialEvent } from "matrix-events-sdk";
|
||||
|
||||
import { ISyncStateData, SyncApi } from "./sync";
|
||||
import { ISyncStateData, SyncApi, SyncState } from "./sync";
|
||||
import { EventStatus, IContent, IDecryptOptions, IEvent, MatrixEvent } from "./models/event";
|
||||
import { StubStore } from "./store/stub";
|
||||
import { createNewMatrixCall, MatrixCall } from "./webrtc/call";
|
||||
@@ -86,7 +87,14 @@ import {
|
||||
} from "./crypto/keybackup";
|
||||
import { IIdentityServerProvider } from "./@types/IIdentityServerProvider";
|
||||
import { MatrixScheduler } from "./scheduler";
|
||||
import { IAuthData, ICryptoCallbacks, IMinimalEvent, IRoomEvent, IStateEvent, NotificationCountType } from "./matrix";
|
||||
import {
|
||||
IAuthData,
|
||||
ICryptoCallbacks,
|
||||
IMinimalEvent,
|
||||
IRoomEvent,
|
||||
IStateEvent,
|
||||
NotificationCountType,
|
||||
} from "./matrix";
|
||||
import {
|
||||
CrossSigningKey,
|
||||
IAddSecretStorageKeyOpts,
|
||||
@@ -96,7 +104,6 @@ import {
|
||||
IRecoveryKey,
|
||||
ISecretStorageKeyInfo,
|
||||
} from "./crypto/api";
|
||||
import { SyncState } from "./sync";
|
||||
import { EventTimelineSet } from "./models/event-timeline-set";
|
||||
import { VerificationRequest } from "./crypto/verification/request/VerificationRequest";
|
||||
import { VerificationBase as Verification } from "./crypto/verification/Base";
|
||||
@@ -804,7 +811,7 @@ export class MatrixClient extends EventEmitter {
|
||||
// TODO: This should expire: https://github.com/matrix-org/matrix-js-sdk/issues/1020
|
||||
protected serverVersionsPromise: Promise<IServerVersions>;
|
||||
|
||||
protected cachedCapabilities: {
|
||||
public cachedCapabilities: {
|
||||
capabilities: ICapabilities;
|
||||
expiration: number;
|
||||
};
|
||||
@@ -816,6 +823,7 @@ export class MatrixClient extends EventEmitter {
|
||||
protected exportedOlmDeviceToImport: IOlmDevice;
|
||||
protected txnCtr = 0;
|
||||
protected mediaHandler = new MediaHandler();
|
||||
protected pendingEventEncryption = new Map<string, Promise<void>>();
|
||||
|
||||
constructor(opts: IMatrixClientCreateOpts) {
|
||||
super();
|
||||
@@ -870,7 +878,7 @@ export class MatrixClient extends EventEmitter {
|
||||
|
||||
this.scheduler = opts.scheduler;
|
||||
if (this.scheduler) {
|
||||
this.scheduler.setProcessFunction(async (eventToSend) => {
|
||||
this.scheduler.setProcessFunction(async (eventToSend: MatrixEvent) => {
|
||||
const room = this.getRoom(eventToSend.getRoomId());
|
||||
if (eventToSend.status !== EventStatus.SENDING) {
|
||||
this.updatePendingEventStatus(room, eventToSend, EventStatus.SENDING);
|
||||
@@ -1001,7 +1009,7 @@ export class MatrixClient extends EventEmitter {
|
||||
* state change events.
|
||||
* @param {Object=} opts Options to apply when syncing.
|
||||
*/
|
||||
public async startClient(opts: IStartClientOpts): Promise<void> {
|
||||
public async startClient(opts?: IStartClientOpts): Promise<void> {
|
||||
if (this.clientRunning) {
|
||||
// client is already running.
|
||||
return;
|
||||
@@ -1041,9 +1049,7 @@ export class MatrixClient extends EventEmitter {
|
||||
this.syncApi.stop();
|
||||
}
|
||||
|
||||
if (!this.isGuest()) {
|
||||
await this.getCapabilities(true);
|
||||
}
|
||||
await this.getCapabilities(true);
|
||||
|
||||
// shallow-copy the opts dict before modifying and storing it
|
||||
this.clientOpts = Object.assign({}, opts) as IStoredClientOpts;
|
||||
@@ -2576,8 +2582,10 @@ export class MatrixClient extends EventEmitter {
|
||||
|
||||
return {
|
||||
algorithm,
|
||||
/* eslint-disable camelcase */
|
||||
auth_data,
|
||||
recovery_key,
|
||||
/* eslint-enable camelcase */
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3399,15 +3407,18 @@ export class MatrixClient extends EventEmitter {
|
||||
* Cancel a queued or unsent event.
|
||||
*
|
||||
* @param {MatrixEvent} event Event to cancel
|
||||
* @throws Error if the event is not in QUEUED or NOT_SENT state
|
||||
* @throws Error if the event is not in QUEUED, NOT_SENT or ENCRYPTING state
|
||||
*/
|
||||
public cancelPendingEvent(event: MatrixEvent) {
|
||||
if ([EventStatus.QUEUED, EventStatus.NOT_SENT].indexOf(event.status) < 0) {
|
||||
if (![EventStatus.QUEUED, EventStatus.NOT_SENT, EventStatus.ENCRYPTING].includes(event.status)) {
|
||||
throw new Error("cannot cancel an event with status " + event.status);
|
||||
}
|
||||
|
||||
// first tell the scheduler to forget about it, if it's queued
|
||||
if (this.scheduler) {
|
||||
// if the event is currently being encrypted then
|
||||
if (event.status === EventStatus.ENCRYPTING) {
|
||||
this.pendingEventEncryption.delete(event.getId());
|
||||
} else if (this.scheduler && event.status === EventStatus.QUEUED) {
|
||||
// tell the scheduler to forget about it, if it's queued
|
||||
this.scheduler.removeEventFromQueue(event);
|
||||
}
|
||||
|
||||
@@ -3582,6 +3593,22 @@ export class MatrixClient extends EventEmitter {
|
||||
eventType = threadId;
|
||||
threadId = null;
|
||||
}
|
||||
|
||||
if (threadId && content["m.relates_to"]?.rel_type !== RelationType.Thread) {
|
||||
content["m.relates_to"] = {
|
||||
...content["m.relates_to"],
|
||||
"rel_type": RelationType.Thread,
|
||||
"event_id": threadId,
|
||||
};
|
||||
|
||||
const thread = this.getRoom(roomId)?.threads.get(threadId);
|
||||
if (thread) {
|
||||
content["m.relates_to"]["m.in_reply_to"] = {
|
||||
"event_id": thread.replyToEvent.getId(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return this.sendCompleteEvent(roomId, threadId, { type: eventType, content }, txnId as string, callback);
|
||||
}
|
||||
|
||||
@@ -3642,12 +3669,12 @@ export class MatrixClient extends EventEmitter {
|
||||
const type = localEvent.getType();
|
||||
logger.log(`sendEvent of type ${type} in ${roomId} with txnId ${txnId}`);
|
||||
|
||||
localEvent.setTxnId(txnId as string);
|
||||
localEvent.setTxnId(txnId);
|
||||
localEvent.setStatus(EventStatus.SENDING);
|
||||
|
||||
// add this event immediately to the local store as 'sending'.
|
||||
if (room) {
|
||||
room.addPendingEvent(localEvent, txnId as string);
|
||||
room.addPendingEvent(localEvent, txnId);
|
||||
}
|
||||
|
||||
// addPendingEvent can change the state to NOT_SENT if it believes
|
||||
@@ -3669,16 +3696,26 @@ export class MatrixClient extends EventEmitter {
|
||||
* @private
|
||||
*/
|
||||
private encryptAndSendEvent(room: Room, event: MatrixEvent, callback?: Callback): Promise<ISendEventResponse> {
|
||||
let cancelled = false;
|
||||
// Add an extra Promise.resolve() to turn synchronous exceptions into promise rejections,
|
||||
// so that we can handle synchronous and asynchronous exceptions with the
|
||||
// same code path.
|
||||
return Promise.resolve().then(() => {
|
||||
const encryptionPromise = this.encryptEventIfNeeded(event, room);
|
||||
if (!encryptionPromise) return null;
|
||||
if (!encryptionPromise) return null; // doesn't need encryption
|
||||
|
||||
this.pendingEventEncryption.set(event.getId(), encryptionPromise);
|
||||
this.updatePendingEventStatus(room, event, EventStatus.ENCRYPTING);
|
||||
return encryptionPromise.then(() => this.updatePendingEventStatus(room, event, EventStatus.SENDING));
|
||||
return encryptionPromise.then(() => {
|
||||
if (!this.pendingEventEncryption.has(event.getId())) {
|
||||
// cancelled via MatrixClient::cancelPendingEvent
|
||||
cancelled = true;
|
||||
return;
|
||||
}
|
||||
this.updatePendingEventStatus(room, event, EventStatus.SENDING);
|
||||
});
|
||||
}).then(() => {
|
||||
if (cancelled) return {} as ISendEventResponse;
|
||||
let promise: Promise<ISendEventResponse>;
|
||||
if (this.scheduler) {
|
||||
// if this returns a promise then the scheduler has control now and will
|
||||
@@ -3922,11 +3959,51 @@ export class MatrixClient extends EventEmitter {
|
||||
callback = txnId as any as Callback; // for legacy
|
||||
txnId = undefined;
|
||||
}
|
||||
|
||||
// Populate all outbound events with Extensible Events metadata to ensure there's a
|
||||
// reasonably large pool of messages to parse.
|
||||
let eventType: string = EventType.RoomMessage;
|
||||
let sendContent: IContent = content as IContent;
|
||||
const makeContentExtensible = (content: IContent = {}, recurse = true): IPartialEvent<object> => {
|
||||
let newEvent: IPartialEvent<object> = null;
|
||||
|
||||
if (content['msgtype'] === MsgType.Text) {
|
||||
newEvent = MessageEvent.from(content['body'], content['formatted_body']).serialize();
|
||||
} else if (content['msgtype'] === MsgType.Emote) {
|
||||
newEvent = EmoteEvent.from(content['body'], content['formatted_body']).serialize();
|
||||
} else if (content['msgtype'] === MsgType.Notice) {
|
||||
newEvent = NoticeEvent.from(content['body'], content['formatted_body']).serialize();
|
||||
}
|
||||
|
||||
if (newEvent && content['m.new_content'] && recurse) {
|
||||
const newContent = makeContentExtensible(content['m.new_content'], false);
|
||||
if (newContent) {
|
||||
newEvent.content['m.new_content'] = newContent.content;
|
||||
}
|
||||
}
|
||||
|
||||
if (newEvent) {
|
||||
// copy over all other fields we don't know about
|
||||
for (const [k, v] of Object.entries(content)) {
|
||||
if (!newEvent.content.hasOwnProperty(k)) {
|
||||
newEvent.content[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newEvent;
|
||||
};
|
||||
const result = makeContentExtensible(sendContent);
|
||||
if (result) {
|
||||
eventType = result.type;
|
||||
sendContent = result.content;
|
||||
}
|
||||
|
||||
return this.sendEvent(
|
||||
roomId,
|
||||
threadId as (string | null),
|
||||
EventType.RoomMessage,
|
||||
content as IContent,
|
||||
eventType,
|
||||
sendContent,
|
||||
txnId as string,
|
||||
callback,
|
||||
);
|
||||
@@ -4151,6 +4228,7 @@ export class MatrixClient extends EventEmitter {
|
||||
info: info,
|
||||
body: text,
|
||||
};
|
||||
|
||||
return this.sendEvent(roomId, threadId, EventType.Sticker, content, undefined, callback);
|
||||
}
|
||||
|
||||
@@ -4994,7 +5072,7 @@ export class MatrixClient extends EventEmitter {
|
||||
limit,
|
||||
Direction.Backward,
|
||||
);
|
||||
}).then((res: IMessagesResponse) => {
|
||||
}).then(async (res: IMessagesResponse) => {
|
||||
const matrixEvents = res.chunk.map(this.getEventMapper());
|
||||
if (res.state) {
|
||||
const stateEvents = res.state.map(this.getEventMapper());
|
||||
@@ -5004,7 +5082,7 @@ export class MatrixClient extends EventEmitter {
|
||||
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(matrixEvents);
|
||||
|
||||
room.addEventsToTimeline(timelineEvents, true, room.getLiveTimeline());
|
||||
this.processThreadEvents(room, threadedEvents);
|
||||
await this.processThreadEvents(room, threadedEvents);
|
||||
|
||||
room.oldState.paginationToken = res.end;
|
||||
if (res.chunk.length === 0) {
|
||||
@@ -5082,7 +5160,7 @@ export class MatrixClient extends EventEmitter {
|
||||
|
||||
// TODO: we should implement a backoff (as per scrollback()) to deal more
|
||||
// nicely with HTTP errors.
|
||||
const promise = this.http.authedRequest<any>(undefined, Method.Get, path, params).then((res) => { // TODO types
|
||||
const promise = this.http.authedRequest<any>(undefined, Method.Get, path, params).then(async (res) => { // TODO types
|
||||
if (!res.event) {
|
||||
throw new Error("'event' not in '/context' result - homeserver too old?");
|
||||
}
|
||||
@@ -5115,7 +5193,7 @@ export class MatrixClient extends EventEmitter {
|
||||
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(matrixEvents);
|
||||
|
||||
timelineSet.addEventsToTimeline(timelineEvents, true, timeline, res.start);
|
||||
this.processThreadEvents(timelineSet.room, threadedEvents);
|
||||
await this.processThreadEvents(timelineSet.room, threadedEvents);
|
||||
|
||||
// there is no guarantee that the event ended up in "timeline" (we
|
||||
// might have switched to a neighbouring timeline) - so check the
|
||||
@@ -5230,7 +5308,7 @@ export class MatrixClient extends EventEmitter {
|
||||
|
||||
promise = this.http.authedRequest<any>( // TODO types
|
||||
undefined, Method.Get, path, params, undefined,
|
||||
).then((res) => {
|
||||
).then(async (res) => {
|
||||
const token = res.next_token;
|
||||
const matrixEvents = [];
|
||||
|
||||
@@ -5248,7 +5326,7 @@ export class MatrixClient extends EventEmitter {
|
||||
|
||||
const timelineSet = eventTimeline.getTimelineSet();
|
||||
timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
|
||||
this.processThreadEvents(timelineSet.room, threadedEvents);
|
||||
await this.processThreadEvents(timelineSet.room, threadedEvents);
|
||||
|
||||
// if we've hit the end of the timeline, we need to stop trying to
|
||||
// paginate. We need to keep the 'forwards' token though, to make sure
|
||||
@@ -5273,7 +5351,7 @@ export class MatrixClient extends EventEmitter {
|
||||
opts.limit,
|
||||
dir,
|
||||
eventTimeline.getFilter(),
|
||||
).then((res) => {
|
||||
).then(async (res) => {
|
||||
if (res.state) {
|
||||
const roomState = eventTimeline.getState(dir);
|
||||
const stateEvents = res.state.map(this.getEventMapper());
|
||||
@@ -5286,7 +5364,7 @@ export class MatrixClient extends EventEmitter {
|
||||
|
||||
eventTimeline.getTimelineSet()
|
||||
.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
|
||||
this.processThreadEvents(room, threadedEvents);
|
||||
await this.processThreadEvents(room, threadedEvents);
|
||||
|
||||
// if we've hit the end of the timeline, we need to stop trying to
|
||||
// paginate. We need to keep the 'forwards' token though, to make sure
|
||||
@@ -6557,13 +6635,18 @@ export class MatrixClient extends EventEmitter {
|
||||
* Check whether a username is available prior to registration. An error response
|
||||
* indicates an invalid/unavailable username.
|
||||
* @param {string} username The username to check the availability of.
|
||||
* @return {Promise} Resolves: to `true`.
|
||||
* @return {Promise} Resolves: to boolean of whether the username is available.
|
||||
*/
|
||||
public isUsernameAvailable(username: string): Promise<true> {
|
||||
public isUsernameAvailable(username: string): Promise<boolean> {
|
||||
return this.http.authedRequest<{ available: true }>(
|
||||
undefined, Method.Get, '/register/available', { username: username },
|
||||
undefined, Method.Get, '/register/available', { username },
|
||||
).then((response) => {
|
||||
return response.available;
|
||||
}).catch(response => {
|
||||
if (response.errcode === "M_USER_IN_USE") {
|
||||
return false;
|
||||
}
|
||||
return Promise.reject(response);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8982,33 +9065,53 @@ export class MatrixClient extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(events: MatrixEvent[]): [MatrixEvent[], MatrixEvent[]] {
|
||||
// Indices to the events array, for readibility
|
||||
const ROOM = 0;
|
||||
const THREAD = 1;
|
||||
const threadRoots = new Set<string>();
|
||||
if (this.supportsExperimentalThreads()) {
|
||||
const threadRoots = this.findThreadRoots(events);
|
||||
return events.reduce((memo, event: MatrixEvent) => {
|
||||
const room = this.getRoom(event.getRoomId());
|
||||
// An event should live in the thread timeline if
|
||||
// - It's a reply in thread event
|
||||
// - It's related to a reply in thread event
|
||||
let shouldLiveInThreadTimeline = event.isThreadRelation;
|
||||
if (shouldLiveInThreadTimeline) {
|
||||
threadRoots.add(event.relationEventId);
|
||||
} else {
|
||||
if (!shouldLiveInThreadTimeline) {
|
||||
const parentEventId = event.parentEventId;
|
||||
const parentEvent = room?.findEventById(parentEventId) || events.find((mxEv: MatrixEvent) => {
|
||||
return mxEv.getId() === parentEventId;
|
||||
});
|
||||
shouldLiveInThreadTimeline = parentEvent?.isThreadRelation;
|
||||
|
||||
// Copy all the reactions and annotations to the root event
|
||||
// to the thread timeline. They will end up living in both
|
||||
// timelines at the same time
|
||||
const targetingThreadRoot = parentEvent?.isThreadRoot || threadRoots.has(event.relationEventId);
|
||||
|
||||
if (targetingThreadRoot && !event.isThreadRelation && event.relationEventId) {
|
||||
memo[THREAD].push(event);
|
||||
// If we refer to the thread root, we should be copied
|
||||
// into the thread as well as the main timeline.
|
||||
// This happens for reactions, annotations, poll votes etc.
|
||||
const copiedEvent = event.toSnapshot();
|
||||
|
||||
// The copied event is in this thread:
|
||||
copiedEvent.setThreadId(parentEventId);
|
||||
memo[THREAD].push(copiedEvent);
|
||||
} else if (parentEvent?.isThreadRelation) {
|
||||
// If our parent is in a thread, we are in that
|
||||
// same thread too. (E.g. if I reply within a thread.)
|
||||
shouldLiveInThreadTimeline = true;
|
||||
event.setThreadId(parentEvent.threadRootId);
|
||||
}
|
||||
}
|
||||
const targetTimeline = shouldLiveInThreadTimeline ? THREAD : ROOM;
|
||||
@@ -9028,12 +9131,11 @@ export class MatrixClient extends EventEmitter {
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
public processThreadEvents(room: Room, threadedEvents: MatrixEvent[]): void {
|
||||
threadedEvents
|
||||
.sort((a, b) => a.getTs() - b.getTs())
|
||||
.forEach(event => {
|
||||
room.addThreadedEvent(event);
|
||||
});
|
||||
public async processThreadEvents(room: Room, threadedEvents: MatrixEvent[]): Promise<void> {
|
||||
threadedEvents.sort((a, b) => a.getTs() - b.getTs());
|
||||
for (const event of threadedEvents) {
|
||||
await room.addThreadedEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+14
-34
@@ -1,6 +1,5 @@
|
||||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2018 - 2021 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2018 - 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -19,8 +18,13 @@ limitations under the License.
|
||||
|
||||
import { MsgType } from "./@types/event";
|
||||
import { TEXT_NODE_TYPE } from "./@types/extensible_events";
|
||||
import { ILocationContent, LOCATION_EVENT_TYPE, TIMESTAMP_NODE_TYPE } from "./@types/location";
|
||||
import { IPollContent, POLL_START_EVENT_TYPE } from "./@types/polls";
|
||||
import {
|
||||
ASSET_NODE_TYPE,
|
||||
ASSET_TYPE_SELF,
|
||||
ILocationContent,
|
||||
LOCATION_EVENT_TYPE,
|
||||
TIMESTAMP_NODE_TYPE,
|
||||
} from "./@types/location";
|
||||
|
||||
/**
|
||||
* Generates the content for a HTML Message event
|
||||
@@ -103,35 +107,6 @@ export function makeEmoteMessage(body: string) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the content for a Poll Start event
|
||||
* @param question the poll question
|
||||
* @param answers the possible answers
|
||||
* @param kind whether the poll is disclosed or undisclosed. Allowed values are
|
||||
* "m.poll.disclosed" or "m.poll.undisclosed", or the unstable equivalents
|
||||
*/
|
||||
export function makePollContent(
|
||||
question: string,
|
||||
answers: string[],
|
||||
kind: string,
|
||||
): IPollContent {
|
||||
question = question.trim();
|
||||
answers = answers.map(a => a.trim()).filter(a => !!a);
|
||||
return {
|
||||
[TEXT_NODE_TYPE.name]:
|
||||
`${question}\n${answers.map((a, i) => `${i + 1}. ${a}`).join('\n')}`,
|
||||
[POLL_START_EVENT_TYPE.name]: {
|
||||
kind: kind,
|
||||
question: {
|
||||
[TEXT_NODE_TYPE.name]: question,
|
||||
},
|
||||
answers: answers.map(
|
||||
(a, i) => ({ id: `${i}-${a}`, [TEXT_NODE_TYPE.name]: a }),
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the content for a Location event
|
||||
* @param text a text for of our location
|
||||
@@ -139,12 +114,14 @@ export function makePollContent(
|
||||
* @param ts the timestamp when the location was correct (milliseconds since
|
||||
* the UNIX epoch)
|
||||
* @param description the (optional) label for this location on the map
|
||||
* @param asset_type the (optional) asset type of this location e.g. "m.self"
|
||||
*/
|
||||
export function makeLocationContent(
|
||||
text: string,
|
||||
uri: string,
|
||||
ts: number,
|
||||
description?: string,
|
||||
assetType?: string,
|
||||
): ILocationContent {
|
||||
return {
|
||||
"body": text,
|
||||
@@ -154,8 +131,11 @@ export function makeLocationContent(
|
||||
uri,
|
||||
description,
|
||||
},
|
||||
[TIMESTAMP_NODE_TYPE.name]: ts,
|
||||
[ASSET_NODE_TYPE.name]: {
|
||||
type: assetType ?? ASSET_TYPE_SELF,
|
||||
},
|
||||
[TEXT_NODE_TYPE.name]: text,
|
||||
[TIMESTAMP_NODE_TYPE.name]: ts,
|
||||
// TODO: MSC1767 fallbacks m.image thumbnail
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2728,7 +2728,6 @@ export class Crypto extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-disable valid-jsdoc */ //https://github.com/eslint/eslint/issues/7307
|
||||
/**
|
||||
* Encrypt an event according to the configuration of the room.
|
||||
*
|
||||
@@ -2739,8 +2738,6 @@ export class Crypto extends EventEmitter {
|
||||
* @return {Promise?} Promise which resolves when the event has been
|
||||
* encrypted, or null if nothing was needed
|
||||
*/
|
||||
/* eslint-enable valid-jsdoc */
|
||||
// TODO this return type lies
|
||||
public async encryptEvent(event: MatrixEvent, room: Room): Promise<void> {
|
||||
if (!room) {
|
||||
throw new Error("Cannot send encrypted messages in unknown rooms");
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
|
||||
}
|
||||
}
|
||||
if (!preventReEmit) {
|
||||
client.reEmitter.reEmit(event, ["Event.replaced"]);
|
||||
client.reEmitter.reEmit(event, ["Event.replaced", "Event.visibilityChange"]);
|
||||
}
|
||||
return event;
|
||||
}
|
||||
|
||||
+48
-4
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { RelationType } from "./@types/event";
|
||||
import { UNSTABLE_FILTER_RELATION_SENDERS, UNSTABLE_FILTER_RELATION_TYPES } from "./filter";
|
||||
import { MatrixEvent } from "./models/event";
|
||||
|
||||
@@ -47,6 +48,7 @@ export interface IFilterComponent {
|
||||
not_senders?: string[];
|
||||
contains_url?: boolean;
|
||||
limit?: number;
|
||||
[UNSTABLE_FILTER_RELATION_TYPES.name]?: Array<RelationType | string>;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
@@ -62,7 +64,7 @@ export interface IFilterComponent {
|
||||
* @param {Object} filterJson the definition of this filter JSON, e.g. { 'contains_url': true }
|
||||
*/
|
||||
export class FilterComponent {
|
||||
constructor(private filterJson: IFilterComponent) {}
|
||||
constructor(private filterJson: IFilterComponent, public readonly userId?: string) {}
|
||||
|
||||
/**
|
||||
* Checks with the filter component matches the given event
|
||||
@@ -70,11 +72,24 @@ export class FilterComponent {
|
||||
* @return {boolean} true if the event matches the filter
|
||||
*/
|
||||
public check(event: MatrixEvent): boolean {
|
||||
const bundledRelationships = event.getUnsigned()?.["m.relations"] || {};
|
||||
const relations: Array<string | RelationType> = Object.keys(bundledRelationships);
|
||||
// Relation senders allows in theory a look-up of any senders
|
||||
// however clients can only know about the current user participation status
|
||||
// as sending a whole list of participants could be proven problematic in terms
|
||||
// of performance
|
||||
// This should be improved when bundled relationships solve that problem
|
||||
const relationSenders = [];
|
||||
if (this.userId && relations?.[RelationType.Thread]?.current_user_participated) {
|
||||
relationSenders.push(this.userId);
|
||||
}
|
||||
return this.checkFields(
|
||||
event.getRoomId(),
|
||||
event.getSender(),
|
||||
event.getType(),
|
||||
event.getContent() ? event.getContent().url !== undefined : false,
|
||||
relations,
|
||||
relationSenders,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -101,9 +116,18 @@ export class FilterComponent {
|
||||
* @param {String} sender the sender of the event being checked
|
||||
* @param {String} eventType the type of the event being checked
|
||||
* @param {boolean} containsUrl whether the event contains a content.url field
|
||||
* @param {boolean} relationTypes whether has aggregated relation of the given type
|
||||
* @param {boolean} relationSenders whether one of the relation is sent by the user listed
|
||||
* @return {boolean} true if the event fields match the filter
|
||||
*/
|
||||
private checkFields(roomId: string, sender: string, eventType: string, containsUrl: boolean): boolean {
|
||||
private checkFields(
|
||||
roomId: string,
|
||||
sender: string,
|
||||
eventType: string,
|
||||
containsUrl: boolean,
|
||||
relationTypes: Array<RelationType | string>,
|
||||
relationSenders: string[],
|
||||
): boolean {
|
||||
const literalKeys = {
|
||||
"rooms": function(v: string): boolean {
|
||||
return roomId === v;
|
||||
@@ -136,15 +160,35 @@ export class FilterComponent {
|
||||
return false;
|
||||
}
|
||||
|
||||
const relationTypesFilter = this.filterJson[UNSTABLE_FILTER_RELATION_TYPES.name];
|
||||
if (relationTypesFilter !== undefined) {
|
||||
if (!this.arrayMatchesFilter(relationTypesFilter, relationTypes)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const relationSendersFilter = this.filterJson[UNSTABLE_FILTER_RELATION_SENDERS.name];
|
||||
if (relationSendersFilter !== undefined) {
|
||||
if (!this.arrayMatchesFilter(relationSendersFilter, relationSenders)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private arrayMatchesFilter(filter: any[], values: any[]): boolean {
|
||||
return values.length > 0 && values.every(value => {
|
||||
return filter.includes(value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters a list of events down to those which match this filter component
|
||||
* @param {MatrixEvent[]} events Events to be checked against the filter component
|
||||
* @return {MatrixEvent[]} events which matched the filter component
|
||||
*/
|
||||
filter(events: MatrixEvent[]): MatrixEvent[] {
|
||||
public filter(events: MatrixEvent[]): MatrixEvent[] {
|
||||
return events.filter(this.check, this);
|
||||
}
|
||||
|
||||
@@ -153,7 +197,7 @@ export class FilterComponent {
|
||||
* 10 if none is otherwise specified. Cargo-culted from Synapse.
|
||||
* @return {Number} the limit for this filter component.
|
||||
*/
|
||||
limit(): number {
|
||||
public limit(): number {
|
||||
return this.filterJson.limit !== undefined ? this.filterJson.limit : 10;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -185,8 +185,8 @@ export class Filter {
|
||||
}
|
||||
}
|
||||
|
||||
this.roomFilter = new FilterComponent(roomFilterFields);
|
||||
this.roomTimelineFilter = new FilterComponent(roomFilterJson?.timeline || {});
|
||||
this.roomFilter = new FilterComponent(roomFilterFields, this.userId);
|
||||
this.roomTimelineFilter = new FilterComponent(roomFilterJson?.timeline || {}, this.userId);
|
||||
|
||||
// don't bother porting this from synapse yet:
|
||||
// this._room_state_filter =
|
||||
|
||||
+1
-1
@@ -508,7 +508,7 @@ export class MatrixHttpApi {
|
||||
if (method === Method.Get) {
|
||||
opts.qs = params;
|
||||
} else if (typeof params === "object") {
|
||||
opts.json = !!params; // XXX: this feels strange
|
||||
opts.json = params;
|
||||
}
|
||||
|
||||
if (accessToken) {
|
||||
|
||||
@@ -53,6 +53,11 @@ export enum DuplicateStrategy {
|
||||
Replace = "replace",
|
||||
}
|
||||
|
||||
export interface IRoomTimelineData {
|
||||
timeline: EventTimeline;
|
||||
liveEvent?: boolean;
|
||||
}
|
||||
|
||||
export class EventTimelineSet extends EventEmitter {
|
||||
private readonly timelineSupport: boolean;
|
||||
private unstableClientRelationAggregation: boolean;
|
||||
@@ -594,7 +599,7 @@ export class EventTimelineSet extends EventEmitter {
|
||||
this.setRelationsTarget(event);
|
||||
this.aggregateRelations(event);
|
||||
|
||||
const data = {
|
||||
const data: IRoomTimelineData = {
|
||||
timeline: timeline,
|
||||
liveEvent: !toStartOfTimeline && timeline == this.liveTimeline && !fromCache,
|
||||
};
|
||||
|
||||
+246
-25
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2015 - 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -21,6 +21,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { ExtensibleEvent, ExtensibleEvents, Optional } from "matrix-events-sdk";
|
||||
|
||||
import { logger } from '../logger';
|
||||
import { VerificationRequest } from "../crypto/verification/request/VerificationRequest";
|
||||
@@ -28,6 +29,7 @@ import {
|
||||
EventType,
|
||||
MsgType,
|
||||
RelationType,
|
||||
EVENT_VISIBILITY_CHANGE_TYPE,
|
||||
} from "../@types/event";
|
||||
import { Crypto, IEventDecryptionResult } from "../crypto";
|
||||
import { deepSortedObjectEntries } from "../utils";
|
||||
@@ -89,6 +91,13 @@ export interface IUnsigned {
|
||||
redacted_because?: IEvent;
|
||||
transaction_id?: string;
|
||||
invite_room_state?: StrippedState[];
|
||||
"m.relations"?: Record<RelationType | string, any>; // No common pattern for aggregated relations
|
||||
}
|
||||
|
||||
export interface IThreadBundledRelationship {
|
||||
latest_event: IEvent;
|
||||
count: number;
|
||||
current_user_participated?: boolean;
|
||||
}
|
||||
|
||||
export interface IEvent {
|
||||
@@ -110,7 +119,7 @@ export interface IEvent {
|
||||
age?: number;
|
||||
}
|
||||
|
||||
interface IAggregatedRelation {
|
||||
export interface IAggregatedRelation {
|
||||
origin_server_ts: number;
|
||||
event_id?: string;
|
||||
sender?: string;
|
||||
@@ -122,9 +131,42 @@ interface IAggregatedRelation {
|
||||
export interface IEventRelation {
|
||||
rel_type: RelationType | string;
|
||||
event_id: string;
|
||||
"m.in_reply_to"?: {
|
||||
event_id: string;
|
||||
"m.render_in"?: string[];
|
||||
};
|
||||
key?: string;
|
||||
}
|
||||
|
||||
export interface IVisibilityEventRelation extends IEventRelation {
|
||||
visibility: "visible" | "hidden";
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* When an event is a visibility change event, as per MSC3531,
|
||||
* the visibility change implied by the event.
|
||||
*/
|
||||
export interface IVisibilityChange {
|
||||
/**
|
||||
* If `true`, the target event should be made visible.
|
||||
* Otherwise, it should be hidden.
|
||||
*/
|
||||
visible: boolean;
|
||||
|
||||
/**
|
||||
* The event id affected.
|
||||
*/
|
||||
eventId: string;
|
||||
|
||||
/**
|
||||
* Optionally, a human-readable reason explaining why
|
||||
* the event was hidden. Ignored if the event was made
|
||||
* visible.
|
||||
*/
|
||||
reason: string | null;
|
||||
}
|
||||
|
||||
export interface IClearEvent {
|
||||
room_id?: string;
|
||||
type: string;
|
||||
@@ -143,6 +185,30 @@ export interface IDecryptOptions {
|
||||
isRetry?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Message hiding, as specified by https://github.com/matrix-org/matrix-doc/pull/3531.
|
||||
*/
|
||||
export type MessageVisibility = IMessageVisibilityHidden | IMessageVisibilityVisible;
|
||||
/**
|
||||
* Variant of `MessageVisibility` for the case in which the message should be displayed.
|
||||
*/
|
||||
export interface IMessageVisibilityVisible {
|
||||
readonly visible: true;
|
||||
}
|
||||
/**
|
||||
* Variant of `MessageVisibility` for the case in which the message should be hidden.
|
||||
*/
|
||||
export interface IMessageVisibilityHidden {
|
||||
readonly visible: false;
|
||||
/**
|
||||
* Optionally, a human-readable reason to show to the user indicating why the
|
||||
* message has been hidden (e.g. "Message Pending Moderation").
|
||||
*/
|
||||
readonly reason: string | null;
|
||||
}
|
||||
// A singleton implementing `IMessageVisibilityVisible`.
|
||||
const MESSAGE_VISIBLE: IMessageVisibilityVisible = Object.freeze({ visible: true });
|
||||
|
||||
export class MatrixEvent extends EventEmitter {
|
||||
private pushActions: IActionsObject = null;
|
||||
private _replacingEvent: MatrixEvent = null;
|
||||
@@ -150,6 +216,18 @@ export class MatrixEvent extends EventEmitter {
|
||||
private _isCancelled = false;
|
||||
private clearEvent?: IClearEvent;
|
||||
|
||||
/* Message hiding, as specified by https://github.com/matrix-org/matrix-doc/pull/3531.
|
||||
|
||||
Note: We're returning this object, so any value stored here MUST be frozen.
|
||||
*/
|
||||
private visibility: MessageVisibility = MESSAGE_VISIBLE;
|
||||
|
||||
// Not all events will be extensible-event compatible, so cache a flag in
|
||||
// addition to a falsy cached event value. We check the flag later on in
|
||||
// a public getter to decide if the cache is valid.
|
||||
private _hasCachedExtEv = false;
|
||||
private _cachedExtEv: Optional<ExtensibleEvent> = undefined;
|
||||
|
||||
/* curve25519 key which we believe belongs to the sender of the event. See
|
||||
* getSenderKey()
|
||||
*/
|
||||
@@ -191,6 +269,7 @@ export class MatrixEvent extends EventEmitter {
|
||||
* A reference to the thread this event belongs to
|
||||
*/
|
||||
private thread: Thread = null;
|
||||
private threadId: string;
|
||||
|
||||
/* Set an approximate timestamp for the event relative the local clock.
|
||||
* This will inherently be approximate because it doesn't take into account
|
||||
@@ -198,7 +277,7 @@ export class MatrixEvent extends EventEmitter {
|
||||
* it to us and the time we're now constructing this event, but that's better
|
||||
* than assuming the local clock is in sync with the origin HS's clock.
|
||||
*/
|
||||
private readonly localTimestamp: number;
|
||||
public localTimestamp: number;
|
||||
|
||||
// XXX: these should be read-only
|
||||
public sender: RoomMember = null;
|
||||
@@ -263,10 +342,29 @@ export class MatrixEvent extends EventEmitter {
|
||||
});
|
||||
|
||||
this.txnId = event.txn_id || null;
|
||||
this.localTimestamp = Date.now() - this.getAge();
|
||||
this.localTimestamp = Date.now() - (this.getAge() ?? 0);
|
||||
this.reEmitter = new ReEmitter(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unstable getter to try and get an extensible event. Note that this might
|
||||
* return a falsy value if the event could not be parsed as an extensible
|
||||
* event.
|
||||
*
|
||||
* @deprecated Use stable functions where possible.
|
||||
*/
|
||||
public get unstableExtensibleEvent(): Optional<ExtensibleEvent> {
|
||||
if (!this._hasCachedExtEv) {
|
||||
this._cachedExtEv = ExtensibleEvents.parse(this.getEffectiveEvent());
|
||||
}
|
||||
return this._cachedExtEv;
|
||||
}
|
||||
|
||||
private invalidateExtensibleEvent() {
|
||||
// just reset the flag - that'll trick the getter into parsing a new event
|
||||
this._hasCachedExtEv = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the event as though it would appear unencrypted. If the event is already not
|
||||
* encrypted, it is simply returned as-is.
|
||||
@@ -339,10 +437,10 @@ export class MatrixEvent extends EventEmitter {
|
||||
/**
|
||||
* Get the room_id for this event. This will return <code>undefined</code>
|
||||
* for <code>m.presence</code> events.
|
||||
* @return {string} The room ID, e.g. <code>!cURbafjkfsMDVwdRDQ:matrix.org
|
||||
* @return {string?} The room ID, e.g. <code>!cURbafjkfsMDVwdRDQ:matrix.org
|
||||
* </code>
|
||||
*/
|
||||
public getRoomId(): string {
|
||||
public getRoomId(): string | undefined {
|
||||
return this.event.room_id;
|
||||
}
|
||||
|
||||
@@ -409,10 +507,13 @@ export class MatrixEvent extends EventEmitter {
|
||||
* @experimental
|
||||
* Get the event ID of the thread head
|
||||
*/
|
||||
public get threadRootId(): string {
|
||||
public get threadRootId(): string | undefined {
|
||||
const relatesTo = this.getWireContent()?.["m.relates_to"];
|
||||
if (relatesTo?.rel_type === RelationType.Thread) {
|
||||
return relatesTo.event_id;
|
||||
} else {
|
||||
return this.threadId
|
||||
|| this.getThread()?.id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -420,17 +521,20 @@ export class MatrixEvent extends EventEmitter {
|
||||
* @experimental
|
||||
*/
|
||||
public get isThreadRelation(): boolean {
|
||||
return !!this.threadRootId;
|
||||
return !!this.threadRootId && this.threadId !== this.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
public get isThreadRoot(): boolean {
|
||||
// TODO, change the inner working of this getter for it to use the
|
||||
// bundled relationship return on the event, view MSC3440
|
||||
const thread = this.getThread();
|
||||
return thread?.id === this.getId();
|
||||
const threadDetails = this
|
||||
.getServerAggregatedRelation<IThreadBundledRelationship>(RelationType.Thread);
|
||||
|
||||
// Bundled relationships only returned when the sync response is limited
|
||||
// hence us having to check both bundled relation and inspect the thread
|
||||
// model
|
||||
return !!threadDetails || (this.getThread()?.id === this.getId());
|
||||
}
|
||||
|
||||
public get parentEventId(): string {
|
||||
@@ -438,8 +542,13 @@ export class MatrixEvent extends EventEmitter {
|
||||
}
|
||||
|
||||
public get replyEventId(): string {
|
||||
const relations = this.getWireContent()["m.relates_to"];
|
||||
return relations?.["m.in_reply_to"]?.["event_id"];
|
||||
// We're prefer ev.getContent() over ev.getWireContent() to make sure
|
||||
// we grab the latest edit with potentially new relations. But we also
|
||||
// can't just rely on ev.getContent() by itself because historically we
|
||||
// still show the reply from the original message even though the edit
|
||||
// event does not include the relation reply.
|
||||
const mRelatesTo = this.getContent()['m.relates_to'] || this.getWireContent()['m.relates_to'];
|
||||
return mRelatesTo?.['m.in_reply_to']?.event_id;
|
||||
}
|
||||
|
||||
public get relationEventId(): string {
|
||||
@@ -475,9 +584,10 @@ export class MatrixEvent extends EventEmitter {
|
||||
* Get the age of this event. This represents the age of the event when the
|
||||
* event arrived at the device, and not the age of the event when this
|
||||
* function was called.
|
||||
* @return {Number} The age of this event in milliseconds.
|
||||
* Can only be returned once the server has echo'ed back
|
||||
* @return {Number|undefined} The age of this event in milliseconds.
|
||||
*/
|
||||
public getAge(): number {
|
||||
public getAge(): number | undefined {
|
||||
return this.getUnsigned().age || this.event.age; // v2 / v1
|
||||
}
|
||||
|
||||
@@ -570,7 +680,12 @@ export class MatrixEvent extends EventEmitter {
|
||||
}
|
||||
|
||||
public shouldAttemptDecryption() {
|
||||
return this.isEncrypted() && !this.isBeingDecrypted() && !this.clearEvent;
|
||||
if (this.isRedacted()) return false;
|
||||
if (this.isBeingDecrypted()) return false;
|
||||
if (this.clearEvent) return false;
|
||||
if (!this.isEncrypted()) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -801,6 +916,7 @@ export class MatrixEvent extends EventEmitter {
|
||||
this.forwardingCurve25519KeyChain =
|
||||
decryptionResult.forwardingCurve25519KeyChain || [];
|
||||
this.untrusted = decryptionResult.untrusted || false;
|
||||
this.invalidateExtensibleEvent();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -904,6 +1020,10 @@ export class MatrixEvent extends EventEmitter {
|
||||
return this.event.unsigned || {};
|
||||
}
|
||||
|
||||
public setUnsigned(unsigned: IUnsigned): void {
|
||||
this.event.unsigned = unsigned;
|
||||
}
|
||||
|
||||
public unmarkLocallyRedacted(): boolean {
|
||||
const value = this._localRedactionEvent;
|
||||
this._localRedactionEvent = null;
|
||||
@@ -923,6 +1043,53 @@ export class MatrixEvent extends EventEmitter {
|
||||
this.event.unsigned.redacted_because = redactionEvent.event as IEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the visibility of an event, as per https://github.com/matrix-org/matrix-doc/pull/3531 .
|
||||
*
|
||||
* @fires module:models/event.MatrixEvent#"Event.visibilityChange" if `visibilityEvent`
|
||||
* caused a change in the actual visibility of this event, either by making it
|
||||
* visible (if it was hidden), by making it hidden (if it was visible) or by
|
||||
* changing the reason (if it was hidden).
|
||||
* @param visibilityEvent event holding a hide/unhide payload, or nothing
|
||||
* if the event is being reset to its original visibility (presumably
|
||||
* by a visibility event being redacted).
|
||||
*/
|
||||
public applyVisibilityEvent(visibilityChange?: IVisibilityChange): void {
|
||||
const visible = visibilityChange ? visibilityChange.visible : true;
|
||||
const reason = visibilityChange ? visibilityChange.reason : null;
|
||||
let change = false;
|
||||
if (this.visibility.visible !== visibilityChange.visible) {
|
||||
change = true;
|
||||
} else if (!this.visibility.visible && this.visibility["reason"] !== reason) {
|
||||
change = true;
|
||||
}
|
||||
if (change) {
|
||||
if (visible) {
|
||||
this.visibility = MESSAGE_VISIBLE;
|
||||
} else {
|
||||
this.visibility = Object.freeze({
|
||||
visible: false,
|
||||
reason: reason,
|
||||
});
|
||||
}
|
||||
if (change) {
|
||||
this.emit("Event.visibilityChange", this, visible);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return instructions to display or hide the message.
|
||||
*
|
||||
* @returns Instructions determining whether the message
|
||||
* should be displayed.
|
||||
*/
|
||||
public messageVisibility(): MessageVisibility {
|
||||
// Note: We may return `this.visibility` without fear, as
|
||||
// this is a shallow frozen object.
|
||||
return this.visibility;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the content of an event in the same way it would be by the server
|
||||
* if it were redacted before it was sent to us
|
||||
@@ -972,6 +1139,8 @@ export class MatrixEvent extends EventEmitter {
|
||||
delete content[key];
|
||||
}
|
||||
}
|
||||
|
||||
this.invalidateExtensibleEvent();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -992,6 +1161,54 @@ export class MatrixEvent extends EventEmitter {
|
||||
return this.getType() === EventType.RoomRedaction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the visibility change caused by this event,
|
||||
* as per https://github.com/matrix-org/matrix-doc/pull/3531.
|
||||
*
|
||||
* @returns If the event is a well-formed visibility change event,
|
||||
* an instance of `IVisibilityChange`, otherwise `null`.
|
||||
*/
|
||||
public asVisibilityChange(): IVisibilityChange | null {
|
||||
if (!EVENT_VISIBILITY_CHANGE_TYPE.matches(this.getType())) {
|
||||
// Not a visibility change event.
|
||||
return null;
|
||||
}
|
||||
const relation = this.getRelation();
|
||||
if (!relation || relation.rel_type != "m.reference") {
|
||||
// Ill-formed, ignore this event.
|
||||
return null;
|
||||
}
|
||||
const eventId = relation.event_id;
|
||||
if (!eventId) {
|
||||
// Ill-formed, ignore this event.
|
||||
return null;
|
||||
}
|
||||
const content = this.getWireContent();
|
||||
const visible = !!content.visible;
|
||||
const reason = content.reason;
|
||||
if (reason && typeof reason != "string") {
|
||||
// Ill-formed, ignore this event.
|
||||
return null;
|
||||
}
|
||||
// Well-formed visibility change event.
|
||||
return {
|
||||
visible,
|
||||
reason,
|
||||
eventId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this event alters the visibility of another event,
|
||||
* as per https://github.com/matrix-org/matrix-doc/pull/3531.
|
||||
*
|
||||
* @returns {boolean} True if this event alters the visibility
|
||||
* of another event.
|
||||
*/
|
||||
public isVisibilityEvent(): boolean {
|
||||
return EVENT_VISIBILITY_CHANGE_TYPE.matches(this.getType());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the (decrypted, if necessary) redaction event JSON
|
||||
* if event was redacted
|
||||
@@ -1053,6 +1270,8 @@ export class MatrixEvent extends EventEmitter {
|
||||
// emit the event if it changed
|
||||
this.emit("Event.localEventIdReplaced", this);
|
||||
}
|
||||
|
||||
this.localTimestamp = Date.now() - this.getAge();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1127,6 +1346,7 @@ export class MatrixEvent extends EventEmitter {
|
||||
if (this._replacingEvent !== newEvent) {
|
||||
this._replacingEvent = newEvent;
|
||||
this.emit("Event.replaced", this);
|
||||
this.invalidateExtensibleEvent();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1146,11 +1366,8 @@ export class MatrixEvent extends EventEmitter {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
public getServerAggregatedRelation(relType: RelationType): IAggregatedRelation {
|
||||
const relations = this.getUnsigned()["m.relations"];
|
||||
if (relations) {
|
||||
return relations[relType];
|
||||
}
|
||||
public getServerAggregatedRelation<T>(relType: RelationType): T | undefined {
|
||||
return this.getUnsigned()["m.relations"]?.[relType];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1159,7 +1376,7 @@ export class MatrixEvent extends EventEmitter {
|
||||
* @return {string?}
|
||||
*/
|
||||
public replacingEventId(): string | undefined {
|
||||
const replaceRelation = this.getServerAggregatedRelation(RelationType.Replace);
|
||||
const replaceRelation = this.getServerAggregatedRelation<IAggregatedRelation>(RelationType.Replace);
|
||||
if (replaceRelation) {
|
||||
return replaceRelation.event_id;
|
||||
} else if (this._replacingEvent) {
|
||||
@@ -1184,7 +1401,7 @@ export class MatrixEvent extends EventEmitter {
|
||||
* @return {Date?}
|
||||
*/
|
||||
public replacingEventDate(): Date | undefined {
|
||||
const replaceRelation = this.getServerAggregatedRelation(RelationType.Replace);
|
||||
const replaceRelation = this.getServerAggregatedRelation<IAggregatedRelation>(RelationType.Replace);
|
||||
if (replaceRelation) {
|
||||
const ts = replaceRelation.origin_server_ts;
|
||||
if (Number.isFinite(ts)) {
|
||||
@@ -1350,9 +1567,13 @@ export class MatrixEvent extends EventEmitter {
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
public getThread(): Thread {
|
||||
public getThread(): Thread | undefined {
|
||||
return this.thread;
|
||||
}
|
||||
|
||||
public setThreadId(threadId: string): void {
|
||||
this.threadId = threadId;
|
||||
}
|
||||
}
|
||||
|
||||
/* REDACT_KEEP_KEYS gives the keys we keep when an event is redacted
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Relations } from "./relations";
|
||||
import { MatrixEvent } from "./event";
|
||||
|
||||
export class RelatedRelations {
|
||||
private relations: Relations[];
|
||||
|
||||
public constructor(relations: Relations[]) {
|
||||
this.relations = relations.filter(r => !!r);
|
||||
}
|
||||
|
||||
public getRelations(): MatrixEvent[] {
|
||||
return this.relations.reduce((c, p) => [...c, ...p.getRelations()], []);
|
||||
}
|
||||
|
||||
public on(ev: string, fn: (...params) => void) {
|
||||
this.relations.forEach(r => r.on(ev, fn));
|
||||
}
|
||||
|
||||
public off(ev: string, fn: (...params) => void) {
|
||||
this.relations.forEach(r => r.off(ev, fn));
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ limitations under the License.
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { EventStatus, MatrixEvent } from './event';
|
||||
import { EventStatus, MatrixEvent, IAggregatedRelation } from './event';
|
||||
import { Room } from './room';
|
||||
import { logger } from '../logger';
|
||||
import { RelationType } from "../@types/event";
|
||||
@@ -319,7 +319,7 @@ export class Relations extends EventEmitter {
|
||||
|
||||
// the all-knowning server tells us that the event at some point had
|
||||
// this timestamp for its replacement, so any following replacement should definitely not be less
|
||||
const replaceRelation = this.targetEvent.getServerAggregatedRelation(RelationType.Replace);
|
||||
const replaceRelation = this.targetEvent.getServerAggregatedRelation<IAggregatedRelation>(RelationType.Replace);
|
||||
const minTs = replaceRelation && replaceRelation.origin_server_ts;
|
||||
|
||||
const lastReplacement = this.getRelations().reduce((last, event) => {
|
||||
|
||||
@@ -618,14 +618,14 @@ export class RoomState extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given MatrixClient has permission to send a state
|
||||
* event of type `stateEventType` into this room.
|
||||
* @param {string} stateEventType The type of state events to test
|
||||
* @param {MatrixClient} cli The client to test permission for
|
||||
* @return {boolean} true if the given client should be permitted to send
|
||||
* the given type of state event into this room,
|
||||
* according to the room's state.
|
||||
*/
|
||||
* Returns true if the given MatrixClient has permission to send a state
|
||||
* event of type `stateEventType` into this room.
|
||||
* @param {string} stateEventType The type of state events to test
|
||||
* @param {MatrixClient} cli The client to test permission for
|
||||
* @return {boolean} true if the given client should be permitted to send
|
||||
* the given type of state event into this room,
|
||||
* according to the room's state.
|
||||
*/
|
||||
public mayClientSendStateEvent(stateEventType: EventType | string, cli: MatrixClient): boolean {
|
||||
if (cli.isGuest()) {
|
||||
return false;
|
||||
|
||||
+335
-91
@@ -30,7 +30,10 @@ import { RoomMember } from "./room-member";
|
||||
import { IRoomSummary, RoomSummary } from "./room-summary";
|
||||
import { logger } from '../logger';
|
||||
import { ReEmitter } from '../ReEmitter';
|
||||
import { EventType, RoomCreateTypeField, RoomType, UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../@types/event";
|
||||
import {
|
||||
EventType, RoomCreateTypeField, RoomType, UNSTABLE_ELEMENT_FUNCTIONAL_USERS,
|
||||
EVENT_VISIBILITY_CHANGE_TYPE,
|
||||
} from "../@types/event";
|
||||
import { IRoomVersionsCapability, MatrixClient, PendingEventOrdering, RoomVersionStability } from "../client";
|
||||
import { GuestAccess, HistoryVisibility, JoinRule, ResizeMethod } from "../@types/partials";
|
||||
import { Filter } from "../filter";
|
||||
@@ -93,7 +96,7 @@ interface ICachedReceipt {
|
||||
data: IReceipt;
|
||||
}
|
||||
|
||||
type ReceiptCache = Record<string, ICachedReceipt[]>;
|
||||
type ReceiptCache = {[eventId: string]: ICachedReceipt[]};
|
||||
|
||||
interface IReceiptContent {
|
||||
[eventId: string]: {
|
||||
@@ -103,7 +106,29 @@ interface IReceiptContent {
|
||||
};
|
||||
}
|
||||
|
||||
type Receipts = Record<string, Record<string, IWrappedReceipt>>;
|
||||
const ReceiptPairRealIndex = 0;
|
||||
const ReceiptPairSyntheticIndex = 1;
|
||||
type Receipts = {
|
||||
[receiptType: string]: {
|
||||
[userId: string]: [IWrappedReceipt, IWrappedReceipt]; // Pair<real receipt, synthetic receipt> (both nullable)
|
||||
};
|
||||
};
|
||||
|
||||
// When inserting a visibility event affecting event `eventId`, we
|
||||
// need to scan through existing visibility events for `eventId`.
|
||||
// In theory, this could take an unlimited amount of time if:
|
||||
//
|
||||
// - the visibility event was sent by a moderator; and
|
||||
// - `eventId` already has many visibility changes (usually, it should
|
||||
// be 2 or less); and
|
||||
// - for some reason, the visibility changes are received out of order
|
||||
// (usually, this shouldn't happen at all).
|
||||
//
|
||||
// For this reason, we limit the number of events to scan through,
|
||||
// expecting that a broken visibility change for a single event in
|
||||
// an extremely uncommon case (possibly a DoS) is a small
|
||||
// price to pay to keep matrix-js-sdk responsive.
|
||||
const MAX_NUMBER_OF_VISIBILITY_EVENTS_TO_SCAN_THROUGH = 30;
|
||||
|
||||
export enum NotificationCountType {
|
||||
Highlight = "highlight",
|
||||
@@ -126,8 +151,6 @@ export class Room extends EventEmitter {
|
||||
// a pre-cached list for this purpose.
|
||||
private receipts: Receipts = {}; // { receipt_type: { user_id: IReceipt } }
|
||||
private receiptCacheByEventId: ReceiptCache = {}; // { event_id: IReceipt2[] }
|
||||
// only receipts that came from the server, not synthesized ones
|
||||
private realReceipts: Receipts = {};
|
||||
private notificationCounts: Partial<Record<NotificationCountType, number>> = {};
|
||||
private readonly timelineSets: EventTimelineSet[];
|
||||
// any filtered timeline sets we're maintaining for this room
|
||||
@@ -192,6 +215,25 @@ export class Room extends EventEmitter {
|
||||
* @experimental
|
||||
*/
|
||||
public threads = new Map<string, Thread>();
|
||||
public lastThread: Thread;
|
||||
|
||||
/**
|
||||
* A mapping of eventId to all visibility changes to apply
|
||||
* to the event, by chronological order, as per
|
||||
* https://github.com/matrix-org/matrix-doc/pull/3531
|
||||
*
|
||||
* # Invariants
|
||||
*
|
||||
* - within each list, all events are classed by
|
||||
* chronological order;
|
||||
* - all events are events such that
|
||||
* `asVisibilityEvent()` returns a non-null `IVisibilityChange`;
|
||||
* - within each list with key `eventId`, all events
|
||||
* are in relation to `eventId`.
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
private visibilityEvents = new Map<string, MatrixEvent[]>();
|
||||
|
||||
/**
|
||||
* Construct a new Room.
|
||||
@@ -253,7 +295,9 @@ export class Room extends EventEmitter {
|
||||
// all our per-room timeline sets. the first one is the unfiltered ones;
|
||||
// the subsequent ones are the filtered ones in no particular order.
|
||||
this.timelineSets = [new EventTimelineSet(this, opts)];
|
||||
this.reEmitter.reEmit(this.getUnfilteredTimelineSet(), ["Room.timeline", "Room.timelineReset"]);
|
||||
this.reEmitter.reEmit(this.getUnfilteredTimelineSet(), [
|
||||
"Room.timeline", "Room.timelineReset",
|
||||
]);
|
||||
|
||||
this.fixUpLegacyTimelineFields();
|
||||
|
||||
@@ -1304,7 +1348,7 @@ export class Room extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
public findThreadForEvent(event: MatrixEvent): Thread {
|
||||
public findThreadForEvent(event: MatrixEvent): Thread | null {
|
||||
if (!event) {
|
||||
return null;
|
||||
}
|
||||
@@ -1332,12 +1376,24 @@ export class Room extends EventEmitter {
|
||||
let rootEvent = this.findEventById(event.threadRootId);
|
||||
// If the rootEvent does not exist in the current sync, then look for
|
||||
// it over the network
|
||||
if (!rootEvent) {
|
||||
const eventData = await this.client.fetchRoomEvent(this.roomId, event.threadRootId);
|
||||
rootEvent = new MatrixEvent(eventData);
|
||||
try {
|
||||
let eventData;
|
||||
if (event.threadRootId) {
|
||||
eventData = await this.client.fetchRoomEvent(this.roomId, event.threadRootId);
|
||||
}
|
||||
|
||||
if (!rootEvent) {
|
||||
rootEvent = new MatrixEvent(eventData);
|
||||
} else {
|
||||
rootEvent.setUnsigned(eventData.unsigned);
|
||||
}
|
||||
} finally {
|
||||
// The root event might be not be visible to the person requesting
|
||||
// it. If it wasn't fetched successfully the thread will work
|
||||
// in "limited" mode and won't benefit from all the APIs a homeserver
|
||||
// can provide to enhance the thread experience
|
||||
thread = this.createThread(rootEvent, events);
|
||||
}
|
||||
events.unshift(rootEvent);
|
||||
thread = this.createThread(events);
|
||||
}
|
||||
|
||||
if (event.getUnsigned().transaction_id) {
|
||||
@@ -1352,17 +1408,30 @@ export class Room extends EventEmitter {
|
||||
this.emit(ThreadEvent.Update, thread);
|
||||
}
|
||||
|
||||
public createThread(events: MatrixEvent[]): Thread {
|
||||
const thread = new Thread(events, this, this.client);
|
||||
this.threads.set(thread.id, thread);
|
||||
this.reEmitter.reEmit(thread, [
|
||||
ThreadEvent.Update,
|
||||
ThreadEvent.Ready,
|
||||
"Room.timeline",
|
||||
"Room.timelineReset",
|
||||
]);
|
||||
this.emit(ThreadEvent.New, thread);
|
||||
return thread;
|
||||
public createThread(rootEvent: MatrixEvent, events?: MatrixEvent[]): Thread | undefined {
|
||||
const thread = new Thread(rootEvent, {
|
||||
initialEvents: events,
|
||||
room: this,
|
||||
client: this.client,
|
||||
});
|
||||
// If we managed to create a thread and figure out its `id`
|
||||
// then we can use it
|
||||
if (thread.id) {
|
||||
this.threads.set(thread.id, thread);
|
||||
this.reEmitter.reEmit(thread, [
|
||||
ThreadEvent.Update,
|
||||
ThreadEvent.Ready,
|
||||
"Room.timeline",
|
||||
"Room.timelineReset",
|
||||
]);
|
||||
|
||||
if (!this.lastThread || this.lastThread.rootEvent.localTimestamp < rootEvent.localTimestamp) {
|
||||
this.lastThread = thread;
|
||||
}
|
||||
|
||||
this.emit(ThreadEvent.New, thread);
|
||||
return thread;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1402,6 +1471,15 @@ export class Room extends EventEmitter {
|
||||
// (in the sender and target fields). We should get those
|
||||
// RoomMember objects to update themselves when the events that
|
||||
// they are based on are changed.
|
||||
|
||||
// Remove any visibility change on this event.
|
||||
this.visibilityEvents.delete(redactId);
|
||||
|
||||
// If this event is a visibility change event, remove it from the
|
||||
// list of visibility changes and update any event affected by it.
|
||||
if (redactedEvent.isVisibilityEvent()) {
|
||||
this.redactVisibilityChangeEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: apply redactions to notification list
|
||||
@@ -1411,6 +1489,15 @@ export class Room extends EventEmitter {
|
||||
// this may be needed to trigger an update.
|
||||
}
|
||||
|
||||
// Implement MSC3531: hiding messages.
|
||||
if (event.isVisibilityEvent()) {
|
||||
// This event changes the visibility of another event, record
|
||||
// the visibility change, inform clients if necessary.
|
||||
this.applyNewVisibilityEvent(event);
|
||||
}
|
||||
// If any pending visibility change is waiting for this (older) event,
|
||||
this.applyPendingVisibilityEvents(event);
|
||||
|
||||
if (event.getUnsigned().transaction_id) {
|
||||
const existingEvent = this.txnToEvent[event.getUnsigned().transaction_id];
|
||||
if (existingEvent) {
|
||||
@@ -1477,7 +1564,7 @@ export class Room extends EventEmitter {
|
||||
EventTimeline.setEventMetadata(event, this.getLiveTimeline().getState(EventTimeline.FORWARDS), false);
|
||||
|
||||
this.txnToEvent[txnId] = event;
|
||||
const thread = this.threads.get(event.threadRootId);
|
||||
const thread = this.findThreadForEvent(event);
|
||||
if (this.opts.pendingEventOrdering === PendingEventOrdering.Detached && !thread) {
|
||||
if (this.pendingEventList.some((e) => e.status === EventStatus.NOT_SENT)) {
|
||||
logger.warn("Setting event as NOT_SENT due to messages in the same state");
|
||||
@@ -1506,8 +1593,7 @@ export class Room extends EventEmitter {
|
||||
}
|
||||
} else {
|
||||
if (thread) {
|
||||
thread.timelineSet.addEventToTimeline(event,
|
||||
thread.timelineSet.getLiveTimeline(), false);
|
||||
thread.addEvent(event, false);
|
||||
} else {
|
||||
for (let i = 0; i < this.timelineSets.length; i++) {
|
||||
const timelineSet = this.timelineSets[i];
|
||||
@@ -1632,7 +1718,7 @@ export class Room extends EventEmitter {
|
||||
// any, which is good, because we don't want to try decoding it again).
|
||||
localEvent.handleRemoteEcho(remoteEvent.event);
|
||||
|
||||
const thread = this.threads.get(remoteEvent.threadRootId);
|
||||
const thread = this.findThreadForEvent(remoteEvent);
|
||||
if (thread) {
|
||||
thread.timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId);
|
||||
} else {
|
||||
@@ -1793,7 +1879,7 @@ export class Room extends EventEmitter {
|
||||
// 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);
|
||||
const thread = this.threads.get(events[i].getId());
|
||||
const thread = this.findThreadForEvent(events[i]);
|
||||
if (thread) {
|
||||
thread.addEvent(events[i], true);
|
||||
}
|
||||
@@ -1899,19 +1985,11 @@ export class Room extends EventEmitter {
|
||||
}
|
||||
|
||||
public getReadReceiptForUserId(userId: string, ignoreSynthesized = false): IWrappedReceipt | null {
|
||||
let receipts = this.receipts;
|
||||
if (ignoreSynthesized) {
|
||||
receipts = this.realReceipts;
|
||||
const [realReceipt, syntheticReceipt] = this.receipts["m.read"]?.[userId] ?? [];
|
||||
if (ignoreSynthesized || realReceipt) {
|
||||
return realReceipt;
|
||||
}
|
||||
|
||||
if (
|
||||
receipts["m.read"] === undefined ||
|
||||
receipts["m.read"][userId] === undefined
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return receipts["m.read"][userId];
|
||||
return syntheticReceipt;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1975,47 +2053,44 @@ export class Room extends EventEmitter {
|
||||
/**
|
||||
* Add a receipt event to the room.
|
||||
* @param {MatrixEvent} event The m.receipt event.
|
||||
* @param {Boolean} fake True if this event is implicit
|
||||
* @param {Boolean} fake True if this event is implicit.
|
||||
*/
|
||||
public addReceipt(event: MatrixEvent, fake = false): void {
|
||||
if (!fake) {
|
||||
this.addReceiptsToStructure(event, this.realReceipts);
|
||||
// we don't bother caching real receipts by event ID
|
||||
// as there's nothing that would read it.
|
||||
}
|
||||
this.addReceiptsToStructure(event, this.receipts);
|
||||
this.receiptCacheByEventId = this.buildReceiptCache(this.receipts);
|
||||
|
||||
// send events after we've regenerated the cache, otherwise things that
|
||||
// listened for the event would read from a stale cache
|
||||
this.addReceiptsToStructure(event, fake);
|
||||
// send events after we've regenerated the structure & cache, otherwise things that
|
||||
// listened for the event would read stale data.
|
||||
this.emit("Room.receipt", event, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a receipt event to the room.
|
||||
* @param {MatrixEvent} event The m.receipt event.
|
||||
* @param {Object} receipts The object to add receipts to
|
||||
* @param {Boolean} fake True if this event is implicit.
|
||||
*/
|
||||
private addReceiptsToStructure(event: MatrixEvent, receipts: Receipts): void {
|
||||
private addReceiptsToStructure(event: MatrixEvent, fake: boolean): void {
|
||||
const content = event.getContent<IReceiptContent>();
|
||||
Object.keys(content).forEach((eventId) => {
|
||||
Object.keys(content[eventId]).forEach((receiptType) => {
|
||||
Object.keys(content[eventId][receiptType]).forEach((userId) => {
|
||||
const receipt = content[eventId][receiptType][userId];
|
||||
|
||||
if (!receipts[receiptType]) {
|
||||
receipts[receiptType] = {};
|
||||
if (!this.receipts[receiptType]) {
|
||||
this.receipts[receiptType] = {};
|
||||
}
|
||||
if (!this.receipts[receiptType][userId]) {
|
||||
this.receipts[receiptType][userId] = [null, null];
|
||||
}
|
||||
|
||||
const existingReceipt = receipts[receiptType][userId];
|
||||
const pair = this.receipts[receiptType][userId];
|
||||
|
||||
if (!existingReceipt) {
|
||||
receipts[receiptType][userId] = {} as IWrappedReceipt;
|
||||
} else {
|
||||
// we only want to add this receipt if we think it is later
|
||||
// than the one we already have. (This is managed
|
||||
// server-side, but because we synthesize RRs locally we
|
||||
// have to do it here too.)
|
||||
let existingReceipt = pair[ReceiptPairRealIndex];
|
||||
if (fake && !existingReceipt) {
|
||||
existingReceipt = pair[ReceiptPairSyntheticIndex];
|
||||
}
|
||||
|
||||
if (existingReceipt) {
|
||||
// we only want to add this receipt if we think it is later than the one we already have.
|
||||
// This is managed server-side, but because we synthesize RRs locally we have to do it here too.
|
||||
const ordering = this.getUnfilteredTimelineSet().compareEventOrdering(
|
||||
existingReceipt.eventId, eventId);
|
||||
if (ordering !== null && ordering >= 0) {
|
||||
@@ -2023,36 +2098,48 @@ export class Room extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
receipts[receiptType][userId] = {
|
||||
eventId: eventId,
|
||||
const wrappedReceipt: IWrappedReceipt = {
|
||||
eventId,
|
||||
data: receipt,
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Build and return a map of receipts by event ID
|
||||
* @param {Object} receipts A map of receipts
|
||||
* @return {Object} Map of receipts by event ID
|
||||
*/
|
||||
private buildReceiptCache(receipts: Receipts): ReceiptCache {
|
||||
const receiptCacheByEventId: ReceiptCache = {};
|
||||
Object.keys(receipts).forEach(function(receiptType) {
|
||||
Object.keys(receipts[receiptType]).forEach(function(userId) {
|
||||
const receipt = receipts[receiptType][userId];
|
||||
if (!receiptCacheByEventId[receipt.eventId]) {
|
||||
receiptCacheByEventId[receipt.eventId] = [];
|
||||
}
|
||||
receiptCacheByEventId[receipt.eventId].push({
|
||||
userId: userId,
|
||||
type: receiptType,
|
||||
data: receipt.data,
|
||||
// we don't bother caching just real receipts by event ID as there's nothing that would read it.
|
||||
const cachedReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex];
|
||||
// clean up any previous cache entry
|
||||
if (cachedReceipt && this.receiptCacheByEventId[cachedReceipt.eventId]) {
|
||||
const previousEventId = cachedReceipt.eventId;
|
||||
// Remove the receipt we're about to clobber out of existence from the cache
|
||||
this.receiptCacheByEventId[previousEventId] = (
|
||||
this.receiptCacheByEventId[previousEventId].filter(r => {
|
||||
return r.type !== receiptType || r.userId !== userId;
|
||||
})
|
||||
);
|
||||
|
||||
if (this.receiptCacheByEventId[previousEventId].length < 1) {
|
||||
delete this.receiptCacheByEventId[previousEventId]; // clean up the cache keys
|
||||
}
|
||||
}
|
||||
|
||||
// cache the new one
|
||||
if (!this.receiptCacheByEventId[eventId]) {
|
||||
this.receiptCacheByEventId[eventId] = [];
|
||||
}
|
||||
this.receiptCacheByEventId[eventId].push({
|
||||
userId: userId,
|
||||
type: receiptType,
|
||||
data: receipt,
|
||||
});
|
||||
|
||||
if (fake) {
|
||||
pair[ReceiptPairSyntheticIndex] = wrappedReceipt;
|
||||
} else {
|
||||
pair[ReceiptPairRealIndex] = wrappedReceipt;
|
||||
// a real receipt for a receiptType+userId tuple should clobber any synthetic one
|
||||
pair[ReceiptPairSyntheticIndex] = null;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
return receiptCacheByEventId;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2118,8 +2205,9 @@ export class Room extends EventEmitter {
|
||||
* message events into the room.
|
||||
*/
|
||||
public maySendMessage(): boolean {
|
||||
return this.getMyMembership() === 'join' &&
|
||||
this.currentState.maySendEvent(EventType.RoomMessage, this.myUserId);
|
||||
return this.getMyMembership() === 'join' && (this.client.isRoomEncrypted(this.roomId)
|
||||
? this.currentState.maySendEvent(EventType.RoomMessageEncrypted, this.myUserId)
|
||||
: this.currentState.maySendEvent(EventType.RoomMessage, this.myUserId));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2292,6 +2380,161 @@ export class Room extends EventEmitter {
|
||||
return "Empty room";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When we receive a new visibility change event:
|
||||
*
|
||||
* - store this visibility change alongside the timeline, in case we
|
||||
* later need to apply it to an event that we haven't received yet;
|
||||
* - if we have already received the event whose visibility has changed,
|
||||
* patch it to reflect the visibility change and inform listeners.
|
||||
*/
|
||||
private applyNewVisibilityEvent(event: MatrixEvent): void {
|
||||
const visibilityChange = event.asVisibilityChange();
|
||||
if (!visibilityChange) {
|
||||
// The event is ill-formed.
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore visibility change events that are not emitted by moderators.
|
||||
const userId = event.getSender();
|
||||
if (!userId) {
|
||||
return;
|
||||
}
|
||||
const isPowerSufficient =
|
||||
(
|
||||
EVENT_VISIBILITY_CHANGE_TYPE.name
|
||||
&& this.currentState.maySendStateEvent(EVENT_VISIBILITY_CHANGE_TYPE.name, userId)
|
||||
)
|
||||
|| (
|
||||
EVENT_VISIBILITY_CHANGE_TYPE.altName
|
||||
&& this.currentState.maySendStateEvent(EVENT_VISIBILITY_CHANGE_TYPE.altName, userId)
|
||||
);
|
||||
if (!isPowerSufficient) {
|
||||
// Powerlevel is insufficient.
|
||||
return;
|
||||
}
|
||||
|
||||
// Record this change in visibility.
|
||||
// If the event is not in our timeline and we only receive it later,
|
||||
// we may need to apply the visibility change at a later date.
|
||||
|
||||
const visibilityEventsOnOriginalEvent = this.visibilityEvents.get(visibilityChange.eventId);
|
||||
if (visibilityEventsOnOriginalEvent) {
|
||||
// It would be tempting to simply erase the latest visibility change
|
||||
// but we need to record all of the changes in case the latest change
|
||||
// is ever redacted.
|
||||
//
|
||||
// In practice, linear scans through `visibilityEvents` should be fast.
|
||||
// However, to protect against a potential DoS attack, we limit the
|
||||
// number of iterations in this loop.
|
||||
let index = visibilityEventsOnOriginalEvent.length - 1;
|
||||
const min = Math.max(0,
|
||||
visibilityEventsOnOriginalEvent.length - MAX_NUMBER_OF_VISIBILITY_EVENTS_TO_SCAN_THROUGH);
|
||||
for (; index >= min; --index) {
|
||||
const target = visibilityEventsOnOriginalEvent[index];
|
||||
if (target.getTs() < event.getTs()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (index === -1) {
|
||||
visibilityEventsOnOriginalEvent.unshift(event);
|
||||
} else {
|
||||
visibilityEventsOnOriginalEvent.splice(index + 1, 0, event);
|
||||
}
|
||||
} else {
|
||||
this.visibilityEvents.set(visibilityChange.eventId, [event]);
|
||||
}
|
||||
|
||||
// Finally, let's check if the event is already in our timeline.
|
||||
// If so, we need to patch it and inform listeners.
|
||||
|
||||
const originalEvent = this.findEventById(visibilityChange.eventId);
|
||||
if (!originalEvent) {
|
||||
return;
|
||||
}
|
||||
originalEvent.applyVisibilityEvent(visibilityChange);
|
||||
}
|
||||
|
||||
private redactVisibilityChangeEvent(event: MatrixEvent) {
|
||||
// Sanity checks.
|
||||
if (!event.isVisibilityEvent) {
|
||||
throw new Error("expected a visibility change event");
|
||||
}
|
||||
const relation = event.getRelation();
|
||||
const originalEventId = relation.event_id;
|
||||
const visibilityEventsOnOriginalEvent = this.visibilityEvents.get(originalEventId);
|
||||
if (!visibilityEventsOnOriginalEvent) {
|
||||
// No visibility changes on the original event.
|
||||
// In particular, this change event was not recorded,
|
||||
// most likely because it was ill-formed.
|
||||
return;
|
||||
}
|
||||
const index = visibilityEventsOnOriginalEvent.findIndex(change => change.getId() === event.getId());
|
||||
if (index === -1) {
|
||||
// This change event was not recorded, most likely because
|
||||
// it was ill-formed.
|
||||
return;
|
||||
}
|
||||
// Remove visibility change.
|
||||
visibilityEventsOnOriginalEvent.splice(index, 1);
|
||||
|
||||
// If we removed the latest visibility change event, propagate changes.
|
||||
if (index === visibilityEventsOnOriginalEvent.length) {
|
||||
const originalEvent = this.findEventById(originalEventId);
|
||||
if (!originalEvent) {
|
||||
return;
|
||||
}
|
||||
if (index === 0) {
|
||||
// We have just removed the only visibility change event.
|
||||
this.visibilityEvents.delete(originalEventId);
|
||||
originalEvent.applyVisibilityEvent();
|
||||
} else {
|
||||
const newEvent = visibilityEventsOnOriginalEvent[visibilityEventsOnOriginalEvent.length - 1];
|
||||
const newVisibility = newEvent.asVisibilityChange();
|
||||
if (!newVisibility) {
|
||||
// Event is ill-formed.
|
||||
// This breaks our invariant.
|
||||
throw new Error("at this stage, visibility changes should be well-formed");
|
||||
}
|
||||
originalEvent.applyVisibilityEvent(newVisibility);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When we receive an event whose visibility has been altered by
|
||||
* a (more recent) visibility change event, patch the event in
|
||||
* place so that clients now not to display it.
|
||||
*
|
||||
* @param event Any matrix event. If this event has at least one a
|
||||
* pending visibility change event, apply the latest visibility
|
||||
* change event.
|
||||
*/
|
||||
private applyPendingVisibilityEvents(event: MatrixEvent): void {
|
||||
const visibilityEvents = this.visibilityEvents.get(event.getId());
|
||||
if (!visibilityEvents || visibilityEvents.length == 0) {
|
||||
// No pending visibility change in store.
|
||||
return;
|
||||
}
|
||||
const visibilityEvent = visibilityEvents[visibilityEvents.length - 1];
|
||||
const visibilityChange = visibilityEvent.asVisibilityChange();
|
||||
if (!visibilityChange) {
|
||||
return;
|
||||
}
|
||||
if (visibilityChange.visible) {
|
||||
// Events are visible by default, no need to apply a visibility change.
|
||||
// Note that we need to keep the visibility changes in `visibilityEvents`,
|
||||
// in case we later fetch an older visibility change event that is superseded
|
||||
// by `visibilityChange`.
|
||||
}
|
||||
if (visibilityEvent.getTs() < event.getTs()) {
|
||||
// Something is wrong, the visibility change cannot happen before the
|
||||
// event. Presumably an ill-formed event.
|
||||
return;
|
||||
}
|
||||
event.applyVisibilityEvent(visibilityChange);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2302,12 +2545,12 @@ function pendingEventsKey(roomId: string): string {
|
||||
return `mx_pending_events_${roomId}`;
|
||||
}
|
||||
|
||||
/* a map from current event status to a list of allowed next statuses
|
||||
*/
|
||||
// a map from current event status to a list of allowed next statuses
|
||||
const ALLOWED_TRANSITIONS: Record<EventStatus, EventStatus[]> = {
|
||||
[EventStatus.ENCRYPTING]: [
|
||||
EventStatus.SENDING,
|
||||
EventStatus.NOT_SENT,
|
||||
EventStatus.CANCELLED,
|
||||
],
|
||||
[EventStatus.SENDING]: [
|
||||
EventStatus.ENCRYPTING,
|
||||
@@ -2460,3 +2703,4 @@ function memberNamesToRoomName(names: string[], count = (names.length + 1)) {
|
||||
* @param {string} membership The new membership value
|
||||
* @param {string} prevMembership The previous membership value
|
||||
*/
|
||||
|
||||
|
||||
+198
-76
@@ -16,11 +16,14 @@ limitations under the License.
|
||||
|
||||
import { MatrixClient } from "../matrix";
|
||||
import { ReEmitter } from "../ReEmitter";
|
||||
import { MatrixEvent } from "./event";
|
||||
import { EventTimeline } from "./event-timeline";
|
||||
import { RelationType } from "../@types/event";
|
||||
import { IRelationsRequestOpts } from "../@types/requests";
|
||||
import { MatrixEvent, IThreadBundledRelationship } from "./event";
|
||||
import { Direction, EventTimeline } from "./event-timeline";
|
||||
import { EventTimelineSet } from './event-timeline-set';
|
||||
import { Room } from './room';
|
||||
import { TypedEventEmitter } from "./typed-event-emitter";
|
||||
import { RoomState } from "./room-state";
|
||||
|
||||
export enum ThreadEvent {
|
||||
New = "Thread.new",
|
||||
@@ -30,14 +33,16 @@ export enum ThreadEvent {
|
||||
ViewThread = "Thred.viewThread",
|
||||
}
|
||||
|
||||
interface IThreadOpts {
|
||||
initialEvents?: MatrixEvent[];
|
||||
room: Room;
|
||||
client: MatrixClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
export class Thread extends TypedEventEmitter<ThreadEvent> {
|
||||
/**
|
||||
* A reference to the event ID at the top of the thread
|
||||
*/
|
||||
private root: string;
|
||||
/**
|
||||
* A reference to all the events ID at the bottom of the threads
|
||||
*/
|
||||
@@ -47,33 +52,55 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
|
||||
|
||||
private reEmitter: ReEmitter;
|
||||
|
||||
private lastEvent: MatrixEvent;
|
||||
private replyCount = 0;
|
||||
|
||||
public readonly room: Room;
|
||||
public readonly client: MatrixClient;
|
||||
|
||||
public initialEventsFetched = false;
|
||||
|
||||
public readonly id: string;
|
||||
|
||||
constructor(
|
||||
events: MatrixEvent[] = [],
|
||||
public readonly room: Room,
|
||||
public readonly client: MatrixClient,
|
||||
public readonly rootEvent: MatrixEvent | undefined,
|
||||
opts: IThreadOpts,
|
||||
) {
|
||||
super();
|
||||
if (events.length === 0) {
|
||||
throw new Error("Can't create an empty thread");
|
||||
}
|
||||
|
||||
this.reEmitter = new ReEmitter(this);
|
||||
|
||||
this.room = opts.room;
|
||||
this.client = opts.client;
|
||||
this.timelineSet = new EventTimelineSet(this.room, {
|
||||
unstableClientRelationAggregation: true,
|
||||
timelineSupport: true,
|
||||
pendingEvents: true,
|
||||
});
|
||||
this.reEmitter = new ReEmitter(this);
|
||||
|
||||
this.reEmitter.reEmit(this.timelineSet, [
|
||||
"Room.timeline",
|
||||
"Room.timelineReset",
|
||||
]);
|
||||
|
||||
events.forEach(event => this.addEvent(event));
|
||||
// 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
|
||||
if (!rootEvent) {
|
||||
this.id = opts?.initialEvents
|
||||
?.find(event => event.isThreadRelation)?.relationEventId;
|
||||
} else {
|
||||
this.id = rootEvent.getId();
|
||||
}
|
||||
this.initialiseThread(this.rootEvent);
|
||||
|
||||
room.on("Room.localEchoUpdated", this.onEcho);
|
||||
room.on("Room.timeline", this.onEcho);
|
||||
opts?.initialEvents?.forEach(event => this.addEvent(event));
|
||||
|
||||
this.room.on("Room.localEchoUpdated", this.onEcho);
|
||||
this.room.on("Room.timeline", this.onEcho);
|
||||
}
|
||||
|
||||
public get hasServerSideSupport(): boolean {
|
||||
return this.client.cachedCapabilities
|
||||
?.capabilities?.[RelationType.Thread]?.enabled;
|
||||
}
|
||||
|
||||
onEcho = (event: MatrixEvent) => {
|
||||
@@ -82,6 +109,10 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
|
||||
}
|
||||
};
|
||||
|
||||
public get roomState(): RoomState {
|
||||
return this.room.getLiveTimeline().getState(EventTimeline.FORWARDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an event to the thread and updates
|
||||
* the tail/root references if needed
|
||||
@@ -89,43 +120,104 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
|
||||
* @param event The event to add
|
||||
*/
|
||||
public async addEvent(event: MatrixEvent, toStartOfTimeline = false): Promise<void> {
|
||||
if (this.timelineSet.findEventById(event.getId()) || event.status !== null) {
|
||||
return;
|
||||
// Add all incoming events to the thread's timeline set when there's
|
||||
// no server support
|
||||
if (!this.hasServerSideSupport) {
|
||||
if (this.timelineSet.findEventById(event.getId())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// all the relevant membership info to hydrate events with a sender
|
||||
// is held in the main room timeline
|
||||
// We want to fetch the room state from there and pass it down to this thread
|
||||
// timeline set to let it reconcile an event with its relevant RoomMember
|
||||
|
||||
event.setThread(this);
|
||||
this.timelineSet.addEventToTimeline(
|
||||
event,
|
||||
this.liveTimeline,
|
||||
toStartOfTimeline,
|
||||
false,
|
||||
this.roomState,
|
||||
);
|
||||
|
||||
await this.client.decryptEventIfNeeded(event, {});
|
||||
}
|
||||
|
||||
if (!this.root) {
|
||||
if (event.isThreadRelation) {
|
||||
this.root = event.threadRootId;
|
||||
} else {
|
||||
this.root = event.getId();
|
||||
if (this.hasServerSideSupport && this.initialEventsFetched) {
|
||||
if (event.localTimestamp > this.lastReply().localTimestamp && !this.findEventById(event.getId())) {
|
||||
this.timelineSet.addEventToTimeline(
|
||||
event,
|
||||
this.liveTimeline,
|
||||
false,
|
||||
false,
|
||||
this.roomState,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// all the relevant membership info to hydrate events with a sender
|
||||
// is held in the main room timeline
|
||||
// We want to fetch the room state from there and pass it down to this thread
|
||||
// timeline set to let it reconcile an event with its relevant RoomMember
|
||||
const roomState = this.room.getLiveTimeline().getState(EventTimeline.FORWARDS);
|
||||
|
||||
event.setThread(this);
|
||||
this.timelineSet.addEventToTimeline(
|
||||
event,
|
||||
this.timelineSet.getLiveTimeline(),
|
||||
toStartOfTimeline,
|
||||
false,
|
||||
roomState,
|
||||
);
|
||||
|
||||
if (!this._currentUserParticipated && event.getSender() === this.client.getUserId()) {
|
||||
this._currentUserParticipated = true;
|
||||
}
|
||||
|
||||
await this.client.decryptEventIfNeeded(event, {});
|
||||
this.emit(ThreadEvent.Update, this);
|
||||
|
||||
if (event.isThreadRelation) {
|
||||
this.emit(ThreadEvent.NewReply, this, event);
|
||||
const isThreadReply = event.getRelation()?.rel_type === RelationType.Thread;
|
||||
// If no thread support exists we want to count all thread relation
|
||||
// added as a reply. We can't rely on the bundled relationships count
|
||||
if (!this.hasServerSideSupport && isThreadReply) {
|
||||
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.localTimestamp > this.replyToEvent.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 (this.hasServerSideSupport) {
|
||||
this.replyCount++;
|
||||
}
|
||||
|
||||
this.emit(ThreadEvent.NewReply, this, event);
|
||||
}
|
||||
}
|
||||
|
||||
this.emit(ThreadEvent.Update, this);
|
||||
}
|
||||
|
||||
private initialiseThread(rootEvent: MatrixEvent | undefined): void {
|
||||
const bundledRelationship = rootEvent
|
||||
?.getServerAggregatedRelation<IThreadBundledRelationship>(RelationType.Thread);
|
||||
|
||||
if (this.hasServerSideSupport && bundledRelationship) {
|
||||
this.replyCount = bundledRelationship.count;
|
||||
this._currentUserParticipated = bundledRelationship.current_user_participated;
|
||||
|
||||
const event = new MatrixEvent(bundledRelationship.latest_event);
|
||||
this.setEventMetadata(event);
|
||||
this.lastEvent = event;
|
||||
}
|
||||
|
||||
if (!bundledRelationship && rootEvent) {
|
||||
this.addEvent(rootEvent);
|
||||
}
|
||||
}
|
||||
|
||||
public async fetchInitialEvents(): Promise<boolean> {
|
||||
try {
|
||||
await this.fetchEvents();
|
||||
this.initialEventsFetched = true;
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private setEventMetadata(event: MatrixEvent): void {
|
||||
EventTimeline.setEventMetadata(event, this.roomState, false);
|
||||
event.setThread(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,28 +230,17 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
|
||||
/**
|
||||
* Return last reply to the thread
|
||||
*/
|
||||
public get lastReply(): MatrixEvent {
|
||||
const threadReplies = this.events
|
||||
.filter(event => event.isThreadRelation);
|
||||
return threadReplies[threadReplies.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* The thread ID, which is the same as the root event ID
|
||||
*/
|
||||
public get id(): string {
|
||||
return this.root;
|
||||
}
|
||||
|
||||
/**
|
||||
* The thread root event
|
||||
*/
|
||||
public get rootEvent(): MatrixEvent {
|
||||
return this.findEventById(this.root);
|
||||
public lastReply(matches: (ev: MatrixEvent) => boolean = () => true): MatrixEvent {
|
||||
for (let i = this.events.length - 1; i >= 0; i--) {
|
||||
const event = this.events[i];
|
||||
if (matches(event)) {
|
||||
return event;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public get roomId(): string {
|
||||
return this.rootEvent.getRoomId();
|
||||
return this.room.roomId;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,28 +249,18 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
|
||||
* exclude annotations from that number
|
||||
*/
|
||||
public get length(): number {
|
||||
return this.events
|
||||
.filter(event => event.isThreadRelation)
|
||||
.length;
|
||||
return this.replyCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* A getter for the last event added to the thread
|
||||
*/
|
||||
public get replyToEvent(): MatrixEvent {
|
||||
const events = this.events;
|
||||
return events[events.length -1];
|
||||
return this.lastEvent;
|
||||
}
|
||||
|
||||
public get events(): MatrixEvent[] {
|
||||
return this.timelineSet.getLiveTimeline().getEvents();
|
||||
}
|
||||
|
||||
public merge(thread: Thread): void {
|
||||
thread.events.forEach(event => {
|
||||
this.addEvent(event);
|
||||
});
|
||||
this.events.forEach(event => event.setThread(this));
|
||||
return this.liveTimeline.getEvents();
|
||||
}
|
||||
|
||||
public has(eventId: string): boolean {
|
||||
@@ -199,4 +270,55 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
|
||||
public get hasCurrentUserParticipated(): boolean {
|
||||
return this._currentUserParticipated;
|
||||
}
|
||||
|
||||
public get liveTimeline(): EventTimeline {
|
||||
return this.timelineSet.getLiveTimeline();
|
||||
}
|
||||
|
||||
public async fetchEvents(opts: IRelationsRequestOpts = { limit: 20 }): Promise<{
|
||||
originalEvent: MatrixEvent;
|
||||
events: MatrixEvent[];
|
||||
nextBatch?: string;
|
||||
prevBatch?: string;
|
||||
}> {
|
||||
let {
|
||||
originalEvent,
|
||||
events,
|
||||
prevBatch,
|
||||
nextBatch,
|
||||
} = await this.client.relations(
|
||||
this.room.roomId,
|
||||
this.id,
|
||||
RelationType.Thread,
|
||||
null,
|
||||
opts,
|
||||
);
|
||||
|
||||
// When there's no nextBatch returned with a `from` request we have reached
|
||||
// the end of the thread, and therefore want to return an empty one
|
||||
if (!opts.to && !nextBatch) {
|
||||
events = [originalEvent, ...events];
|
||||
}
|
||||
|
||||
for (const event of events) {
|
||||
await this.client.decryptEventIfNeeded(event);
|
||||
this.setEventMetadata(event);
|
||||
}
|
||||
|
||||
const prependEvents = !opts.direction || opts.direction === Direction.Backward;
|
||||
|
||||
this.timelineSet.addEventsToTimeline(
|
||||
events,
|
||||
prependEvents,
|
||||
this.liveTimeline,
|
||||
prependEvents ? nextBatch : prevBatch,
|
||||
);
|
||||
|
||||
return {
|
||||
originalEvent,
|
||||
events,
|
||||
prevBatch,
|
||||
nextBatch,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ interface IOpts {
|
||||
export interface IMinimalEvent {
|
||||
content: IContent;
|
||||
type: EventType | string;
|
||||
unsigned?: IUnsigned;
|
||||
}
|
||||
|
||||
export interface IEphemeral {
|
||||
|
||||
+7
-6
@@ -201,6 +201,7 @@ export class SyncApi {
|
||||
"Room.accountData",
|
||||
"Room.myMembership",
|
||||
"Room.replaceEvent",
|
||||
"Room.visibilityChange",
|
||||
]);
|
||||
this.registerStateListeners(room);
|
||||
return room;
|
||||
@@ -283,7 +284,7 @@ export class SyncApi {
|
||||
leaveRooms = this.mapSyncResponseToRoomArray(data.rooms.leave);
|
||||
}
|
||||
const rooms = [];
|
||||
leaveRooms.forEach((leaveObj) => {
|
||||
leaveRooms.forEach(async (leaveObj) => {
|
||||
const room = leaveObj.room;
|
||||
rooms.push(room);
|
||||
if (!leaveObj.isBrandNewRoom) {
|
||||
@@ -309,7 +310,7 @@ export class SyncApi {
|
||||
EventTimeline.BACKWARDS);
|
||||
|
||||
this.processRoomEvents(room, stateEvents, timelineEvents);
|
||||
this.processThreadEvents(room, threadedEvents);
|
||||
await this.processThreadEvents(room, threadedEvents);
|
||||
|
||||
room.recalculate();
|
||||
client.store.storeRoom(room);
|
||||
@@ -1306,7 +1307,7 @@ export class SyncApi {
|
||||
const [timelineEvents, threadedEvents] = this.client.partitionThreadedEvents(events);
|
||||
|
||||
this.processRoomEvents(room, stateEvents, timelineEvents, syncEventData.fromCache);
|
||||
this.processThreadEvents(room, threadedEvents);
|
||||
await this.processThreadEvents(room, threadedEvents);
|
||||
|
||||
// set summary after processing events,
|
||||
// because it will trigger a name calculation
|
||||
@@ -1365,7 +1366,7 @@ export class SyncApi {
|
||||
});
|
||||
|
||||
// Handle leaves (e.g. kicked rooms)
|
||||
leaveRooms.forEach((leaveObj) => {
|
||||
leaveRooms.forEach(async (leaveObj) => {
|
||||
const room = leaveObj.room;
|
||||
const stateEvents = this.mapSyncEventsFormat(leaveObj.state, room);
|
||||
const events = this.mapSyncEventsFormat(leaveObj.timeline, room);
|
||||
@@ -1374,7 +1375,7 @@ export class SyncApi {
|
||||
const [timelineEvents, threadedEvents] = this.client.partitionThreadedEvents(events);
|
||||
|
||||
this.processRoomEvents(room, stateEvents, timelineEvents);
|
||||
this.processThreadEvents(room, threadedEvents);
|
||||
await this.processThreadEvents(room, threadedEvents);
|
||||
room.addAccountData(accountDataEvents);
|
||||
|
||||
room.recalculate();
|
||||
@@ -1719,7 +1720,7 @@ export class SyncApi {
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
private processThreadEvents(room: Room, threadedEvents: MatrixEvent[]): void {
|
||||
private processThreadEvents(room: Room, threadedEvents: MatrixEvent[]): Promise<void> {
|
||||
return this.client.processThreadEvents(room, threadedEvents);
|
||||
}
|
||||
|
||||
|
||||
+9
-3
@@ -937,7 +937,7 @@ export class MatrixCall extends EventEmitter {
|
||||
const upgradeVideo = video && !this.hasLocalUserMediaVideoTrack;
|
||||
logger.debug(`Upgrading call: audio?=${upgradeAudio} video?=${upgradeVideo}`);
|
||||
|
||||
const stream = await this.client.getMediaHandler().getUserMediaStream(upgradeAudio, upgradeVideo);
|
||||
const stream = await this.client.getMediaHandler().getUserMediaStream(upgradeAudio, upgradeVideo, false);
|
||||
if (upgradeAudio && upgradeVideo) {
|
||||
if (this.hasLocalUserMediaAudioTrack) return;
|
||||
if (this.hasLocalUserMediaVideoTrack) return;
|
||||
@@ -1717,7 +1717,12 @@ export class MatrixCall extends EventEmitter {
|
||||
|
||||
const stream = ev.streams[0];
|
||||
this.pushRemoteFeed(stream);
|
||||
stream.addEventListener("removetrack", () => this.deleteFeedByStream(stream));
|
||||
stream.addEventListener("removetrack", () => {
|
||||
if (stream.getTracks().length === 0) {
|
||||
logger.info(`Stream ID ${stream.id} has no tracks remaining - removing`);
|
||||
this.deleteFeedByStream(stream);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
private onDataChannel = (ev: RTCDataChannelEvent): void => {
|
||||
@@ -1973,7 +1978,7 @@ export class MatrixCall extends EventEmitter {
|
||||
}
|
||||
|
||||
private stopAllMedia(): void {
|
||||
logger.debug(`stopAllMedia (stream=${this.localUsermediaStream})`);
|
||||
logger.debug("Stopping all media for call", this.callId);
|
||||
|
||||
for (const feed of this.feeds) {
|
||||
if (feed.isLocal() && feed.purpose === SDPStreamMetadataPurpose.Usermedia) {
|
||||
@@ -1981,6 +1986,7 @@ export class MatrixCall extends EventEmitter {
|
||||
} else if (feed.isLocal() && feed.purpose === SDPStreamMetadataPurpose.Screenshare) {
|
||||
this.client.getMediaHandler().stopScreensharingStream(feed.stream);
|
||||
} else {
|
||||
logger.debug("Stopping remote stream", feed.stream.id);
|
||||
for (const track of feed.stream.getTracks()) {
|
||||
track.stop();
|
||||
}
|
||||
|
||||
@@ -54,9 +54,12 @@ export class MediaHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param audio should have an audio track
|
||||
* @param video should have a video track
|
||||
* @param reusable is allowed to be reused by the MediaHandler
|
||||
* @returns {MediaStream} based on passed parameters
|
||||
*/
|
||||
public async getUserMediaStream(audio: boolean, video: boolean): Promise<MediaStream> {
|
||||
public async getUserMediaStream(audio: boolean, video: boolean, reusable = true): Promise<MediaStream> {
|
||||
const shouldRequestAudio = audio && await this.hasAudioDevice();
|
||||
const shouldRequestVideo = video && await this.hasVideoDevice();
|
||||
|
||||
@@ -78,7 +81,9 @@ export class MediaHandler {
|
||||
stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
}
|
||||
|
||||
this.userMediaStreams.push(stream);
|
||||
if (reusable) {
|
||||
this.userMediaStreams.push(stream);
|
||||
}
|
||||
|
||||
return stream;
|
||||
}
|
||||
@@ -101,9 +106,11 @@ export class MediaHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param desktopCapturerSourceId sourceId for Electron DesktopCapturer
|
||||
* @param reusable is allowed to be reused by the MediaHandler
|
||||
* @returns {MediaStream} based on passed parameters
|
||||
*/
|
||||
public async getScreensharingStream(desktopCapturerSourceId: string): Promise<MediaStream | null> {
|
||||
public async getScreensharingStream(desktopCapturerSourceId: string, reusable = true): Promise<MediaStream | null> {
|
||||
let stream: MediaStream;
|
||||
|
||||
if (this.screensharingStreams.length === 0) {
|
||||
@@ -125,7 +132,9 @@ export class MediaHandler {
|
||||
stream = matchingStream.clone();
|
||||
}
|
||||
|
||||
this.screensharingStreams.push(stream);
|
||||
if (reusable) {
|
||||
this.screensharingStreams.push(stream);
|
||||
}
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user