Compare commits
103 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 65de184d88 | |||
| 52764045ce | |||
| 4b0db6c472 | |||
| 40cd4629db | |||
| 52a893a811 | |||
| 7cc94e1e1e | |||
| 88945a6d6d | |||
| 7203c5aaf0 | |||
| c305058c46 | |||
| 91bbf7d1a8 | |||
| 2d5857f145 | |||
| 91af9a411d | |||
| 9a9ed124f5 | |||
| 4e5442d972 | |||
| b5fa54f91e | |||
| 2246aede4b | |||
| 4b2d409c69 | |||
| 684511ff06 | |||
| 88b328df7e | |||
| e3583dd04e | |||
| e484a2ebb5 | |||
| 72f86258d0 | |||
| 874cb3b779 | |||
| f21e0228b4 | |||
| 01cd82bc6a | |||
| d2a6a8b283 | |||
| f242e460ed | |||
| 95e08253a6 | |||
| 202a4fa6f1 | |||
| 576f46cb88 | |||
| 2d73805ca3 | |||
| eedfa550a6 | |||
| 4ca718adc4 | |||
| 7c4ced8f46 | |||
| 7018f4ab25 | |||
| fda13875ef | |||
| 8005917452 | |||
| 91eee8587e | |||
| e9132abc25 | |||
| 444eac5c6e | |||
| a449c5f8c2 | |||
| 19d6dbaa52 | |||
| 8820619e82 | |||
| fb33bc7e07 | |||
| 57d1fa8410 | |||
| 838e38d84c | |||
| fc29056530 | |||
| 4e967c979c | |||
| 62a34848c7 | |||
| 01fe6cc542 | |||
| 3fdc25777d | |||
| 3b7d6f8334 | |||
| 4b3c8b2969 | |||
| ad80d69369 | |||
| e2d2249686 | |||
| dc64c34ccb | |||
| 0bf50659ab | |||
| ec26b16ddf | |||
| a044b74a1d | |||
| 1e7a1dce90 | |||
| 41c2772cff | |||
| 1bba2bc0ed | |||
| e11c523a75 | |||
| b911a890cf | |||
| c8f69c0b79 | |||
| 8a6248f120 | |||
| 88b310c394 | |||
| 973de2db55 | |||
| 1fe92f10c1 | |||
| 0e2e906b24 | |||
| 4667f8be03 | |||
| 4a51ac7a74 | |||
| 6099efe41a | |||
| 4254d595fc | |||
| 069ca4a89d | |||
| bca8568d64 | |||
| 5407717534 | |||
| 74ef760591 | |||
| 1b31d0622e | |||
| 56797948af | |||
| c0af2f25a1 | |||
| dc12b1df00 | |||
| 72a0931663 | |||
| 67584b9cc3 | |||
| 1bfaa28f9c | |||
| 07f49bcc37 | |||
| 193de8d3a9 | |||
| 1f2b3512c1 | |||
| ddc5bd3b36 | |||
| ee828d454a | |||
| 2916a73f4c | |||
| ae69af7e70 | |||
| 7f4397f8ca | |||
| 32830b93f1 | |||
| cdc0d5623b | |||
| e78b415832 | |||
| ff1379fd29 | |||
| d8134aa168 | |||
| 702b3e8473 | |||
| b307a177f4 | |||
| 059430bd0a | |||
| 530b60cbc2 | |||
| ba8577f268 |
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(",")
|
||||
+ ']';
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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];
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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><video></code> DOM element
|
||||
* to render video to.
|
||||
* @param {Element} localVideoElement a <code><video></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><video></code> DOM element
|
||||
* to render video to.
|
||||
* @param {Element} localVideoElement a <code><video></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><video></code> DOM element.
|
||||
* @return {Element} The dom element
|
||||
*/
|
||||
public getLocalVideoElement(): HTMLVideoElement {
|
||||
return this.localVideoElement;
|
||||
public getRemoteAssertedIdentity(): AssertedIdentity {
|
||||
return this.remoteAssertedIdentity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the remote <code><video></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><audio></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><video></code> DOM element. If this call is active,
|
||||
* video will be rendered to it immediately.
|
||||
* @param {Element} element The <code><video></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><video></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><video></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><audio></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><video></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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user