Compare commits

..

53 Commits

Author SHA1 Message Date
RiotRobot bf8ee39a3f v15.5.1 2022-02-14 15:14:04 +00:00
RiotRobot cc1d1d76d0 Prepare changelog for v15.5.1 2022-02-14 15:14:04 +00:00
David Baker 30fdd96168 Revert event-mapper optimisations (#2171) (#2172)
Co-authored-by: Germain <germains@element.io>
2022-02-14 13:53:12 +00:00
RiotRobot d473a2e095 v15.5.1-rc.1 2022-02-08 15:32:28 +00:00
RiotRobot d398ffd6df Prepare changelog for v15.5.1-rc.1 2022-02-08 15:32:27 +00:00
Michael Telatynski ffab55452a Fix issue with rooms not getting marked as unread (#2163) 2022-02-08 12:32:14 +00:00
Germain 9120b1dfd9 Expose room state helper (#2162) 2022-02-08 12:08:43 +00:00
Šimon Brandner 5d4e3183aa Don't store streams that are only used once (#2157)
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-02-08 10:13:58 +00:00
Michael Telatynski 07171a95c4 Fix edge cases around RR calculations (#2160) 2022-02-07 17:49:14 +00:00
Šimon Brandner 12afcd3850 Account for encryption in maySendMessage() (#2159) 2022-02-07 15:32:21 +00:00
Andy Balaam 2d23330b74 Send references to thread root to threads, even out of order (#2156)
Co-authored-by: Germain <germains@element.io>
2022-02-04 15:50:06 +00:00
Germain 7faff66006 Out of sync thread summary in main timeline (#2153) 2022-02-04 11:57:42 +00:00
Michael Telatynski b07457726b Avoid re-doing as much work when processing m.receipt event (#2151) 2022-02-03 11:06:19 +00:00
Germain 6bf8142ff6 Fix initial sync fail when event fetching unsuccessful (#2150) 2022-02-02 17:56:43 +00:00
Christian Paul cf0ccaf93d TypeScript: opts parameter of client.startClient is optional (#2106) 2022-02-02 15:29:58 +00:00
Christian Paul 34a3955b60 MatrixEvent.getRoomId can return undefined (#2036) 2022-02-02 16:22:57 +01:00
Germain 82122872bf Add thread relation in sendEvent if it's missing (#2149) 2022-02-02 14:44:38 +00:00
Germain 9c242a9ce6 Keep a reference to the latest thread created (#2148) 2022-02-01 14:50:42 +00:00
Germain 51f3fac87b Add thread relation to sticker message (#2147) 2022-02-01 10:36:04 +00:00
Germain 66b98844a2 Refactor thread model to be created from the root event (#2142) 2022-02-01 08:58:39 +00:00
David Baker d03db00e4c Yarn upgrade (#2146)
Apologies if this means everyone has to `rm -rf node_modules`: react-ace
decided to turn some paths that were regular files into directories
and it turns out this really confuses yarn: https://github.com/securingsincity/react-ace/issues/1048
2022-01-31 16:45:42 +00:00
RiotRobot cf8d2bf6ef Resetting package fields for development 2022-01-31 14:44:13 +00:00
RiotRobot 913a0b51a6 Merge branch 'master' into develop 2022-01-31 14:44:13 +00:00
RiotRobot ede0f696ee v15.5.0 2022-01-31 14:41:10 +00:00
RiotRobot d05214a169 Prepare changelog for v15.5.0 2022-01-31 14:41:09 +00:00
David Baker 1e93d0b19f Bump node-fetch from 2.6.6 to 2.6.7 (#2140) (#2145)
Bumps [node-fetch](https://github.com/node-fetch/node-fetch) from 2.6.6 to 2.6.7.
- [Release notes](https://github.com/node-fetch/node-fetch/releases)
- [Commits](https://github.com/node-fetch/node-fetch/compare/v2.6.6...v2.6.7)

---
updated-dependencies:
- dependency-name: node-fetch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-31 14:24:05 +00:00
Šimon Brandner 5e125e646c Don't decrypt redacted messages (#2143) 2022-01-31 11:21:12 +00:00
dependabot[bot] 1643201363 Bump node-fetch from 2.6.6 to 2.6.7 (#2140)
Bumps [node-fetch](https://github.com/node-fetch/node-fetch) from 2.6.6 to 2.6.7.
- [Release notes](https://github.com/node-fetch/node-fetch/releases)
- [Commits](https://github.com/node-fetch/node-fetch/compare/v2.6.6...v2.6.7)

---
updated-dependencies:
- dependency-name: node-fetch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-27 21:59:03 +00:00
dependabot[bot] 8e99e084a8 Bump cached-path-relative from 1.0.2 to 1.1.0 (#2138)
Bumps [cached-path-relative](https://github.com/ashaffer/cached-path-relative) from 1.0.2 to 1.1.0.
- [Release notes](https://github.com/ashaffer/cached-path-relative/releases)
- [Commits](https://github.com/ashaffer/cached-path-relative/commits)

---
updated-dependencies:
- dependency-name: cached-path-relative
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-27 14:52:23 -07:00
Germain 67d362fdd9 Implement local relation types filter (#2139) 2022-01-27 17:47:05 +00:00
RiotRobot e66430ada6 v15.5.0-rc.1 2022-01-26 16:56:34 +00:00
RiotRobot 52ca60142a Prepare changelog for v15.5.0-rc.1 2022-01-26 16:56:33 +00:00
Michael Telatynski 7d2a5afa6d Fix http-api butchering idServer requests (#2134) 2022-01-26 15:43:24 +00:00
Germain 02ba233644 Improved threads reliability with/without server side support (#2132) 2022-01-26 13:54:00 +00:00
Michael Telatynski 50493a3330 Improve typing for Room.timeline data parameter (#2131) 2022-01-26 13:24:10 +00:00
Germain 8434f29c54 Make threads reply chain filter out local event ID (#2129) 2022-01-25 10:51:12 +00:00
Michael Telatynski a34426a7f6 Improve signature of MatrixClient::isUsernameAvailable to not rely on throwing (#2130) 2022-01-25 10:45:39 +00:00
Germain 033693283d Fix local echo for reactions in threads (#2128) 2022-01-24 17:53:05 +00:00
Germain fab7b7f26a Add default IContent value in makeContentExtensible (#2126) 2022-01-24 09:28:58 +00:00
David Baker 230e3b4ace Add eslint camelcase ignores & remove unnecessary casts (#2110)
Presumbaly casts left over from typescript migration.
2022-01-19 18:19:08 +00:00
Andy Balaam a50a627300 Support m.asset in m.location event content (#2109) 2022-01-19 09:08:41 +00:00
Germain 80930f6690 Extend IEventRelation to define m.in_reply_to (#2108)
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-01-19 08:57:32 +00:00
Michael Telatynski 652b3a9208 Revert "Remove getCapabilities call for guest users (#2100)" (#2107) 2022-01-18 09:55:54 +00:00
Travis Ralston 0a8b825702 Move polls to events-sdk, and invent a RelatedRelations type (#2102)
* Move polls handling to events-sdk & invent a multi-relation handler

Polls are now fully handled by the events-sdk instead of by the js-sdk.

The multi-relation handler (RelatedRelations) is primarily meant to handle unstable & stable support in polls when it becomes stable.

* update events-sdk for polls

* Update events-sdk for polls bugfix

* Update events-sdk for maintenance
2022-01-17 10:06:42 -07:00
RiotRobot f4e2a38f4b Resetting package fields for development 2022-01-17 14:10:20 +00:00
RiotRobot b3474531c9 Merge branch 'master' into develop 2022-01-17 14:10:20 +00:00
David Baker 16ca09eed8 Don't remove streams that still have tracks (#2104)
If a renogotiation ends up with one track being removed, we removed
the whole stream, which would cause us to lose, for example, audio
rather than just video.
2022-01-14 09:43:50 +00:00
Travis Ralston 129fb7f10f Keep unknown fields when adding extensible formats (#2105)
* Keep unknown fields when adding extensible formats

This also properly handles `m.new_content` to also be extensible.

* appease the linter
2022-01-13 20:17:46 +00:00
Travis Ralston 016e24472a Send extensible events structure and support on-demand parsing (#2091)
* Parse extensible events on demand

* Decorate messages with MSC1767 when appropriate

We do this automatically to force a pool of messages in the wild we can use for testing.

* Include the SDK

* Appease linter and tests

* Change property name to appease linter

* Update SDK
2022-01-13 09:56:11 -07:00
David Teller 4acb98c496 [Fix] Nullcheck redactedEvent (#2103)
We should only check whether a redacted event is a visibility event *if the redacted event is not null*.
2022-01-12 13:27:03 +01:00
David Teller 96d1b30012 MSC3531: Hiding messages during moderation (#2041) 2022-01-12 11:27:33 +01:00
David Baker 6fc586598a Yarn upgrade (#2101) 2022-01-11 17:17:01 +00:00
Michael Telatynski 2d9c938765 Support cancelling events whilst they are in status = ENCRYPTING (#2095) 2022-01-11 15:03:33 +00:00
32 changed files with 2817 additions and 1499 deletions
+48
View File
@@ -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
View File
@@ -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",
+1 -1
View File
@@ -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",
});
+488
View File
@@ -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) {
+2
View File
@@ -365,3 +365,5 @@ export function setHttpResponses(
.respond(200, response.data);
});
}
export const emitPromise = (e, k) => new Promise(r => e.once(k, r));
+56
View File
@@ -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);
});
});
+82 -1
View File
@@ -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);
});
});
});
+1 -1
View File
@@ -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",
});
});
+1 -1
View File
@@ -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");
+10
View File
@@ -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;
+10
View File
@@ -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;
}
-68
View File
@@ -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;
};
}
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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
};
}
-3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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) {
+6 -1
View File
@@ -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
View File
@@ -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
+38
View File
@@ -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));
}
}
+2 -2
View File
@@ -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) => {
+8 -8
View File
@@ -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
View File
@@ -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
View File
@@ -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,
};
}
}
+1
View File
@@ -32,6 +32,7 @@ interface IOpts {
export interface IMinimalEvent {
content: IContent;
type: EventType | string;
unsigned?: IUnsigned;
}
export interface IEphemeral {
+7 -6
View File
@@ -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
View File
@@ -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();
}
+13 -4
View File
@@ -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;
}
+1037 -1119
View File
File diff suppressed because it is too large Load Diff