Compare commits

..

103 Commits

Author SHA1 Message Date
RiotRobot 65de184d88 v11.1.0-rc.1 2021-05-19 14:29:51 +01:00
RiotRobot 52764045ce Prepare changelog for v11.1.0-rc.1 2021-05-19 14:29:50 +01:00
J. Ryan Stinnett 4b0db6c472 Guard against duplicates in Relations model
The `Relations` model was relying on object reference equality to prevent
duplicates, which breaks down if we ever have two objects that represent the
same event.

This fixes things to additionally track event IDs we've seen before and discard
any attempts to add them twice.

Fixes https://github.com/vector-im/element-web/issues/11161
2021-05-19 13:20:40 +01:00
Germain 40cd4629db Decrypt relations before applying them to target event (#1696) 2021-05-18 17:21:06 +01:00
RiotRobot 52a893a811 Resetting package fields for development 2021-05-17 13:32:49 +01:00
RiotRobot 7cc94e1e1e Merge branch 'master' into develop 2021-05-17 13:32:48 +01:00
RiotRobot 88945a6d6d v11.0.0 2021-05-17 13:30:08 +01:00
RiotRobot 7203c5aaf0 Prepare changelog for v11.0.0 2021-05-17 13:30:07 +01:00
J. Ryan Stinnett c305058c46 Merge pull request #1695 from matrix-org/jryans/glare-streams-release
[Release] Fix regressed glare
2021-05-17 11:55:25 +01:00
Šimon Brandner 91bbf7d1a8 Fix glare
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-17 11:20:14 +01:00
J. Ryan Stinnett 2d5857f145 Merge pull request #1690 from SimonBrandner/fix/glare/17250
Fix regressed glare
2021-05-17 11:11:39 +01:00
Travis Ralston 91af9a411d Null-guard power level object usage (#1694) 2021-05-14 08:36:44 +01:00
Germain 9a9ed124f5 Use native Object and Array methods (#1693) 2021-05-13 17:20:09 +01:00
Michael Telatynski 4e5442d972 Merge pull request #1692 from matrix-org/t3chguy/fix/i80
Add m.reaction to EventType enum
2021-05-13 12:44:42 +01:00
Michael Telatynski b5fa54f91e Add m.reaction to EventType enum 2021-05-13 10:57:02 +01:00
Germain 2246aede4b Merge pull request #1684 from matrix-org/gsouquet/cache-decrypt 2021-05-12 12:19:59 +01:00
Šimon Brandner 4b2d409c69 Fix glare
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-12 07:47:48 +02:00
RiotRobot 684511ff06 v11.0.0-rc.1 2021-05-11 15:03:04 +01:00
RiotRobot 88b328df7e Prepare changelog for v11.0.0-rc.1 2021-05-11 15:03:03 +01:00
Michael Telatynski e3583dd04e Merge pull request #1679 from matrix-org/t3chguy/spaces-stable
Switch from MSC1772 unstable prefixes to stable
2021-05-11 13:39:47 +01:00
Germain Souquet e484a2ebb5 add missing coma to appease linter 2021-05-11 13:02:42 +01:00
Germain Souquet 72f86258d0 Add explainer for awaitDecryption 2021-05-11 10:05:24 +01:00
Germain Souquet 874cb3b779 make attemptDecryption backwards compatible 2021-05-11 10:02:32 +01:00
Germain f21e0228b4 Update documentation wording
Co-authored-by: Travis Ralston <travpc@gmail.com>
2021-05-11 09:20:14 +01:00
Germain Souquet 01cd82bc6a undo test timeout 2021-05-10 18:12:47 +01:00
Germain Souquet d2a6a8b283 Merge branch 'develop' into gsouquet/cache-decrypt 2021-05-10 17:35:05 +01:00
Germain Souquet f242e460ed fix tests 2021-05-10 17:28:00 +01:00
Germain Souquet 95e08253a6 Appease linter 2021-05-10 16:59:54 +01:00
Germain Souquet 202a4fa6f1 Better document new room methods 2021-05-10 15:55:16 +01:00
Germain Souquet 576f46cb88 Add flag to prevent emitting event.decrypted 2021-05-10 15:25:07 +01:00
J. Ryan Stinnett 2d73805ca3 Merge pull request #1680 from SimonBrandner/feed-example
Update the VoIP example to work with the new changes
2021-05-10 14:50:18 +01:00
J. Ryan Stinnett eedfa550a6 Merge pull request #1687 from matrix-org/dependabot/npm_and_yarn/hosted-git-info-2.8.9
Bump hosted-git-info from 2.8.8 to 2.8.9
2021-05-10 14:48:34 +01:00
dependabot[bot] 4ca718adc4 Bump hosted-git-info from 2.8.8 to 2.8.9
Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.8 to 2.8.9.
- [Release notes](https://github.com/npm/hosted-git-info/releases)
- [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md)
- [Commits](https://github.com/npm/hosted-git-info/compare/v2.8.8...v2.8.9)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-10 13:46:58 +00:00
RiotRobot 7c4ced8f46 Resetting package fields for development 2021-05-10 14:46:14 +01:00
RiotRobot 7018f4ab25 Merge branch 'master' into develop 2021-05-10 14:46:13 +01:00
RiotRobot fda13875ef v10.1.0 2021-05-10 14:43:18 +01:00
RiotRobot 8005917452 Prepare changelog for v10.1.0 2021-05-10 14:43:17 +01:00
Germain Souquet 91eee8587e extract shouldAttemptDecryption to event model 2021-05-07 15:16:04 +01:00
Germain Souquet e9132abc25 Do not attempt to decrypt already clear events 2021-05-07 12:58:53 +01:00
Germain Souquet 444eac5c6e consolidate critical event decryption implementation 2021-05-07 11:23:59 +01:00
Šimon Brandner a449c5f8c2 Merge branch 'develop' into feed-example 2021-05-06 16:44:29 +02:00
Germain Souquet 19d6dbaa52 Use read receipt instead of read marker 2021-05-06 14:07:38 +01:00
Germain Souquet 8820619e82 Pass decrypt flag to event mapper 2021-05-06 12:34:21 +01:00
Germain Souquet fb33bc7e07 Lazily decrypt event on room view 2021-05-06 11:46:14 +01:00
J. Ryan Stinnett 57d1fa8410 Merge pull request #1660 from SimonBrandner/feed
Support for multiple streams (not MSC3077)
2021-05-06 11:19:52 +01:00
Michael Telatynski 838e38d84c Merge pull request #1683 from matrix-org/t3chguy/spaces-logs
Tweak missing m.room.create errors to describe their source
2021-05-06 00:06:38 +01:00
Michael Telatynski fc29056530 Tweak missing m.room.create errors to describe their source 2021-05-05 23:58:40 +01:00
RiotRobot 4e967c979c v10.1.0-rc.1 2021-05-04 15:37:55 +01:00
RiotRobot 62a34848c7 Prepare changelog for v10.1.0-rc.1 2021-05-04 15:37:54 +01:00
Travis Ralston 01fe6cc542 Merge pull request #1681 from matrix-org/revert-1678-travis/event-logging
Revert "Raise logging dramatically to chase pending event errors"
2021-05-02 19:11:06 -06:00
Travis Ralston 3fdc25777d Revert "Raise logging dramatically to chase pending event errors" 2021-05-02 19:10:47 -06:00
Šimon Brandner 3b7d6f8334 This check doesn't seem to be necessary
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-04-30 14:43:00 +02:00
Šimon Brandner 4b3c8b2969 Update the example to work with the new feed code
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-04-30 14:18:20 +02:00
Šimon Brandner ad80d69369 Add some basic styling
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-04-30 14:17:30 +02:00
Michael Telatynski e2d2249686 Switch from MSC1772 unstable prefixes to stable 2021-04-30 11:33:45 +01:00
J. Ryan Stinnett dc64c34ccb Merge pull request #1677 from matrix-org/jryans/coverage
Add test coverage collection script
2021-04-28 17:18:07 +01:00
Travis Ralston 0bf50659ab Merge pull request #1678 from matrix-org/travis/event-logging
Raise logging dramatically to chase pending event errors
2021-04-28 09:30:04 -06:00
Travis Ralston ec26b16ddf Raise logging dramatically to chase pending event errors
For https://github.com/vector-im/element-web/issues/17090 and similar issues

This logging is expected to exist no longer than a day.
2021-04-28 09:24:19 -06:00
J. Ryan Stinnett a044b74a1d Add test coverage collection script
This makes it clear to how collect basic test coverage when desired.
2021-04-28 14:17:08 +01:00
J. Ryan Stinnett 1e7a1dce90 Move Jest options into config block 2021-04-28 12:58:39 +01:00
Šimon Brandner 41c2772cff Merge branch 'develop' into feed
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-04-28 10:57:59 +02:00
David Baker 1bba2bc0ed Merge pull request #1674 from matrix-org/dbkr/asserted_identity
Support MSC3086 asserted identity
2021-04-28 09:47:30 +01:00
Šimon Brandner e11c523a75 Add getLocalFeeds() and getRemoteFeeds()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-04-27 10:06:21 +02:00
RiotRobot b911a890cf Add breaking notice 2021-04-26 18:03:19 +01:00
RiotRobot c8f69c0b79 Resetting package fields for development 2021-04-26 17:37:06 +01:00
RiotRobot 8a6248f120 Merge branch 'master' into develop 2021-04-26 17:37:06 +01:00
Šimon Brandner 88b310c394 Rename stuff to make it easy to read
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-04-26 16:00:17 +02:00
Šimon Brandner 973de2db55 stopAllMedia() before deleteAllFeeds()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-04-24 12:56:28 +02:00
Šimon Brandner 1fe92f10c1 Delint
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-04-24 12:26:54 +02:00
Šimon Brandner 0e2e906b24 Remove remoteStream prop
This is done in order to be more generic

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-04-24 12:24:15 +02:00
Šimon Brandner 4667f8be03 Use feeds in stopAllMedia()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-04-24 12:08:45 +02:00
David Baker 4a51ac7a74 Move createNewMatrixCall to the client object
So we can mock it out it tests (and also I'm not sure why it was
like this in the first place: we passed the client in anyway...)

Deprecate createNewMatrixCall
2021-04-23 14:36:56 +01:00
J. Ryan Stinnett 6099efe41a Merge pull request #1670 from timokoesters/fix-undefined-results
Fix `/search` with no results field work again
2021-04-23 13:02:52 +01:00
Germain 4254d595fc Merge pull request #1672 from hannojg/patch-5 2021-04-22 09:27:22 +01:00
Timo Kösters 069ca4a89d fix: make /search with no results field work again 2021-04-21 22:56:36 +02:00
David Baker bca8568d64 missing semicolon 2021-04-20 12:58:33 +01:00
David Baker 5407717534 Assert event emitted 2021-04-20 12:57:11 +01:00
David Baker 74ef760591 Tests: They find bugs 2021-04-20 12:48:54 +01:00
David Baker 1b31d0622e Unit test for asserted identity messages 2021-04-20 11:08:06 +01:00
David Baker 56797948af lint 2021-04-19 20:36:00 +01:00
David Baker c0af2f25a1 Support MSC3086 asserted identity 2021-04-19 20:28:42 +01:00
Hanno J. Gödecke dc12b1df00 feat: room.getMembers
Signed-off-by: Hanno Gödecke <hgoedecke@cuvent.com>
2021-04-18 16:01:51 +02:00
Šimon Brandner 72a0931663 Remove comment: // Fix when client is TSified
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-04-16 11:38:01 +02:00
Šimon Brandner 67584b9cc3 Merge branch 'feed' of https://github.com/SimonBrandner/matrix-js-sdk into feed 2021-04-16 11:10:22 +02:00
Šimon Brandner 1bfaa28f9c Fix missing types
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-04-16 11:09:28 +02:00
Šimon Brandner 07f49bcc37 Merge branch 'develop' into feed 2021-04-13 12:33:23 +02:00
Šimon Brandner 193de8d3a9 Merge branch 'feed' of https://github.com/SimonBrandner/matrix-js-sdk into feed 2021-04-08 11:15:07 +02:00
Šimon Brandner 1f2b3512c1 Move SDPStreamMetadataPurpose into callEventTypes.ts
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-04-08 11:14:42 +02:00
Šimon Brandner ddc5bd3b36 Add prefixes to SDPStreamMetadataPurpose
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-04-08 11:07:53 +02:00
Šimon Brandner ee828d454a Rename CallFeedPurpose to SDPStreamMetadataPurpose
This is to match MSC3077

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-04-08 11:07:22 +02:00
Šimon Brandner 2916a73f4c Make selectDesktopCapturerSource optional 2021-04-08 11:01:17 +02:00
Šimon Brandner ae69af7e70 Merge branch 'develop' into feed 2021-04-07 19:14:17 +02:00
Šimon Brandner 7f4397f8ca Use getAudioTracks() and getVideoTracks()
This is much nicer. Before I hadn't realized this was possible

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-04-05 10:31:27 +02:00
Šimon Brandner 32830b93f1 Rename audioOnly to videoMuted
This makes more sense and will match a possible mute events MSC

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-04-04 08:50:27 +02:00
Šimon Brandner cdc0d5623b Rename to match MSC3077
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-04-04 08:37:09 +02:00
Šimon Brandner e78b415832 Add getMember() to CallFeed
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-04-04 08:33:51 +02:00
Šimon Brandner ff1379fd29 Merge branch 'develop' into feed 2021-04-03 08:35:05 +02:00
Šimon Brandner d8134aa168 Merge branch 'feed' into feed-audio 2021-03-17 16:16:33 +01:00
Šimon Brandner 702b3e8473 Merge branch 'develop' into feed
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-03-17 16:16:07 +01:00
Šimon Brandner b307a177f4 Remove handling of audio from MatrixCall
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-03-10 12:24:48 +01:00
Šimon Brandner 059430bd0a Doc public methods
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-03-10 12:21:15 +01:00
Šimon Brandner 530b60cbc2 Make MatrixCall use CallFeed
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-03-10 12:21:07 +01:00
Šimon Brandner ba8577f268 Add CallFeed class
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-03-10 08:43:01 +01:00
35 changed files with 726 additions and 764 deletions
+76
View File
@@ -1,3 +1,71 @@
Changes in [11.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v11.1.0-rc.1) (2021-05-19)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v11.0.0...v11.1.0-rc.1)
* Fix regressed glare
[\#1690](https://github.com/matrix-org/matrix-js-sdk/pull/1690)
* Add m.reaction to EventType enum
[\#1692](https://github.com/matrix-org/matrix-js-sdk/pull/1692)
* Prioritise and reduce the amount of events decrypted on application startup
[\#1684](https://github.com/matrix-org/matrix-js-sdk/pull/1684)
* Decrypt relations before applying them to target event
[\#1696](https://github.com/matrix-org/matrix-js-sdk/pull/1696)
* Guard against duplicates in `Relations` model
Changes in [11.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v11.0.0) (2021-05-17)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v11.0.0-rc.1...v11.0.0)
* [Release] Fix regressed glare
[\#1695](https://github.com/matrix-org/matrix-js-sdk/pull/1695)
Changes in [11.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v11.0.0-rc.1) (2021-05-11)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v10.1.0...v11.0.0-rc.1)
BREAKING CHANGES
---
* `MatrixCall` and related APIs have been redesigned to support multiple streams
(see [\#1660](https://github.com/matrix-org/matrix-js-sdk/pull/1660) for more details)
All changes
---
* Switch from MSC1772 unstable prefixes to stable
[\#1679](https://github.com/matrix-org/matrix-js-sdk/pull/1679)
* Update the VoIP example to work with the new changes
[\#1680](https://github.com/matrix-org/matrix-js-sdk/pull/1680)
* Bump hosted-git-info from 2.8.8 to 2.8.9
[\#1687](https://github.com/matrix-org/matrix-js-sdk/pull/1687)
* Support for multiple streams (not MSC3077)
[\#1660](https://github.com/matrix-org/matrix-js-sdk/pull/1660)
* Tweak missing m.room.create errors to describe their source
[\#1683](https://github.com/matrix-org/matrix-js-sdk/pull/1683)
Changes in [10.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v10.1.0) (2021-05-10)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v10.1.0-rc.1...v10.1.0)
* No changes since rc.1
Changes in [10.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v10.1.0-rc.1) (2021-05-04)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v10.0.0...v10.1.0-rc.1)
* Revert "Raise logging dramatically to chase pending event errors"
[\#1681](https://github.com/matrix-org/matrix-js-sdk/pull/1681)
* Add test coverage collection script
[\#1677](https://github.com/matrix-org/matrix-js-sdk/pull/1677)
* Raise logging dramatically to chase pending event errors
[\#1678](https://github.com/matrix-org/matrix-js-sdk/pull/1678)
* Support MSC3086 asserted identity
[\#1674](https://github.com/matrix-org/matrix-js-sdk/pull/1674)
* Fix `/search` with no results field work again
[\#1670](https://github.com/matrix-org/matrix-js-sdk/pull/1670)
* Add room.getMembers method
[\#1672](https://github.com/matrix-org/matrix-js-sdk/pull/1672)
Changes in [10.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v10.0.0) (2021-04-26)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v10.0.0-rc.1...v10.0.0)
@@ -8,6 +76,14 @@ Changes in [10.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/ta
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.11.0...v10.0.0-rc.1)
BREAKING CHANGES
---
* The `RoomState.members` event is now only emitted when the room member's power level or the room's normal power level actually changes
All changes
---
* Restrict event emit for room members that had power levels changed
[\#1675](https://github.com/matrix-org/matrix-js-sdk/pull/1675)
* Fix sync with misconfigured push rules
+18 -4
View File
@@ -31,6 +31,23 @@ function addListeners(call) {
call.hangup();
disableButtons(false, true, true);
});
call.on("feeds_changed", function(feeds) {
const localFeed = feeds.find((feed) => feed.isLocal());
const remoteFeed = feeds.find((feed) => !feed.isLocal());
const remoteElement = document.getElementById("remote");
const localElement = document.getElementById("local");
if (remoteFeed) {
remoteElement.srcObject = remoteFeed.stream;
remoteElement.play();
}
if (localFeed) {
localElement.muted = true;
localElement.srcObject = localFeed.stream;
localElement.play();
}
});
}
window.onload = function() {
@@ -62,10 +79,7 @@ function syncComplete() {
);
console.log("Call => %s", call);
addListeners(call);
call.placeVideoCall(
document.getElementById("remote"),
document.getElementById("local")
);
call.placeVideoCall();
document.getElementById("result").innerHTML = "<p>Placed call.</p>";
disableButtons(true, true, false);
};
+21 -13
View File
@@ -1,26 +1,34 @@
<html>
<head>
<title>VoIP Test</title>
<script src="lib/matrix.js"></script>
<script src="browserTest.js"></script>
<title>VoIP Test</title>
<script src="lib/matrix.js"></script>
<script src="browserTest.js"></script>
</head>
<body>
You can place and receive calls with this example. Make sure to edit the
You can place and receive calls with this example. Make sure to edit the
constants in <code>browserTest.js</code> first.
<div id="config"></div>
<div id="result"></div>
<button id="call">Place Call</button>
<button id="answer">Answer Call</button>
<button id="hangup">Hangup Call</button>
<div id="videoBackground">
<div id="videoContainer">
<video id="remote"></video>
</div>
</div>
<div id="videoBackground">
<div id="videoContainer">
<video id="local"></video>
</div>
<div id="videoBackground" class="video-background">
<video class="video-element" id="local"></video>
<video class="video-element" id="remote"></video>
</div>
</body>
</html>
<style>
.video-background {
height: 500px;
margin: 10px;
}
.video-element {
height: 100%;
}
</style>
+14 -4
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "10.0.0",
"version": "11.1.0-rc.1",
"description": "Matrix Client-Server SDK for Javascript",
"scripts": {
"prepublishOnly": "yarn build",
@@ -17,8 +17,9 @@
"lint": "yarn lint:types && yarn lint:js",
"lint:js": "eslint --max-warnings 72 src spec",
"lint:types": "tsc --noEmit",
"test": "jest spec/ --coverage --testEnvironment node",
"test:watch": "jest spec/ --coverage --testEnvironment node --watch"
"test": "jest",
"test:watch": "jest --watch",
"coverage": "yarn test --coverage"
},
"repository": {
"type": "git",
@@ -97,7 +98,16 @@
"typescript": "^4.1.3"
},
"jest": {
"testEnvironment": "node"
"testEnvironment": "node",
"testMatch": [
"<rootDir>/spec/**/*.spec.{js,ts}"
],
"collectCoverageFrom": [
"<rootDir>/src/**/*.{js,ts}"
],
"coverageReporters": [
"text"
]
},
"typings": "./lib/index.d.ts"
}
+2 -3
View File
@@ -30,7 +30,6 @@ import '../olm-loader';
import {logger} from '../../src/logger';
import * as testUtils from "../test-utils";
import * as utils from "../../src/utils";
import {TestClient} from "../TestClient";
import {CRYPTO_ENABLED} from "../../src/client";
@@ -244,7 +243,7 @@ function bobSendsReplyMessage() {
function expectAliSendMessageRequest() {
return expectSendMessageRequest(aliTestClient.httpBackend).then(function(content) {
aliMessages.push(content);
expect(utils.keys(content.ciphertext)).toEqual([bobTestClient.getDeviceKey()]);
expect(Object.keys(content.ciphertext)).toEqual([bobTestClient.getDeviceKey()]);
const ciphertext = content.ciphertext[bobTestClient.getDeviceKey()];
expect(ciphertext).toBeTruthy();
return ciphertext;
@@ -261,7 +260,7 @@ function expectBobSendMessageRequest() {
bobMessages.push(content);
const aliKeyId = "curve25519:" + aliDeviceId;
const aliDeviceCurve25519Key = aliTestClient.deviceKeys.keys[aliKeyId];
expect(utils.keys(content.ciphertext)).toEqual([aliDeviceCurve25519Key]);
expect(Object.keys(content.ciphertext)).toEqual([aliDeviceCurve25519Key]);
const ciphertext = content.ciphertext[aliDeviceCurve25519Key];
expect(ciphertext).toBeTruthy();
return ciphertext;
+6 -5
View File
@@ -16,7 +16,6 @@ limitations under the License.
*/
import anotherjson from "another-json";
import * as utils from "../../src/utils";
import * as testUtils from "../test-utils";
import {TestClient} from "../TestClient";
import {logger} from "../../src/logger";
@@ -32,7 +31,7 @@ const ROOM_ID = "!room:id";
*/
function createOlmSession(olmAccount, recipientTestClient) {
return recipientTestClient.awaitOneTimeKeyUpload().then((keys) => {
const otkId = utils.keys(keys)[0];
const otkId = Object.keys(keys)[0];
const otk = keys[otkId];
const session = new global.Olm.Session();
@@ -257,7 +256,7 @@ describe("megolm", function() {
const testOneTimeKeys = JSON.parse(testOlmAccount.one_time_keys());
testOlmAccount.mark_keys_as_published();
const keyId = utils.keys(testOneTimeKeys.curve25519)[0];
const keyId = Object.keys(testOneTimeKeys.curve25519)[0];
const oneTimeKey = testOneTimeKeys.curve25519[keyId];
const keyResult = {
'key': oneTimeKey,
@@ -484,8 +483,9 @@ describe("megolm", function() {
return aliceTestClient.flushSync().then(() => {
return aliceTestClient.flushSync();
});
}).then(function() {
}).then(async function() {
const room = aliceTestClient.client.getRoom(ROOM_ID);
await room.decryptCriticalEvents();
const event = room.getLiveTimeline().getEvents()[0];
expect(event.getContent().body).toEqual('42');
});
@@ -933,8 +933,9 @@ describe("megolm", function() {
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
return aliceTestClient.flushSync();
}).then(function() {
}).then(async function() {
const room = aliceTestClient.client.getRoom(ROOM_ID);
await room.decryptCriticalEvents();
const event = room.getLiveTimeline().getEvents()[0];
expect(event.getContent().body).toEqual('42');
+13 -10
View File
@@ -212,18 +212,21 @@ MockStorageApi.prototype = {
* @returns {Promise} promise which resolves (to `event`) when the event has been decrypted
*/
export function awaitDecryption(event) {
if (!event.isBeingDecrypted()) {
return Promise.resolve(event);
}
// An event is not always decrypted ahead of time
// getClearContent is a good signal to know whether an event has been decrypted
// already
if (event.getClearContent() !== null) {
return event;
} else {
logger.log(`${Date.now()} event ${event.getId()} is being decrypted; waiting`);
logger.log(`${Date.now()} event ${event.getId()} is being decrypted; waiting`);
return new Promise((resolve, reject) => {
event.once('Event.decrypted', (ev) => {
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
resolve(ev);
return new Promise((resolve, reject) => {
event.once('Event.decrypted', (ev) => {
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
resolve(ev);
});
});
});
}
}
+73
View File
@@ -0,0 +1,73 @@
/*
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 { MatrixEvent } from "../../src/models/event";
import { Relations } from "../../src/models/relations";
describe("Relations", function() {
it("should deduplicate annotations", function() {
const relations = new Relations("m.annotation", "m.reaction");
// Create an instance of an annotation
const eventData = {
"sender": "@bob:example.com",
"type": "m.reaction",
"event_id": "$cZ1biX33ENJqIm00ks0W_hgiO_6CHrsAc3ZQrnLeNTw",
"room_id": "!pzVjCQSoQPpXQeHpmK:example.com",
"content": {
"m.relates_to": {
"event_id": "$2s4yYpEkVQrPglSCSqB_m6E8vDhWsg0yFNyOJdVIb_o",
"key": "👍️",
"rel_type": "m.annotation",
},
},
};
const eventA = new MatrixEvent(eventData);
// Add the event once and check results
{
relations.addEvent(eventA);
const annotationsByKey = relations.getSortedAnnotationsByKey();
expect(annotationsByKey.length).toEqual(1);
const [key, events] = annotationsByKey[0];
expect(key).toEqual("👍️");
expect(events.size).toEqual(1);
}
// Add the event again and expect the same
{
relations.addEvent(eventA);
const annotationsByKey = relations.getSortedAnnotationsByKey();
expect(annotationsByKey.length).toEqual(1);
const [key, events] = annotationsByKey[0];
expect(key).toEqual("👍️");
expect(events.size).toEqual(1);
}
// Create a fresh object with the same event content
const eventB = new MatrixEvent(eventData);
// Add the event again and expect the same
{
relations.addEvent(eventB);
const annotationsByKey = relations.getSortedAnnotationsByKey();
expect(annotationsByKey.length).toEqual(1);
const [key, events] = annotationsByKey[0];
expect(key).toEqual("👍️");
expect(events.size).toEqual(1);
}
});
});
-48
View File
@@ -26,40 +26,6 @@ describe("utils", function() {
});
});
describe("forEach", function() {
it("should be invoked for each element", function() {
const arr = [];
utils.forEach([55, 66, 77], function(element) {
arr.push(element);
});
expect(arr).toEqual([55, 66, 77]);
});
});
describe("findElement", function() {
it("should find only 1 element if there is a match", function() {
const matchFn = function() {
return true;
};
const arr = [55, 66, 77];
expect(utils.findElement(arr, matchFn)).toEqual(55);
});
it("should be able to find in reverse order", function() {
const matchFn = function() {
return true;
};
const arr = [55, 66, 77];
expect(utils.findElement(arr, matchFn, true)).toEqual(77);
});
it("should find nothing if the function never returns true", function() {
const matchFn = function() {
return false;
};
const arr = [55, 66, 77];
expect(utils.findElement(arr, matchFn)).toBeFalsy();
});
});
describe("removeElement", function() {
it("should remove only 1 element if there is a match", function() {
const matchFn = function() {
@@ -103,20 +69,6 @@ describe("utils", function() {
});
});
describe("isArray", function() {
it("should return true for arrays", function() {
expect(utils.isArray([])).toBe(true);
expect(utils.isArray([5, 3, 7])).toBe(true);
expect(utils.isArray()).toBe(false);
expect(utils.isArray(null)).toBe(false);
expect(utils.isArray({})).toBe(false);
expect(utils.isArray("foo")).toBe(false);
expect(utils.isArray(555)).toBe(false);
expect(utils.isArray(function() {})).toBe(false);
});
});
describe("checkObjectHasKeys", function() {
it("should throw for missing keys", function() {
expect(function() {
+45 -1
View File
@@ -15,7 +15,7 @@ limitations under the License.
*/
import {TestClient} from '../../TestClient';
import {MatrixCall, CallErrorCode} from '../../../src/webrtc/call';
import {MatrixCall, CallErrorCode, CallEvent} from '../../../src/webrtc/call';
const DUMMY_SDP = (
"v=0\r\n" +
@@ -254,4 +254,48 @@ describe('Call', function() {
sdpMid: '',
});
});
it('should map asserted identity messages to remoteAssertedIdentity', async function() {
const callPromise = call.placeVoiceCall();
await client.httpBackend.flush();
await callPromise;
await call.onAnswerReceived({
getContent: () => {
return {
version: 1,
call_id: call.callId,
party_id: 'party_id',
answer: {
sdp: DUMMY_SDP,
},
};
},
});
const identChangedCallback = jest.fn();
call.on(CallEvent.AssertedIdentityChanged, identChangedCallback);
await call.onAssertedIdentityReceived({
getContent: () => {
return {
version: 1,
call_id: call.callId,
party_id: 'party_id',
asserted_identity: {
id: "@steve:example.com",
display_name: "Steve Gibbons",
},
};
},
});
expect(identChangedCallback).toHaveBeenCalled();
const ident = call.getRemoteAssertedIdentity();
expect(ident.id).toEqual("@steve:example.com");
expect(ident.displayName).toEqual("Steve Gibbons");
// Hangup to stop timers
call.hangup(CallErrorCode.UserHangup, true);
});
});
+7 -5
View File
@@ -36,9 +36,8 @@ export enum EventType {
*/
RoomAliases = "m.room.aliases", // deprecated https://matrix.org/docs/spec/client_server/r0.6.1#historical-events
// Spaces MSC1772
SpaceChild = "org.matrix.msc1772.space.child",
SpaceParent = "org.matrix.msc1772.space.parent",
SpaceChild = "m.space.child",
SpaceParent = "m.space.parent",
// Room timeline events
RoomRedaction = "m.room.redaction",
@@ -53,6 +52,8 @@ export enum EventType {
CallSelectAnswer = "m.call.select_answer",
CallNegotiate = "m.call.negotiate",
CallReplaces = "m.call.replaces",
CallAssertedIdentity = "m.call.asserted_identity",
CallAssertedIdentityPrefix = "org.matrix.call.asserted_identity",
KeyVerificationRequest = "m.key.verification.request",
KeyVerificationStart = "m.key.verification.start",
KeyVerificationCancel = "m.key.verification.cancel",
@@ -60,6 +61,7 @@ export enum EventType {
KeyVerificationDone = "m.key.verification.done",
// use of this is discouraged https://matrix.org/docs/spec/client_server/r0.6.1#m-room-message-feedback
RoomMessageFeedback = "m.room.message.feedback",
Reaction = "m.reaction",
// Room ephemeral events
Typing = "m.typing",
@@ -93,8 +95,8 @@ export enum MsgType {
Video = "m.video",
}
export const RoomCreateTypeField = "org.matrix.msc1772.type"; // Spaces MSC1772
export const RoomCreateTypeField = "type";
export enum RoomType {
Space = "org.matrix.msc1772.space", // Spaces MSC1772
Space = "m.space",
}
+47 -11
View File
@@ -729,6 +729,17 @@ MatrixClient.prototype.setSupportsCallTransfer = function(supportsCallTransfer)
this._supportsCallTransfer = supportsCallTransfer;
};
/**
* Creates a new call.
* The place*Call methods on the returned call can be used to actually place a call
*
* @param {string} roomId The room the call is to be placed in.
* @return {MatrixCall} the call or null if the browser doesn't support calling.
*/
MatrixClient.prototype.createCall = function(roomId) {
return createNewMatrixCall(this, roomId);
};
/**
* Get the current sync state.
* @return {?string} the sync state, which may be null.
@@ -3965,9 +3976,9 @@ MatrixClient.prototype.scrollback = function(room, limit, callback) {
limit,
'b');
}).then(function(res) {
const matrixEvents = utils.map(res.chunk, _PojoToMatrixEventMapper(self));
const matrixEvents = res.chunk.map(_PojoToMatrixEventMapper(self));
if (res.state) {
const stateEvents = utils.map(res.state, _PojoToMatrixEventMapper(self));
const stateEvents = res.state.map(_PojoToMatrixEventMapper(self));
room.currentState.setUnknownStateEvents(stateEvents);
}
room.addEventsToTimeline(matrixEvents, true, room.getLiveTimeline());
@@ -4056,16 +4067,16 @@ MatrixClient.prototype.getEventTimeline = function(timelineSet, eventId) {
const events = res.events_after
.concat([res.event])
.concat(res.events_before);
const matrixEvents = utils.map(events, self.getEventMapper());
const matrixEvents = events.map(self.getEventMapper());
let timeline = timelineSet.getTimelineForEvent(matrixEvents[0].getId());
if (!timeline) {
timeline = timelineSet.addTimeline();
timeline.initialiseState(utils.map(res.state,
timeline.initialiseState(res.state.map(
self.getEventMapper()));
timeline.getState(EventTimeline.FORWARDS).paginationToken = res.end;
} else {
const stateEvents = utils.map(res.state, self.getEventMapper());
const stateEvents = res.state.map(self.getEventMapper());
timeline.getState(EventTimeline.BACKWARDS).setUnknownStateEvents(stateEvents);
}
timelineSet.addEventsToTimeline(matrixEvents, true, timeline, res.start);
@@ -4228,11 +4239,11 @@ MatrixClient.prototype.paginateEventTimeline = function(eventTimeline, opts) {
promise.then(function(res) {
if (res.state) {
const roomState = eventTimeline.getState(dir);
const stateEvents = utils.map(res.state, self.getEventMapper());
const stateEvents = res.state.map(self.getEventMapper());
roomState.setUnknownStateEvents(stateEvents);
}
const token = res.end;
const matrixEvents = utils.map(res.chunk, self.getEventMapper());
const matrixEvents = res.chunk.map(self.getEventMapper());
eventTimeline.getTimelineSet()
.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
@@ -4799,7 +4810,8 @@ MatrixClient.prototype._processRoomEventsSearch = function(searchResults, respon
searchResults.highlights = Object.keys(highlights);
// append the new results to our existing results
for (let i = 0; i < room_events.results.length; i++) {
const resultsLength = room_events.results ? room_events.results.length : 0;
for (let i = 0; i < resultsLength; i++) {
const sr = SearchResult.fromJson(room_events.results[i], this.getEventMapper());
searchResults.results.push(sr);
}
@@ -5542,8 +5554,9 @@ function _resolve(callback, resolve, res) {
resolve(res);
}
function _PojoToMatrixEventMapper(client, options) {
const preventReEmit = Boolean(options && options.preventReEmit);
function _PojoToMatrixEventMapper(client, options = {}) {
const preventReEmit = Boolean(options.preventReEmit);
const decrypt = options.decrypt !== false;
function mapper(plainOldJsObject) {
const event = new MatrixEvent(plainOldJsObject);
if (event.isEncrypted()) {
@@ -5552,7 +5565,9 @@ function _PojoToMatrixEventMapper(client, options) {
"Event.decrypted",
]);
}
event.attemptDecryption(client._crypto);
if (decrypt) {
client.decryptEventIfNeeded(event);
}
}
if (!preventReEmit) {
client.reEmitter.reEmit(event, ["Event.replaced"]);
@@ -5565,6 +5580,7 @@ function _PojoToMatrixEventMapper(client, options) {
/**
* @param {object} [options]
* @param {bool} options.preventReEmit don't reemit events emitted on an event mapped by this mapper on the client
* @param {bool} options.decrypt decrypt event proactively
* @return {Function}
*/
MatrixClient.prototype.getEventMapper = function(options = undefined) {
@@ -5592,6 +5608,26 @@ MatrixClient.prototype.generateClientSecret = function() {
return randomString(32);
};
/**
* Attempts to decrypt an event
* @param {MatrixEvent} event The event to decrypt
* @returns {Promise<void>} A decryption promise
* @param {object} options
* @param {bool} options.isRetry True if this is a retry (enables more logging)
* @param {bool} options.emit Emits "event.decrypted" if set to true
*/
MatrixClient.prototype.decryptEventIfNeeded = function(event, options) {
if (event.shouldAttemptDecryption()) {
event.attemptDecryption(this._crypto, options);
}
if (event.isBeingDecrypted()) {
return event._decryptionPromise;
} else {
return Promise.resolve();
}
};
// MatrixClient Event JSDocs
/**
+2 -2
View File
@@ -59,7 +59,7 @@ export function getHttpUriForMxc(baseUrl, mxc, width, height,
if (resizeMethod) {
params.method = resizeMethod;
}
if (utils.keys(params).length > 0) {
if (Object.keys(params).length > 0) {
// these are thumbnailing params so they probably want the
// thumbnailing API...
prefix = "/_matrix/media/r0/thumbnail/";
@@ -72,6 +72,6 @@ export function getHttpUriForMxc(baseUrl, mxc, width, height,
serverAndMediaId = serverAndMediaId.substr(0, fragmentOffset);
}
return baseUrl + prefix + serverAndMediaId +
(utils.keys(params).length === 0 ? "" :
(Object.keys(params).length === 0 ? "" :
("?" + utils.encodeParams(params))) + fragment;
}
+1 -2
View File
@@ -15,7 +15,6 @@ limitations under the License.
*/
import {logger} from '../logger';
import * as utils from '../utils';
/**
* Internal module. Management of outgoing room key requests.
@@ -496,7 +495,7 @@ function stringifyRequestBody(requestBody) {
function stringifyRecipientList(recipients) {
return '['
+ utils.map(recipients, (r) => `${r.userId}:${r.deviceId}`).join(",")
+ recipients.map((r) => `${r.userId}:${r.deviceId}`).join(",")
+ ']';
}
+3 -3
View File
@@ -1076,7 +1076,7 @@ MegolmEncryption.prototype._removeUnknownDevices = function(devicesInRoom) {
*/
MegolmEncryption.prototype._getDevicesInRoom = async function(room) {
const members = await room.getEncryptionTargetMembers();
const roomMembers = utils.map(members, function(u) {
const roomMembers = members.map(function(u) {
return u.userId;
});
@@ -1371,7 +1371,7 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
if (event.getType() == "m.forwarded_room_key") {
exportFormat = true;
forwardingKeyChain = content.forwarding_curve25519_key_chain;
if (!utils.isArray(forwardingKeyChain)) {
if (!Array.isArray(forwardingKeyChain)) {
forwardingKeyChain = [];
}
@@ -1692,7 +1692,7 @@ MegolmDecryption.prototype._retryDecryption = async function(senderKey, sessionI
await Promise.all([...pending].map(async (ev) => {
try {
await ev.attemptDecryption(this._crypto, true);
await ev.attemptDecryption(this._crypto, { isRetry: true });
} catch (e) {
// don't die if something goes wrong
}
+1 -1
View File
@@ -95,7 +95,7 @@ OlmEncryption.prototype.encryptMessage = async function(room, eventType, content
const members = await room.getEncryptionTargetMembers();
const users = utils.map(members, function(u) {
const users = members.map(function(u) {
return u.userId;
});
+5 -2
View File
@@ -186,7 +186,7 @@ export function Crypto(baseApis, sessionStore, userId, deviceId,
// map from algorithm to DecryptionAlgorithm instance, for each room
this._roomDecryptors = {};
this._supportedAlgorithms = utils.keys(
this._supportedAlgorithms = Object.keys(
algorithms.DECRYPTION_CLASSES,
);
@@ -3322,7 +3322,10 @@ Crypto.prototype._onToDeviceEvent = function(event) {
this._onKeyVerificationMessage(event);
} else if (event.getContent().msgtype === "m.bad.encrypted") {
this._onToDeviceBadEncrypted(event);
} else if (event.isBeingDecrypted()) {
} else if (event.isBeingDecrypted() || event.shouldAttemptDecryption()) {
if (!event.isBeingDecrypted()) {
event.attemptDecryption(this);
}
// once the event has been decrypted, try again
event.once('Event.decrypted', (ev) => {
this._onToDeviceEvent(ev);
-1
View File
@@ -19,7 +19,6 @@ import * as utils from "./utils";
import request from "request";
matrixcs.request(request);
utils.runPolyfills();
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
-1
View File
@@ -55,7 +55,6 @@ export * from "./content-repo";
export * as ContentHelpers from "./content-helpers";
export {
createNewMatrixCall,
setAudioOutput as setMatrixCallAudioOutput,
setAudioInput as setMatrixCallAudioInput,
setVideoInput as setMatrixCallVideoInput,
} from "./webrtc/call";
+2 -2
View File
@@ -249,7 +249,7 @@ EventTimelineSet.prototype.findEventById = function(eventId) {
if (!tl) {
return undefined;
}
return utils.findElement(tl.getEvents(), function(ev) {
return tl.getEvents().find(function(ev) {
return ev.getId() == eventId;
});
};
@@ -769,7 +769,7 @@ EventTimelineSet.prototype.aggregateRelations = function(event) {
}
// If the event is currently encrypted, wait until it has been decrypted.
if (event.isBeingDecrypted()) {
if (event.isBeingDecrypted() || event.shouldAttemptDecryption()) {
event.once("Event.decrypted", () => {
this.aggregateRelations(event);
});
+25 -7
View File
@@ -399,6 +399,12 @@ utils.extend(MatrixEvent.prototype, {
this._clearEvent.content.msgtype === "m.bad.encrypted";
},
shouldAttemptDecryption: function() {
return this.isEncrypted()
&& !this.isBeingDecrypted()
&& this.getClearContent() === null;
},
/**
* Start the process of trying to decrypt this event.
*
@@ -407,12 +413,22 @@ utils.extend(MatrixEvent.prototype, {
* @internal
*
* @param {module:crypto} crypto crypto module
* @param {bool} isRetry True if this is a retry (enables more logging)
* @param {object} options
* @param {bool} options.isRetry True if this is a retry (enables more logging)
* @param {bool} options.emit Emits "event.decrypted" if set to true
*
* @returns {Promise} promise which resolves (to undefined) when the decryption
* attempt is completed.
*/
attemptDecryption: async function(crypto, isRetry) {
attemptDecryption: async function(crypto, options = {}) {
// For backwards compatibility purposes
// The function signature used to be attemptDecryption(crypto, isRetry)
if (typeof options === "boolean") {
options = {
isRetry: options,
};
}
// start with a couple of sanity checks.
if (!this.isEncrypted()) {
throw new Error("Attempt to decrypt event which isn't encrypted");
@@ -442,7 +458,7 @@ utils.extend(MatrixEvent.prototype, {
return this._decryptionPromise;
}
this._decryptionPromise = this._decryptionLoop(crypto, isRetry);
this._decryptionPromise = this._decryptionLoop(crypto, options);
return this._decryptionPromise;
},
@@ -487,7 +503,7 @@ utils.extend(MatrixEvent.prototype, {
return recipients;
},
_decryptionLoop: async function(crypto, isRetry) {
_decryptionLoop: async function(crypto, options = {}) {
// make sure that this method never runs completely synchronously.
// (doing so would mean that we would clear _decryptionPromise *before*
// it is set in attemptDecryption - and hence end up with a stuck
@@ -504,7 +520,7 @@ utils.extend(MatrixEvent.prototype, {
res = this._badEncryptedMessage("Encryption not enabled");
} else {
res = await crypto.decryptEvent(this);
if (isRetry) {
if (options.isRetry === true) {
logger.info(`Decrypted event on retry (id=${this.getId()})`);
}
}
@@ -512,7 +528,7 @@ utils.extend(MatrixEvent.prototype, {
if (e.name !== "DecryptionError") {
// not a decryption error: log the whole exception as an error
// (and don't bother with a retry)
const re = isRetry ? 're' : '';
const re = options.isRetry ? 're' : '';
logger.error(
`Error ${re}decrypting event ` +
`(id=${this.getId()}): ${e.stack || e}`,
@@ -578,7 +594,9 @@ utils.extend(MatrixEvent.prototype, {
// pick up the wrong contents.
this.setPushActions(null);
this.emit("Event.decrypted", this, err);
if (options.emit !== false) {
this.emit("Event.decrypted", this, err);
}
return;
}
+25 -11
View File
@@ -41,11 +41,13 @@ export class Relations extends EventEmitter {
super();
this.relationType = relationType;
this.eventType = eventType;
this._relationEventIds = new Set();
this._relations = new Set();
this._annotationsByKey = {};
this._annotationsBySender = {};
this._sortedAnnotationsByKey = [];
this._targetEvent = null;
this._room = room;
}
/**
@@ -54,8 +56,8 @@ export class Relations extends EventEmitter {
* @param {MatrixEvent} event
* The new relation event to be added.
*/
addEvent(event) {
if (this._relations.has(event)) {
async addEvent(event) {
if (this._relationEventIds.has(event.getId())) {
return;
}
@@ -80,11 +82,13 @@ export class Relations extends EventEmitter {
}
this._relations.add(event);
this._relationEventIds.add(event.getId());
if (this.relationType === "m.annotation") {
this._addAnnotationToAggregation(event);
} else if (this.relationType === "m.replace" && this._targetEvent) {
this._targetEvent.makeReplaced(this.getLastReplacement());
const lastReplacement = await this.getLastReplacement();
this._targetEvent.makeReplaced(lastReplacement);
}
event.on("Event.beforeRedaction", this._onBeforeRedaction);
@@ -98,7 +102,7 @@ export class Relations extends EventEmitter {
* @param {MatrixEvent} event
* The relation event to remove.
*/
_removeEvent(event) {
async _removeEvent(event) {
if (!this._relations.has(event)) {
return;
}
@@ -122,7 +126,8 @@ export class Relations extends EventEmitter {
if (this.relationType === "m.annotation") {
this._removeAnnotationFromAggregation(event);
} else if (this.relationType === "m.replace" && this._targetEvent) {
this._targetEvent.makeReplaced(this.getLastReplacement());
const lastReplacement = await this.getLastReplacement();
this._targetEvent.makeReplaced(lastReplacement);
}
this.emit("Relations.remove", event);
@@ -227,7 +232,7 @@ export class Relations extends EventEmitter {
* @param {MatrixEvent} redactedEvent
* The original relation event that is about to be redacted.
*/
_onBeforeRedaction = (redactedEvent) => {
_onBeforeRedaction = async (redactedEvent) => {
if (!this._relations.has(redactedEvent)) {
return;
}
@@ -238,7 +243,8 @@ export class Relations extends EventEmitter {
// Remove the redacted annotation from aggregation by key
this._removeAnnotationFromAggregation(redactedEvent);
} else if (this.relationType === "m.replace" && this._targetEvent) {
this._targetEvent.makeReplaced(this.getLastReplacement());
const lastReplacement = await this.getLastReplacement();
this._targetEvent.makeReplaced(lastReplacement);
}
redactedEvent.removeListener("Event.beforeRedaction", this._onBeforeRedaction);
@@ -291,7 +297,7 @@ export class Relations extends EventEmitter {
*
* @return {MatrixEvent?}
*/
getLastReplacement() {
async getLastReplacement() {
if (this.relationType !== "m.replace") {
// Aggregating on last only makes sense for this relation type
return null;
@@ -309,7 +315,7 @@ export class Relations extends EventEmitter {
this._targetEvent.getServerAggregatedRelation("m.replace");
const minTs = replaceRelation && replaceRelation.origin_server_ts;
return this.getRelations().reduce((last, event) => {
const lastReplacement = this.getRelations().reduce((last, event) => {
if (event.getSender() !== this._targetEvent.getSender()) {
return last;
}
@@ -321,18 +327,26 @@ export class Relations extends EventEmitter {
}
return event;
}, null);
if (lastReplacement?.shouldAttemptDecryption()) {
await lastReplacement.attemptDecryption(this._room._client._crypto);
} else if (lastReplacement?.isBeingDecrypted()) {
await lastReplacement._decryptionPromise;
}
return lastReplacement;
}
/*
* @param {MatrixEvent} targetEvent the event the relations are related to.
*/
setTargetEvent(event) {
async setTargetEvent(event) {
if (this._targetEvent) {
return;
}
this._targetEvent = event;
if (this.relationType === "m.replace") {
const replacement = this.getLastReplacement();
const replacement = await this.getLastReplacement();
// this is the initial update, so only call it if we already have something
// to not emit Event.replaced needlessly
if (replacement) {
+5 -4
View File
@@ -133,14 +133,15 @@ RoomMember.prototype.setPowerLevelEvent = function(powerLevelEvent) {
const evContent = powerLevelEvent.getDirectionalContent();
let maxLevel = evContent.users_default || 0;
utils.forEach(utils.values(evContent.users), function(lvl) {
const users = evContent.users || {};
Object.values(users).forEach(function(lvl) {
maxLevel = Math.max(maxLevel, lvl);
});
const oldPowerLevel = this.powerLevel;
const oldPowerLevelNorm = this.powerLevelNorm;
if (evContent.users && evContent.users[this.userId] !== undefined) {
this.powerLevel = evContent.users[this.userId];
if (users[this.userId] !== undefined) {
this.powerLevel = users[this.userId];
} else if (evContent.users_default !== undefined) {
this.powerLevel = evContent.users_default;
} else {
@@ -172,7 +173,7 @@ RoomMember.prototype.setTypingEvent = function(event) {
const oldTyping = this.typing;
this.typing = false;
const typingList = event.getContent().user_ids;
if (!utils.isArray(typingList)) {
if (!Array.isArray(typingList)) {
// malformed event :/ bail early. TODO: whine?
return;
}
+7 -7
View File
@@ -154,7 +154,7 @@ RoomState.prototype.setInvitedMemberCount = function(count) {
* @return {Array<RoomMember>} A list of RoomMembers.
*/
RoomState.prototype.getMembers = function() {
return utils.values(this.members);
return Object.values(this.members);
};
/**
@@ -163,7 +163,7 @@ RoomState.prototype.getMembers = function() {
* @return {Array<RoomMember>} A list of RoomMembers.
*/
RoomState.prototype.getMembersExcept = function(excludedIds) {
return utils.values(this.members)
return Object.values(this.members)
.filter((m) => !excludedIds.includes(m.userId));
};
@@ -296,7 +296,7 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
this._updateModifiedTime();
// update the core event dict
utils.forEach(stateEvents, function(event) {
stateEvents.forEach(function(event) {
if (event.getRoomId() !== self.roomId) {
return;
}
@@ -319,7 +319,7 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
// core event dict as these structures may depend on other state events in
// the given array (e.g. disambiguating display names in one go to do both
// clashing names rather than progressively which only catches 1 of them).
utils.forEach(stateEvents, function(event) {
stateEvents.forEach(function(event) {
if (event.getRoomId() !== self.roomId) {
return;
}
@@ -349,8 +349,8 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
self._updateMember(member);
self.emit("RoomState.members", event, self, member);
} else if (event.getType() === "m.room.power_levels") {
const members = utils.values(self.members);
utils.forEach(members, function(member) {
const members = Object.values(self.members);
members.forEach(function(member) {
// We only propagate `RoomState.members` event if the
// power levels has been changed
// large room suffer from large re-rendering especially when not needed
@@ -511,7 +511,7 @@ RoomState.prototype._setOutOfBandMember = function(stateEvent) {
* @param {MatrixEvent} event The typing event
*/
RoomState.prototype.setTypingEvent = function(event) {
utils.forEach(utils.values(this.members), function(member) {
Object.values(this.members).forEach(function(member) {
member.setTypingEvent(event);
});
};
+64 -10
View File
@@ -228,6 +228,51 @@ function pendingEventsKey(roomId) {
utils.inherits(Room, EventEmitter);
/**
* Bulk decrypt critical events in a room
*
* Critical events represents the minimal set of events to decrypt
* for a typical UI to function properly
*
* - Last event of every room (to generate likely message preview)
* - All events up to the read receipt (to calculate an accurate notification count)
*
* @returns {Promise} Signals when all events have been decrypted
*/
Room.prototype.decryptCriticalEvents = function() {
const readReceiptEventId = this.getEventReadUpTo(this._client.getUserId(), true);
const events = this.getLiveTimeline().getEvents();
const readReceiptTimelineIndex = events.findIndex(matrixEvent => {
return matrixEvent.event.event_id === readReceiptEventId;
});
const decryptionPromises = events
.slice(readReceiptTimelineIndex)
.filter(event => event.shouldAttemptDecryption())
.reverse()
.map(event => event.attemptDecryption(this._client._crypto, { isRetry: true }));
return Promise.allSettled(decryptionPromises);
};
/**
* Bulk decrypt events in a room
*
* @returns {Promise} Signals when all events have been decrypted
*/
Room.prototype.decryptAllEvents = function() {
const decryptionPromises = this
.getUnfilteredTimelineSet()
.getLiveTimeline()
.getEvents()
.filter(event => event.shouldAttemptDecryption())
.reverse()
.map(event => event.attemptDecryption(this._client._crypto, { isRetry: true }));
return Promise.allSettled(decryptionPromises);
};
/**
* Gets the version of the room
* @returns {string} The version of the room, or null if it could not be determined
@@ -235,7 +280,7 @@ utils.inherits(Room, EventEmitter);
Room.prototype.getVersion = function() {
const createEvent = this.currentState.getStateEvents("m.room.create", "");
if (!createEvent) {
logger.warn("Room " + this.roomId + " does not have an m.room.create event");
logger.warn("[getVersion] Room " + this.roomId + " does not have an m.room.create event");
return '1';
}
const ver = createEvent.getContent()['room_version'];
@@ -904,7 +949,7 @@ Room.prototype.getAliases = function() {
if (aliasEvents) {
for (let i = 0; i < aliasEvents.length; ++i) {
const aliasEvent = aliasEvents[i];
if (utils.isArray(aliasEvent.getContent().aliases)) {
if (Array.isArray(aliasEvent.getContent().aliases)) {
const filteredAliases = aliasEvent.getContent().aliases.filter(a => {
if (typeof(a) !== "string") return false;
if (a[0] !== '#') return false;
@@ -982,6 +1027,15 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
return this.currentState.getMember(userId);
};
/**
* Get all currently loaded members from the current
* room state.
* @returns {RoomMember[]} Room members
*/
Room.prototype.getMembers = function() {
return this.currentState.getMembers();
};
/**
* Get a list of members whose membership state is "join".
* @return {RoomMember[]} A list of currently joined members.
@@ -1023,7 +1077,7 @@ Room.prototype.getInvitedAndJoinedMemberCount = function() {
* @return {RoomMember[]} A list of members with the given membership state.
*/
Room.prototype.getMembersWithMembership = function(membership) {
return utils.filter(this.currentState.getMembers(), function(m) {
return this.currentState.getMembers().filter(function(m) {
return m.membership === membership;
});
};
@@ -1646,7 +1700,7 @@ Room.prototype.recalculate = function() {
);
if (membershipEvent && membershipEvent.getContent().membership === "invite") {
const strippedStateEvents = membershipEvent.event.invite_room_state || [];
utils.forEach(strippedStateEvents, function(strippedEvent) {
strippedStateEvents.forEach(function(strippedEvent) {
const existingEvent = self.currentState.getStateEvents(
strippedEvent.type, strippedEvent.state_key,
);
@@ -1796,9 +1850,9 @@ Room.prototype.addReceipt = function(event, fake) {
*/
Room.prototype._addReceiptsToStructure = function(event, receipts) {
const self = this;
utils.keys(event.getContent()).forEach(function(eventId) {
utils.keys(event.getContent()[eventId]).forEach(function(receiptType) {
utils.keys(event.getContent()[eventId][receiptType]).forEach(
Object.keys(event.getContent()).forEach(function(eventId) {
Object.keys(event.getContent()[eventId]).forEach(function(receiptType) {
Object.keys(event.getContent()[eventId][receiptType]).forEach(
function(userId) {
const receipt = event.getContent()[eventId][receiptType][userId];
@@ -1838,8 +1892,8 @@ Room.prototype._addReceiptsToStructure = function(event, receipts) {
*/
Room.prototype._buildReceiptCache = function(receipts) {
const receiptCacheByEventId = {};
utils.keys(receipts).forEach(function(receiptType) {
utils.keys(receipts[receiptType]).forEach(function(userId) {
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] = [];
@@ -1954,7 +2008,7 @@ Room.prototype.getJoinRule = function() {
Room.prototype.getType = function() {
const createEvent = this.currentState.getStateEvents("m.room.create", "");
if (!createEvent) {
logger.warn("Room " + this.roomId + " does not have an m.room.create event");
logger.warn("[getType] Room " + this.roomId + " does not have an m.room.create event");
return undefined;
}
return createEvent.getContent()[RoomCreateTypeField];
+2 -3
View File
@@ -19,7 +19,6 @@ limitations under the License.
* @module models/search-result
*/
import * as utils from "../utils";
import {EventContext} from "./event-context";
/**
@@ -52,8 +51,8 @@ SearchResult.fromJson = function(jsonObj, eventMapper) {
const context = new EventContext(eventMapper(jsonObj.result));
context.setPaginateToken(jsonContext.start, true);
context.addEvents(utils.map(events_before, eventMapper), true);
context.addEvents(utils.map(events_after, eventMapper), false);
context.addEvents(events_before.map(eventMapper), true);
context.addEvents(events_after.map(eventMapper), false);
context.setPaginateToken(jsonContext.end, false);
return new SearchResult(jsonObj.rank, context);
+15 -13
View File
@@ -65,7 +65,7 @@ MatrixScheduler.prototype.getQueueForEvent = function(event) {
if (!name || !this._queues[name]) {
return null;
}
return utils.map(this._queues[name], function(obj) {
return this._queues[name].map(function(obj) {
return obj.event;
});
};
@@ -196,16 +196,18 @@ function _startProcessingQueues(scheduler) {
return;
}
// for each inactive queue with events in them
utils.forEach(utils.filter(utils.keys(scheduler._queues), function(queueName) {
return scheduler._activeQueues.indexOf(queueName) === -1 &&
scheduler._queues[queueName].length > 0;
}), function(queueName) {
// mark the queue as active
scheduler._activeQueues.push(queueName);
// begin processing the head of the queue
debuglog("Spinning up queue: '%s'", queueName);
_processQueue(scheduler, queueName);
});
Object.keys(scheduler._queues)
.filter(function(queueName) {
return scheduler._activeQueues.indexOf(queueName) === -1 &&
scheduler._queues[queueName].length > 0;
})
.forEach(function(queueName) {
// mark the queue as active
scheduler._activeQueues.push(queueName);
// begin processing the head of the queue
debuglog("Spinning up queue: '%s'", queueName);
_processQueue(scheduler, queueName);
});
}
function _processQueue(scheduler, queueName) {
@@ -267,7 +269,7 @@ function _processQueue(scheduler, queueName) {
function _peekNextEvent(scheduler, queueName) {
const queue = scheduler._queues[queueName];
if (!utils.isArray(queue)) {
if (!Array.isArray(queue)) {
return null;
}
return queue[0];
@@ -275,7 +277,7 @@ function _peekNextEvent(scheduler, queueName) {
function _removeNextEvent(scheduler, queueName) {
const queue = scheduler._queues[queueName];
if (!utils.isArray(queue)) {
if (!Array.isArray(queue)) {
return null;
}
return queue.shift();
+4 -5
View File
@@ -23,7 +23,6 @@ limitations under the License.
*/
import {User} from "../models/user";
import * as utils from "../utils";
function isValidFilterId(filterId) {
const isValidStr = typeof filterId === "string" &&
@@ -113,7 +112,7 @@ MemoryStore.prototype = {
* @return {Group[]} A list of groups, which may be empty.
*/
getGroups: function() {
return utils.values(this.groups);
return Object.values(this.groups);
},
/**
@@ -175,7 +174,7 @@ MemoryStore.prototype = {
* @return {Room[]} A list of rooms, which may be empty.
*/
getRooms: function() {
return utils.values(this.rooms);
return Object.values(this.rooms);
},
/**
@@ -194,7 +193,7 @@ MemoryStore.prototype = {
* @return {RoomSummary[]} A summary of each room.
*/
getRoomSummaries: function() {
return utils.map(utils.values(this.rooms), function(room) {
return Object.values(this.rooms).map(function(room) {
return room.summary;
});
},
@@ -221,7 +220,7 @@ MemoryStore.prototype = {
* @return {User[]} A list of users, which may be empty.
*/
getUsers: function() {
return utils.values(this.users);
return Object.values(this.users);
},
/**
+24 -19
View File
@@ -278,20 +278,15 @@ SyncApi.prototype.peek = function(roomId) {
// FIXME: Mostly duplicated from _processRoomEvents but not entirely
// because "state" in this API is at the BEGINNING of the chunk
const oldStateEvents = utils.map(
utils.deepCopy(response.state), client.getEventMapper(),
);
const stateEvents = utils.map(
response.state, client.getEventMapper(),
);
const messages = utils.map(
response.messages.chunk, client.getEventMapper(),
);
const oldStateEvents = utils.deepCopy(response.state)
.map(client.getEventMapper());
const stateEvents = response.state.map(client.getEventMapper());
const messages = response.messages.chunk.map(client.getEventMapper());
// XXX: copypasted from /sync until we kill off this
// minging v1 API stuff)
// handle presence events (User objects)
if (response.presence && utils.isArray(response.presence)) {
if (response.presence && Array.isArray(response.presence)) {
response.presence.map(client.getEventMapper()).forEach(
function(presenceEvent) {
let user = client.store.getUser(presenceEvent.getContent().user_id);
@@ -1006,7 +1001,7 @@ SyncApi.prototype._processSyncResponse = async function(
// - The isBrandNewRoom boilerplate is boilerplatey.
// handle presence events (User objects)
if (data.presence && utils.isArray(data.presence.events)) {
if (data.presence && Array.isArray(data.presence.events)) {
data.presence.events.map(client.getEventMapper()).forEach(
function(presenceEvent) {
let user = client.store.getUser(presenceEvent.getSender());
@@ -1022,7 +1017,7 @@ SyncApi.prototype._processSyncResponse = async function(
}
// handle non-room account_data
if (data.account_data && utils.isArray(data.account_data.events)) {
if (data.account_data && Array.isArray(data.account_data.events)) {
const events = data.account_data.events.map(client.getEventMapper());
const prevEventsMap = events.reduce((m, c) => {
m[c.getId()] = client.store.getAccountData(c.getType());
@@ -1047,7 +1042,7 @@ SyncApi.prototype._processSyncResponse = async function(
}
// handle to-device events
if (data.to_device && utils.isArray(data.to_device.events) &&
if (data.to_device && Array.isArray(data.to_device.events) &&
data.to_device.events.length > 0
) {
const cancelledKeyVerificationTxns = [];
@@ -1158,10 +1153,15 @@ SyncApi.prototype._processSyncResponse = async function(
await utils.promiseMapSeries(joinRooms, async function(joinObj) {
const room = joinObj.room;
const stateEvents = self._mapSyncEventsFormat(joinObj.state, room);
const timelineEvents = self._mapSyncEventsFormat(joinObj.timeline, room);
// Prevent events from being decrypted ahead of time
// this helps large account to speed up faster
// room::decryptCriticalEvent is in charge of decrypting all the events
// required for a client to function properly
const timelineEvents = self._mapSyncEventsFormat(joinObj.timeline, room, false);
const ephemeralEvents = self._mapSyncEventsFormat(joinObj.ephemeral);
const accountDataEvents = self._mapSyncEventsFormat(joinObj.account_data);
const encrypted = client.isRoomEncrypted(room.roomId);
// we do this first so it's correct when any of the events fire
if (joinObj.unread_notifications) {
room.setUnreadNotificationCount(
@@ -1172,7 +1172,6 @@ SyncApi.prototype._processSyncResponse = async function(
// bother setting it here. We trust our calculations better than the
// server's for this case, and therefore will assume that our non-zero
// count is accurate.
const encrypted = client.isRoomEncrypted(room.roomId);
if (!encrypted
|| (encrypted && room.getUnreadNotificationCount('highlight') <= 0)) {
room.setUnreadNotificationCount(
@@ -1294,6 +1293,11 @@ SyncApi.prototype._processSyncResponse = async function(
});
room.updateMyMembership("join");
// Decrypt only the last message in all rooms to make sure we can generate a preview
// And decrypt all events after the recorded read receipt to ensure an accurate
// notification count
room.decryptCriticalEvents();
});
// Handle leaves (e.g. kicked rooms)
@@ -1499,7 +1503,7 @@ SyncApi.prototype._mapSyncResponseToRoomArray = function(obj) {
// [{stuff+Room+isBrandNewRoom}, {stuff+Room+isBrandNewRoom}]
const client = this.client;
const self = this;
return utils.keys(obj).map(function(roomId) {
return Object.keys(obj).map(function(roomId) {
const arrObj = obj[roomId];
let room = client.store.getRoom(roomId);
let isBrandNewRoom = false;
@@ -1516,13 +1520,14 @@ SyncApi.prototype._mapSyncResponseToRoomArray = function(obj) {
/**
* @param {Object} obj
* @param {Room} room
* @param {bool} decrypt
* @return {MatrixEvent[]}
*/
SyncApi.prototype._mapSyncEventsFormat = function(obj, room) {
if (!obj || !utils.isArray(obj.events)) {
SyncApi.prototype._mapSyncEventsFormat = function(obj, room, decrypt = true) {
if (!obj || !Array.isArray(obj.events)) {
return [];
}
const mapper = this.client.getEventMapper();
const mapper = this.client.getEventMapper({ decrypt });
return obj.events.map(function(e) {
if (room) {
e.room_id = room.roomId;
-321
View File
@@ -61,116 +61,6 @@ export function encodeUri(pathTemplate: string,
return pathTemplate;
}
/**
* Applies a map function to the given array.
* @param {Array} array The array to apply the function to.
* @param {Function} fn The function that will be invoked for each element in
* the array with the signature <code>fn(element){...}</code>
* @return {Array} A new array with the results of the function.
*/
export function map<T, S>(array: T[], fn: (t: T) => S): S[] {
const results = new Array(array.length);
for (let i = 0; i < array.length; i++) {
results[i] = fn(array[i]);
}
return results;
}
/**
* Applies a filter function to the given array.
* @param {Array} array The array to apply the function to.
* @param {Function} fn The function that will be invoked for each element in
* the array. It should return true to keep the element. The function signature
* looks like <code>fn(element, index, array){...}</code>.
* @return {Array} A new array with the results of the function.
*/
export function filter<T>(array: T[],
fn: (t: T, i?: number, a?: T[]) => boolean): T[] {
const results: T[] = [];
for (let i = 0; i < array.length; i++) {
if (fn(array[i], i, array)) {
results.push(array[i]);
}
}
return results;
}
/**
* Get the keys for an object. Same as <code>Object.keys()</code>.
* @param {Object} obj The object to get the keys for.
* @return {string[]} The keys of the object.
*/
export function keys(obj: object): string[] {
const result = [];
for (const key in obj) {
if (!obj.hasOwnProperty(key)) {
continue;
}
result.push(key);
}
return result;
}
/**
* Get the values for an object.
* @param {Object} obj The object to get the values for.
* @return {Array<*>} The values of the object.
*/
export function values<T>(obj: Record<string, T>): T[] {
const result = [];
for (const key in obj) {
if (!obj.hasOwnProperty(key)) {
continue;
}
result.push(obj[key]);
}
return result;
}
/**
* Invoke a function for each item in the array.
* @param {Array} array The array.
* @param {Function} fn The function to invoke for each element. Has the
* function signature <code>fn(element, index)</code>.
*/
export function forEach<T>(array: T[], fn: (t: T, i: number) => void) {
for (let i = 0; i < array.length; i++) {
fn(array[i], i);
}
}
/**
* The findElement() method returns a value in the array, if an element in the array
* satisfies (returns true) the provided testing function. Otherwise undefined
* is returned.
* @param {Array} array The array.
* @param {Function} fn Function to execute on each value in the array, with the
* function signature <code>fn(element, index, array)</code>
* @param {boolean} reverse True to search in reverse order.
* @return {*} The first value in the array which returns <code>true</code> for
* the given function.
*/
export function findElement<T>(
array: T[],
fn: (t: T, i?: number, a?: T[]) => boolean,
reverse?: boolean,
) {
let i;
if (reverse) {
for (i = array.length - 1; i >= 0; i--) {
if (fn(array[i], i, array)) {
return array[i];
}
}
} else {
for (i = 0; i < array.length; i++) {
if (fn(array[i], i, array)) {
return array[i];
}
}
}
}
/**
* The removeElement() method removes the first element in the array that
* satisfies (returns true) the provided testing function.
@@ -217,16 +107,6 @@ export function isFunction(value: any) {
return Object.prototype.toString.call(value) === "[object Function]";
}
/**
* Checks if the given thing is an array.
* @param {*} value The thing to check.
* @return {boolean} True if it is an array.
*/
export function isArray(value: any) {
return Array.isArray ? Array.isArray(value) :
Boolean(value && value.constructor === Array);
}
/**
* Checks that the given object has the specified keys.
* @param {Object} obj The object to check.
@@ -380,207 +260,6 @@ export function extend(...restParams) {
return target;
}
/**
* Run polyfills to add Array.map and Array.filter if they are missing.
*/
export function runPolyfills() {
// Array.prototype.filter
// ========================================================
// SOURCE:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
if (!Array.prototype.filter) {
// eslint-disable-next-line no-extend-native
Array.prototype.filter = function(fun: Function/*, thisArg*/, ...restProps) {
if (this === void 0 || this === null) {
throw new TypeError();
}
const t = Object(this);
const len = t.length >>> 0;
if (typeof fun !== 'function') {
throw new TypeError();
}
const res = [];
const thisArg = restProps ? restProps[0] : void 0;
for (let i = 0; i < len; i++) {
if (i in t) {
const val = t[i];
// NOTE: Technically this should Object.defineProperty at
// the next index, as push can be affected by
// properties on Object.prototype and Array.prototype.
// But that method's new, and collisions should be
// rare, so use the more-compatible alternative.
if (fun.call(thisArg, val, i, t)) {
res.push(val);
}
}
}
return res;
};
}
// Array.prototype.map
// ========================================================
// SOURCE:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map
// Production steps of ECMA-262, Edition 5, 15.4.4.19
// Reference: http://es5.github.io/#x15.4.4.19
if (!Array.prototype.map) {
// eslint-disable-next-line no-extend-native
Array.prototype.map = function(callback, thisArg) {
let T;
let k;
if (this === null || this === undefined) {
throw new TypeError(' this is null or not defined');
}
// 1. Let O be the result of calling ToObject passing the |this|
// value as the argument.
const O = Object(this);
// 2. Let lenValue be the result of calling the Get internal
// method of O with the argument "length".
// 3. Let len be ToUint32(lenValue).
const len = O.length >>> 0;
// 4. If IsCallable(callback) is false, throw a TypeError exception.
// See: http://es5.github.com/#x9.11
if (typeof callback !== 'function') {
throw new TypeError(callback + ' is not a function');
}
// 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
if (arguments.length > 1) {
T = thisArg;
}
// 6. Let A be a new array created as if by the expression new Array(len)
// where Array is the standard built-in constructor with that name and
// len is the value of len.
const A = new Array(len);
// 7. Let k be 0
k = 0;
// 8. Repeat, while k < len
while (k < len) {
let kValue;
let mappedValue;
// a. Let Pk be ToString(k).
// This is implicit for LHS operands of the in operator
// b. Let kPresent be the result of calling the HasProperty internal
// method of O with argument Pk.
// This step can be combined with c
// c. If kPresent is true, then
if (k in O) {
// i. Let kValue be the result of calling the Get internal
// method of O with argument Pk.
kValue = O[k];
// ii. Let mappedValue be the result of calling the Call internal
// method of callback with T as the this value and argument
// list containing kValue, k, and O.
mappedValue = callback.call(T, kValue, k, O);
// iii. Call the DefineOwnProperty internal method of A with arguments
// Pk, Property Descriptor
// { Value: mappedValue,
// Writable: true,
// Enumerable: true,
// Configurable: true },
// and false.
// In browsers that support Object.defineProperty, use the following:
// Object.defineProperty(A, k, {
// value: mappedValue,
// writable: true,
// enumerable: true,
// configurable: true
// });
// For best browser support, use the following:
A[k] = mappedValue;
}
// d. Increase k by 1.
k++;
}
// 9. return A
return A;
};
}
// Array.prototype.forEach
// ========================================================
// SOURCE:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
// Production steps of ECMA-262, Edition 5, 15.4.4.18
// Reference: http://es5.github.io/#x15.4.4.18
if (!Array.prototype.forEach) {
// eslint-disable-next-line no-extend-native
Array.prototype.forEach = function(callback, thisArg) {
let T;
let k;
if (this === null || this === undefined) {
throw new TypeError(' this is null or not defined');
}
// 1. Let O be the result of calling ToObject passing the |this| value as the
// argument.
const O = Object(this);
// 2. Let lenValue be the result of calling the Get internal method of O with the
// argument "length".
// 3. Let len be ToUint32(lenValue).
const len = O.length >>> 0;
// 4. If IsCallable(callback) is false, throw a TypeError exception.
// See: http://es5.github.com/#x9.11
if (typeof callback !== "function") {
throw new TypeError(callback + ' is not a function');
}
// 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
if (arguments.length > 1) {
T = thisArg;
}
// 6. Let k be 0
k = 0;
// 7. Repeat, while k < len
while (k < len) {
let kValue;
// a. Let Pk be ToString(k).
// This is implicit for LHS operands of the in operator
// b. Let kPresent be the result of calling the HasProperty internal
// method of O with
// argument Pk.
// This step can be combined with c
// c. If kPresent is true, then
if (k in O) {
// i. Let kValue be the result of calling the Get internal method of O with
// argument Pk
kValue = O[k];
// ii. Call the Call internal method of callback with T as the this value and
// argument list containing kValue, k, and O.
callback.call(T, kValue, k, O);
}
// d. Increase k by 1.
k++;
}
// 8. return undefined
};
}
}
/**
* Inherit the prototype methods from one constructor into another. This is a
* port of the Node.js implementation with an Object.create polyfill.
+104 -240
View File
@@ -28,7 +28,15 @@ import MatrixEvent from '../models/event';
import {EventType} from '../@types/event';
import { RoomMember } from '../models/room-member';
import { randomString } from '../randomstring';
import { MCallReplacesEvent, MCallAnswer, MCallOfferNegotiate, CallCapabilities } from './callEventTypes';
import {
MCallReplacesEvent,
MCallAnswer,
MCallOfferNegotiate,
CallCapabilities,
SDPStreamMetadataPurpose,
} from './callEventTypes';
import { CallFeed } from './callFeed';
// events: hangup, error(err), replaced(call), state(state, oldState)
@@ -62,6 +70,11 @@ interface TurnServer {
ttl?: number,
}
interface AssertedIdentity {
id: string,
displayName: string,
}
export enum CallState {
Fledgling = 'fledgling',
InviteSent = 'invite_sent',
@@ -101,6 +114,10 @@ export enum CallEvent {
RemoteHoldUnhold = 'remote_hold_unhold',
// backwards compat alias for LocalHoldUnhold: remove in a major version bump
HoldUnhold = 'hold_unhold',
// Feeds have changed
FeedsChanged = 'feeds_changed',
AssertedIdentityChanged = 'asserted_identity_changed',
}
export enum CallErrorCode {
@@ -248,11 +265,8 @@ export class MatrixCall extends EventEmitter {
private candidateSendTries: number;
private sentEndOfCandidates: boolean;
private peerConn: RTCPeerConnection;
private localVideoElement: HTMLVideoElement;
private remoteVideoElement: HTMLVideoElement;
private remoteAudioElement: HTMLAudioElement;
private feeds: Array<CallFeed>;
private screenSharingStream: MediaStream;
private remoteStream: MediaStream;
private localAVStream: MediaStream;
private inviteOrAnswerSent: boolean;
private waitForLocalAVStream: boolean;
@@ -292,6 +306,8 @@ export class MatrixCall extends EventEmitter {
// the call) we buffer them up here so we can then add the ones from the party we pick
private remoteCandidateBuffer = new Map<string, RTCIceCandidate[]>();
private remoteAssertedIdentity: AssertedIdentity;
constructor(opts: CallOpts) {
super();
this.roomId = opts.roomId;
@@ -327,6 +343,8 @@ export class MatrixCall extends EventEmitter {
this.unholdingRemote = false;
this.micMuted = false;
this.vidMuted = false;
this.feeds = [];
}
/**
@@ -343,17 +361,11 @@ export class MatrixCall extends EventEmitter {
/**
* Place a video call to this room.
* @param {Element} remoteVideoElement a <code>&lt;video&gt;</code> DOM element
* to render video to.
* @param {Element} localVideoElement a <code>&lt;video&gt;</code> DOM element
* to render the local camera preview.
* @throws If you have not specified a listener for 'error' events.
*/
async placeVideoCall(remoteVideoElement: HTMLVideoElement, localVideoElement: HTMLVideoElement) {
async placeVideoCall() {
logger.debug("placeVideoCall");
this.checkForErrorListener();
this.localVideoElement = localVideoElement;
this.remoteVideoElement = remoteVideoElement;
const constraints = getUserMediaContraints(ConstraintsType.Video);
this.type = CallType.Video;
await this.placeCallWithConstraints(constraints);
@@ -363,22 +375,11 @@ export class MatrixCall extends EventEmitter {
* Place a screen-sharing call to this room. This includes audio.
* <b>This method is EXPERIMENTAL and subject to change without warning. It
* only works in Google Chrome and Firefox >= 44.</b>
* @param {Element} remoteVideoElement a <code>&lt;video&gt;</code> DOM element
* to render video to.
* @param {Element} localVideoElement a <code>&lt;video&gt;</code> DOM element
* to render the local camera preview.
* @throws If you have not specified a listener for 'error' events.
*/
async placeScreenSharingCall(
remoteVideoElement: HTMLVideoElement,
localVideoElement: HTMLVideoElement,
selectDesktopCapturerSource?: () => Promise<DesktopCapturerSource>,
) {
async placeScreenSharingCall(selectDesktopCapturerSource?: () => Promise<DesktopCapturerSource>) {
logger.debug("placeScreenSharingCall");
this.checkForErrorListener();
this.localVideoElement = localVideoElement;
this.remoteVideoElement = remoteVideoElement;
try {
const screenshareConstraints = await getScreenshareContraints(selectDesktopCapturerSource);
if (!screenshareConstraints) {
@@ -419,86 +420,58 @@ export class MatrixCall extends EventEmitter {
return Boolean(this.opponentCaps && this.opponentCaps["m.call.transferee"]);
}
/**
* Retrieve the local <code>&lt;video&gt;</code> DOM element.
* @return {Element} The dom element
*/
public getLocalVideoElement(): HTMLVideoElement {
return this.localVideoElement;
public getRemoteAssertedIdentity(): AssertedIdentity {
return this.remoteAssertedIdentity;
}
/**
* Retrieve the remote <code>&lt;video&gt;</code> DOM element
* used for playing back video capable streams.
* @return {Element} The dom element
* Returns an array of all CallFeeds
* @returns {Array<CallFeed>} CallFeeds
*/
public getRemoteVideoElement(): HTMLVideoElement {
return this.remoteVideoElement;
public getFeeds(): Array<CallFeed> {
return this.feeds;
}
/**
* Retrieve the remote <code>&lt;audio&gt;</code> DOM element
* used for playing back audio only streams.
* @return {Element} The dom element
* Returns an array of all local CallFeeds
* @returns {Array<CallFeed>} local CallFeeds
*/
public getRemoteAudioElement(): HTMLAudioElement {
return this.remoteAudioElement;
public getLocalFeeds(): Array<CallFeed> {
return this.feeds.filter((feed) => {return feed.isLocal()});
}
/**
* Set the local <code>&lt;video&gt;</code> DOM element. If this call is active,
* video will be rendered to it immediately.
* @param {Element} element The <code>&lt;video&gt;</code> DOM element.
* Returns an array of all remote CallFeeds
* @returns {Array<CallFeed>} remote CallFeeds
*/
public async setLocalVideoElement(element: HTMLVideoElement) {
this.localVideoElement = element;
public getRemoteFeeds(): Array<CallFeed> {
return this.feeds.filter((feed) => {return !feed.isLocal()});
}
if (element && this.localAVStream && this.type === CallType.Video) {
element.autoplay = true;
/**
* Returns true if there are no incoming feeds,
* otherwise returns false
* @returns {boolean} no incoming feeds
*/
public noIncomingFeeds(): boolean {
return !this.feeds.some((feed) => !feed.isLocal());
}
element.srcObject = this.localAVStream;
element.muted = true;
try {
await element.play();
} catch (e) {
logger.info("Failed to play local video element", e);
}
private pushNewFeed(stream: MediaStream, userId: string, purpose: SDPStreamMetadataPurpose) {
// Try to find a feed with the same stream id as the new stream,
// if we find it replace the old stream with the new one
const feed = this.feeds.find((feed) => feed.stream.id === stream.id);
if (feed) {
feed.setNewStream(stream);
} else {
this.feeds.push(new CallFeed(stream, userId, purpose, this.client, this.roomId));
this.emit(CallEvent.FeedsChanged, this.feeds);
}
}
/**
* Set the remote <code>&lt;video&gt;</code> DOM element. If this call is active,
* the first received video-capable stream will be rendered to it immediately.
* @param {Element} element The <code>&lt;video&gt;</code> DOM element.
*/
public setRemoteVideoElement(element: HTMLVideoElement) {
if (element === this.remoteVideoElement) return;
element.autoplay = true;
// if we already have an audio element set, use that instead and mute the audio
// on this video element.
if (this.remoteAudioElement) element.muted = true;
this.remoteVideoElement = element;
if (this.remoteStream) {
this.playRemoteVideo();
}
}
/**
* Set the remote <code>&lt;audio&gt;</code> DOM element. If this call is active,
* the first received audio-only stream will be rendered to it immediately.
* The audio will *not* be rendered from the remoteVideoElement.
* @param {Element} element The <code>&lt;video&gt;</code> DOM element.
*/
public async setRemoteAudioElement(element: HTMLAudioElement) {
if (element === this.remoteAudioElement) return;
this.remoteAudioElement = element;
if (this.remoteStream) this.playRemoteAudio();
private deleteAllFeeds() {
this.feeds = [];
this.emit(CallEvent.FeedsChanged, this.feeds);
}
// The typescript definitions have this type as 'any' :(
@@ -553,16 +526,18 @@ export class MatrixCall extends EventEmitter {
return;
}
const remoteStream = this.feeds.find((feed) => {return !feed.isLocal()})?.stream;
// According to previous comments in this file, firefox at some point did not
// add streams until media started ariving on them. Testing latest firefox
// (81 at time of writing), this is no longer a problem, so let's do it the correct way.
if (!this.remoteStream || this.remoteStream.getTracks().length === 0) {
if (!remoteStream || remoteStream.getTracks().length === 0) {
logger.error("No remote stream or no tracks after setting remote description!");
this.terminate(CallParty.Local, CallErrorCode.SetRemoteDescription, false);
return;
}
this.type = this.remoteStream.getTracks().some(t => t.kind === 'video') ? CallType.Video : CallType.Voice;
this.type = remoteStream.getTracks().some(t => t.kind === 'video') ? CallType.Video : CallType.Voice;
this.setState(CallState.Ringing);
@@ -647,9 +622,6 @@ export class MatrixCall extends EventEmitter {
newCall.gotUserMediaForAnswer(this.localAVStream);
delete(this.localAVStream);
}
newCall.localVideoElement = this.localVideoElement;
newCall.remoteVideoElement = this.remoteVideoElement;
newCall.remoteAudioElement = this.remoteAudioElement;
this.successor = newCall;
this.emit(CallEvent.Replaced, newCall);
this.hangup(CallErrorCode.Replaced, true);
@@ -761,10 +733,6 @@ export class MatrixCall extends EventEmitter {
}
this.updateMuteStatus();
if (!onHold) {
this.playRemoteAudio();
}
this.emit(CallEvent.RemoteHoldUnhold, this.remoteOnHold);
}
@@ -818,16 +786,6 @@ export class MatrixCall extends EventEmitter {
const vidShouldBeMuted = this.vidMuted || this.remoteOnHold;
setTracksEnabled(this.localAVStream.getVideoTracks(), !vidShouldBeMuted);
if (this.remoteOnHold) {
if (this.remoteAudioElement && this.remoteAudioElement.srcObject === this.remoteStream) {
this.remoteAudioElement.muted = true;
} else if (this.remoteVideoElement && this.remoteVideoElement.srcObject === this.remoteStream) {
this.remoteVideoElement.muted = true;
}
} else {
this.playRemoteAudio();
}
}
/**
@@ -850,24 +808,13 @@ export class MatrixCall extends EventEmitter {
logger.debug("gotUserMediaForInvite -> " + this.type);
const videoEl = this.getLocalVideoElement();
if (videoEl && this.type === CallType.Video) {
videoEl.autoplay = true;
if (this.screenSharingStream) {
logger.debug(
"Setting screen sharing stream to the local video element",
);
videoEl.srcObject = this.screenSharingStream;
} else {
videoEl.srcObject = stream;
}
videoEl.muted = true;
try {
await videoEl.play();
} catch (e) {
logger.info("Failed to play local video element", e);
}
if (this.screenSharingStream) {
logger.debug(
"Setting screen sharing stream to the local video element",
);
this.pushNewFeed(this.screenSharingStream, this.client.getUserId(), SDPStreamMetadataPurpose.Screenshare);
} else {
this.pushNewFeed(stream, this.client.getUserId(), SDPStreamMetadataPurpose.Usermedia);
}
// why do we enable audio (and only audio) tracks here? -- matthew
@@ -937,19 +884,7 @@ export class MatrixCall extends EventEmitter {
return;
}
const localVidEl = this.getLocalVideoElement();
if (localVidEl && this.type === CallType.Video) {
localVidEl.autoplay = true;
localVidEl.srcObject = stream;
localVidEl.muted = true;
try {
await localVidEl.play();
} catch (e) {
logger.info("Failed to play local video element", e);
}
}
this.pushNewFeed(stream, this.client.getUserId(), SDPStreamMetadataPurpose.Usermedia);
this.localAVStream = stream;
logger.info("Got local AV stream with id " + this.localAVStream.id);
@@ -1199,6 +1134,16 @@ export class MatrixCall extends EventEmitter {
}
}
async onAssertedIdentityReceived(event: MatrixEvent) {
if (!event.getContent().asserted_identity) return;
this.remoteAssertedIdentity = {
id: event.getContent().asserted_identity.id,
displayName: event.getContent().asserted_identity.display_name,
};
this.emit(CallEvent.AssertedIdentityChanged);
}
private callHasEnded(): boolean {
// This exists as workaround to typescript trying to be clever and erroring
// when putting if (this.state === CallState.Ended) return; twice in the same
@@ -1354,32 +1299,31 @@ export class MatrixCall extends EventEmitter {
logger.warn(`Streamless ${ev.track.kind} found: ignoring.`);
return;
}
const oldRemoteStream = this.feeds.find((feed) => {return !feed.isLocal()})?.stream;
// If we already have a stream, check this track is from the same one
if (this.remoteStream && ev.streams[0].id !== this.remoteStream.id) {
// Note that we check by ID and always set the remote stream: Chrome appears
// to make new stream objects when tranciever directionality is changed and the 'active'
// status of streams change - Dave
if (oldRemoteStream && ev.streams[0].id !== oldRemoteStream.id) {
logger.warn(
`Ignoring new stream ID ${ev.streams[0].id}: we already have stream ID ${this.remoteStream.id}`,
`Ignoring new stream ID ${ev.streams[0].id}: we already have stream ID ${oldRemoteStream.id}`,
);
return;
}
if (!this.remoteStream) {
if (!oldRemoteStream) {
logger.info("Got remote stream with id " + ev.streams[0].id);
}
// Note that we check by ID above and always set the remote stream: Chrome appears
// to make new stream objects when tranciever directionality is changed and the 'active'
// status of streams change
this.remoteStream = ev.streams[0];
const newRemoteStream = ev.streams[0];
logger.debug(`Track id ${ev.track.id} of kind ${ev.track.kind} added`);
if (ev.track.kind === 'video') {
if (this.remoteVideoElement) {
this.playRemoteVideo();
}
} else {
if (this.remoteAudioElement) this.playRemoteAudio();
}
this.pushNewFeed(newRemoteStream, this.getOpponentMember().userId, SDPStreamMetadataPurpose.Usermedia)
logger.info("playing remote. stream active? " + newRemoteStream.active);
};
onNegotiationNeeded = async () => {
@@ -1402,52 +1346,6 @@ export class MatrixCall extends EventEmitter {
}
};
async playRemoteAudio() {
if (this.remoteVideoElement) this.remoteVideoElement.muted = true;
this.remoteAudioElement.muted = false;
this.remoteAudioElement.srcObject = this.remoteStream;
// if audioOutput is non-default:
try {
if (audioOutput) {
// This seems quite unreliable in Chrome, although I haven't yet managed to make a jsfiddle where
// it fails.
// It seems reliable if you set the sink ID after setting the srcObject and then set the sink ID
// back to the default after the call is over
logger.info("Setting audio sink to " + audioOutput + ", was " + this.remoteAudioElement.sinkId);
await this.remoteAudioElement.setSinkId(audioOutput);
}
} catch (e) {
logger.warn("Couldn't set requested audio output device: using default", e);
}
try {
await this.remoteAudioElement.play();
} catch (e) {
logger.error("Failed to play remote audio element", e);
}
}
private async playRemoteVideo() {
// A note on calling methods on media elements:
// We used to have queues per media element to serialise all calls on those elements.
// The reason given for this was that load() and play() were racing. However, we now
// never call load() explicitly so this seems unnecessary. However, serialising every
// operation was causing bugs where video would not resume because some play command
// had got stuck and all media operations were queued up behind it. If necessary, we
// should serialise the ones that need to be serialised but then be able to interrupt
// them with another load() which will cancel the pending one, but since we don't call
// load() explicitly, it shouldn't be a problem.
this.remoteVideoElement.srcObject = this.remoteStream;
logger.info("playing remote video. stream active? " + this.remoteStream.active);
try {
await this.remoteVideoElement.play();
} catch (e) {
logger.info("Failed to play remote video element", e);
}
}
onHangupReceived = (msg) => {
logger.debug("Hangup received for call ID " + this.callId);
@@ -1607,33 +1505,14 @@ export class MatrixCall extends EventEmitter {
this.inviteTimeout = null;
}
const remoteVid = this.getRemoteVideoElement();
const remoteAud = this.getRemoteAudioElement();
const localVid = this.getLocalVideoElement();
// Order is important here: first we stopAllMedia() and only then we can deleteAllFeeds()
// We don't stop media if the call was replaced as we want to re-use streams in the successor
if (hangupReason !== CallErrorCode.Replaced) this.stopAllMedia();
this.deleteAllFeeds();
if (remoteVid) {
remoteVid.pause();
remoteVid.srcObject = null;
}
if (remoteAud) {
remoteAud.pause();
remoteAud.srcObject = null;
try {
// As per comment in playRemoteAudio, setting the sink ID back to the default
// once the call is over makes setSinkId work reliably.
await this.remoteAudioElement.setSinkId('')
} catch (e) {
logger.warn("Failed to set sink ID back to default");
}
}
if (localVid) {
localVid.pause();
localVid.srcObject = null;
}
this.hangupParty = hangupParty;
this.hangupReason = hangupReason;
this.setState(CallState.Ended);
this.stopAllMedia();
if (this.peerConn && this.peerConn.signalingState !== 'closed') {
this.peerConn.close();
}
@@ -1644,19 +1523,9 @@ export class MatrixCall extends EventEmitter {
private stopAllMedia() {
logger.debug(`stopAllMedia (stream=${this.localAVStream})`);
if (this.localAVStream) {
for (const track of this.localAVStream.getTracks()) {
track.stop();
}
}
if (this.screenSharingStream) {
for (const track of this.screenSharingStream.getTracks()) {
track.stop();
}
}
if (this.remoteStream) {
for (const track of this.remoteStream.getTracks()) {
for (const feed of this.feeds) {
for (const track of feed.stream.getTracks()) {
track.stop();
}
}
@@ -1887,16 +1756,8 @@ async function getScreenshareContraints(selectDesktopCapturerSource?: () => Prom
}
}
let audioOutput: string;
let audioInput: string;
let videoInput: string;
/**
* Set an audio output device to use for MatrixCalls
* @function
* @param {string=} deviceId the identifier for the device
* undefined treated as unset
*/
export function setAudioOutput(deviceId: string) { audioOutput = deviceId; }
/**
* Set an audio input device to use for MatrixCalls
* @function
@@ -1913,6 +1774,9 @@ export function setAudioInput(deviceId: string) { audioInput = deviceId; }
export function setVideoInput(deviceId: string) { videoInput = deviceId; }
/**
* DEPRECATED
* Use client.createCall()
*
* Create a new Matrix call for the browser.
* @param {MatrixClient} client The client instance to use.
* @param {string} roomId The room the call is in.
+23 -3
View File
@@ -55,7 +55,9 @@ export class CallEventHandler {
private evaluateEventBuffer = () => {
if (this.client.getSyncState() === "SYNCING") {
// don't process any events until they are all decrypted
if (this.callEventBuffer.some((e) => e.isBeingDecrypted())) return;
if (this.callEventBuffer.some((e) => {
return e.isBeingDecrypted() || e.shouldAttemptDecryption()
})) return;
const ignoreCallIds = new Set<String>();
// inspect the buffer and mark all calls which have been answered
@@ -87,13 +89,19 @@ export class CallEventHandler {
private onEvent = (event: MatrixEvent) => {
// any call events or ones that might be once they're decrypted
if (event.getType().indexOf("m.call.") === 0 || event.isBeingDecrypted()) {
const isBeingDecrypted = event.isBeingDecrypted();
const shouldAttemptDecryption = event.shouldAttemptDecryption();
if (
event.getType().indexOf("m.call.") === 0 ||
event.getType().indexOf("org.matrix.call.") === 0
|| isBeingDecrypted || shouldAttemptDecryption
) {
// queue up for processing once all events from this sync have been
// processed (see above).
this.callEventBuffer.push(event);
}
if (event.isBeingDecrypted() || event.isDecryptionFailure()) {
if (event.isDecryptionFailure() || isBeingDecrypted || shouldAttemptDecryption) {
// add an event listener for once the event is decrypted.
event.once("Event.decrypted", () => {
if (event.getType().indexOf("m.call.") === -1) return;
@@ -271,6 +279,18 @@ export class CallEventHandler {
}
call.onNegotiateReceived(event);
} else if (
event.getType() === EventType.CallAssertedIdentity ||
event.getType() === EventType.CallAssertedIdentityPrefix
) {
if (!call) return;
if (event.getContent().party_id === call.ourPartyId) {
// Ignore remote echo (not that we send asserted identity, but still...)
return;
}
call.onAssertedIdentityReceived(event);
}
}
}
+5
View File
@@ -1,6 +1,11 @@
// allow camelcase as these are events type that go onto the wire
/* eslint-disable camelcase */
export enum SDPStreamMetadataPurpose {
Usermedia = "m.usermedia",
Screenshare = "m.screenshare",
}
interface CallOfferAnswer {
type: string;
sdp: string;
+84
View File
@@ -0,0 +1,84 @@
/*
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
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 EventEmitter from "events";
import {SDPStreamMetadataPurpose} from "./callEventTypes";
import MatrixClient from "../client"
import {RoomMember} from "../models/room-member";
export enum CallFeedEvent {
NewStream = "new_stream",
}
export class CallFeed extends EventEmitter {
constructor(
public stream: MediaStream,
public userId: string,
public purpose: SDPStreamMetadataPurpose,
private client: MatrixClient,
private roomId: string,
) {
super()
}
/**
* Returns callRoom member
* @returns member of the callRoom
*/
public getMember(): RoomMember {
const callRoom = this.client.getRoom(this.roomId);
return callRoom.getMember(this.userId);
}
/**
* Returns true if CallFeed is local, otherwise returns false
* @returns {boolean} is local?
*/
public isLocal(): boolean {
return this.userId === this.client.getUserId();
}
// TODO: The two following methods should be later replaced
// by something that will also check if the remote is muted
/**
* Returns true if audio is muted or if there are no audio
* tracks, otherwise returns false
* @returns {boolean} is audio muted?
*/
public isAudioMuted(): boolean {
return this.stream.getAudioTracks().length === 0;
}
/**
* Returns true video is muted or if there are no video
* tracks, otherwise returns false
* @returns {boolean} is video muted?
*/
public isVideoMuted(): boolean {
// We assume only one video track
return this.stream.getVideoTracks().length === 0;
}
/**
* Replaces the current MediaStream with a new one.
* This method should be only used by MatrixCall.
* @param newStream new stream with which to replace the current one
*/
public setNewStream(newStream: MediaStream) {
this.stream = newStream;
this.emit(CallFeedEvent.NewStream, this.stream);
}
}
+3 -3
View File
@@ -3522,9 +3522,9 @@ hmac-drbg@^1.0.1:
minimalistic-crypto-utils "^1.0.1"
hosted-git-info@^2.1.4:
version "2.8.8"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==
version "2.8.9"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
html-encoding-sniffer@^2.0.1:
version "2.0.1"