Compare commits

...

51 Commits

Author SHA1 Message Date
RiotRobot ab06f57965 v15.0.0 2021-10-25 11:22:22 +01:00
RiotRobot c46209a159 Prepare changelog for v15.0.0 2021-10-25 11:22:21 +01:00
RiotRobot 526758a07f v15.0.0-rc.1 2021-10-19 10:50:42 +01:00
RiotRobot 9686fba81e Prepare changelog for v15.0.0-rc.1 2021-10-19 10:50:41 +01:00
Germain bbc547fd7f Merge pull request #1982 from matrix-org/gsouquet/thread-event-new 2021-10-15 14:32:19 +01:00
Germain 7d5dbeaf2d Merge pull request #1981 from matrix-org/gsouquet/threads-root-guard 2021-10-15 14:32:13 +01:00
Germain Souquet 8c19cf45e2 Dispatch a Thread.New event 2021-10-15 11:09:14 +01:00
Germain Souquet c9277de8ee Remove console statement 2021-10-15 10:31:43 +01:00
Germain Souquet b5baca51a6 Null-guard for thread root event not loaded yet 2021-10-15 10:22:32 +01:00
Germain fc800821f9 Merge pull request #1980 from matrix-org/gsouquet/threads-relations 2021-10-15 08:53:42 +01:00
Germain Souquet efbc46937f Null-guard for event that do not have a thread 2021-10-14 17:37:33 +01:00
Germain Souquet 9ceeb2d220 Delete UnstableValue and use hardcoded value for m.thread 2021-10-14 17:09:00 +01:00
Germain Souquet 7b89862ed0 Make threads use 'm.thread' relation 2021-10-14 16:56:48 +01:00
David Baker 6804e4290a Merge pull request #1972 from SimonBrandner/feature/answer-no-cam
Try to answer a call without video if we can't access the camera
2021-10-14 12:15:35 +01:00
David Baker 0a3d820fda Merge pull request #1943 from freaktechnik/develop
Fix `requestVerificationDM` with chronological `pendingEventOrdering`
2021-10-14 12:14:20 +01:00
Michael Telatynski e2a9c95ea8 Merge pull request #1974 from SimonBrandner/task/security-ts 2021-10-14 10:02:14 +01:00
Šimon Brandner 3aefc9f02e Add tests for falling back to answering with no video
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-14 08:46:36 +02:00
Šimon Brandner 0042cb5292 Fix importRoomKeys() definition
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-13 16:50:27 +02:00
Denis Kasak b288c97372 Merge pull request #1975 from matrix-org/dkasak/more-detailed-room-key-sharing-logging
More detailed logging when sharing room keys
2021-10-12 13:26:43 +00:00
Denis Kasak a0de1740a4 Add comma per review
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2021-10-12 13:23:22 +00:00
Denis Kasak 8e2e3018d8 Log device objects for existing/new devices when sharing room keys. 2021-10-12 13:48:41 +02:00
Denis Kasak 60e93024e7 Fix comment regarding marking blocked devices as notified. 2021-10-12 13:43:28 +02:00
Denis Kasak d89aa365f9 More detailed logging of blocked devices.
Separately log how many blocked devices there are in total and list them
out.

Then, log how many devices were notified because they were newly
blocked and list those out too.
2021-10-12 13:43:28 +02:00
Denis Kasak 4241263f2c Make log message regarding notifying unnotified failed devices clearer. 2021-10-12 13:43:26 +02:00
Denis Kasak b009f9c21b Log more clearly when we're sharing room keys with *new* Olm sessions. 2021-10-12 13:41:04 +02:00
Denis Kasak 04ccec6de3 Log which devices room keys were shared with. 2021-10-12 13:41:04 +02:00
RiotRobot e7b41fadd0 Merge branch 'master' into develop 2021-10-12 08:50:43 +01:00
RiotRobot 2dc1acef2e Resetting package fields for development 2021-10-12 08:36:13 +01:00
RiotRobot b977b629f4 Merge branch 'release-v14.0.1' 2021-10-12 08:35:42 +01:00
RiotRobot 6bbd493e20 v14.0.1 2021-10-12 08:29:30 +01:00
RiotRobot 38ad435af5 Prepare changelog for v14.0.1 2021-10-12 08:29:30 +01:00
RiotRobot 8f245af317 Merge branch 'master' into develop 2021-10-11 11:48:22 +01:00
RiotRobot 9b2bfbe7d0 v14.0.0 2021-10-11 11:44:54 +01:00
RiotRobot f69ea85e55 Prepare changelog for v14.0.0 2021-10-11 11:44:53 +01:00
Šimon Brandner 7559a10526 Make opts in importRoomKeys optional
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-10 08:36:11 +02:00
Šimon Brandner 68dbe959bb Add a logline
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-10 08:19:29 +02:00
Šimon Brandner 74a875aa23 Add MockMediaDeviceInfo
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-10 08:19:29 +02:00
Šimon Brandner 4b679c5056 Try to answer without video if we can't access it
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-10 08:19:22 +02:00
Šimon Brandner cb8a260b9c Improve handling when we don't have a camera
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-09 08:36:19 +02:00
Robert Long 7d7975b8bb Fix connecting to a call without a webcam
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-09 08:05:59 +02:00
Travis Ralston f12513da96 Merge pull request #1971 from matrix-org/travis/spam-log
Remove excess logging from getRoomUpgradeHistory()
2021-10-08 10:33:07 -06:00
Travis Ralston 785a3e3381 Remove excess logging from getRoomUpgradeHistory()
On large accounts this can now cause a massive amount of spam. It's not really providing any value in day-to-day debugging either.
2021-10-08 10:28:20 -06:00
Michael Telatynski 29bc83f4a7 Merge pull request #1968 from matrix-org/t3chguy/fix/19276 2021-10-06 13:27:12 +01:00
Michael Telatynski 13c8774d45 Tweak Room Hierarchy root visibility and add loading getter 2021-10-06 10:25:27 +01:00
Travis Ralston c9212e770d Merge pull request #1966 from Alexendoo/declaration-map
Enable TypeScript declaration maps
2021-10-05 13:39:47 -06:00
Germain 08aa7803e3 Merge pull request #1964 from SimonBrandner/feature/call-feed-opts 2021-10-04 15:02:49 +01:00
Alex Macleod 770858c255 Enable TypeScript declaration maps
This allows go-to-definition to reach the original .ts files for
consumers of the package. It emits .d.ts.map files alongside the regular
.d.ts files

e.g. in sdk.createClient go-to-definition on createClient will place the
cursor at the function in src/matrix.ts rather than inside a definition
file lib/matrix.d.ts

Signed-off-by: Alex Macleod <alex@macleod.io>
2021-10-04 14:36:50 +01:00
Germain Souquet c73bb7d408 Update dependencies 2021-10-04 12:30:14 +01:00
Šimon Brandner c63abe9988 Use ICallFeedOpts in the CallFeed constructor
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-02 08:12:43 +02:00
Martin Giger 482e3c4cb7 Skip waiting if already sent and abort on cancel 2021-09-30 16:58:08 +02:00
Martin Giger 3c97d17770 Fix requestVerificationDM with chronological pendingEventOrdering
Signed-off-by: Martin Giger <martin@humanoids.be>
2021-09-21 20:04:07 +02:00
17 changed files with 565 additions and 463 deletions
+47 -1
View File
@@ -1,6 +1,52 @@
Changes in [14.0.0-rc.2](https://github.com/vector-im/element-desktop/releases/tag/v14.0.0-rc.2) (2021-10-07)
Changes in [15.0.0](https://github.com/vector-im/element-desktop/releases/tag/v15.0.0) (2021-10-25)
===================================================================================================
## 🚨 BREAKING CHANGES
* Use `ICallFeedOpts` in the `CallFeed` constructor. To construct a new `CallFeed` object you have to pass `ICallFeedOpts` e.g. `const callFeed = new CallFeed({client ([\#1964](https://github.com/matrix-org/matrix-js-sdk/pull/1964)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
## ✨ Features
* Make threads use 'm.thread' relation ([\#1980](https://github.com/matrix-org/matrix-js-sdk/pull/1980)).
* Try to answer a call without video if we can't access the camera ([\#1972](https://github.com/matrix-org/matrix-js-sdk/pull/1972)). Fixes vector-im/element-web#17975 and vector-im/element-web#17975. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Make `opts` in `importRoomKeys()` optional ([\#1974](https://github.com/matrix-org/matrix-js-sdk/pull/1974)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Enable TypeScript declaration maps ([\#1966](https://github.com/matrix-org/matrix-js-sdk/pull/1966)). Contributed by [Alexendoo](https://github.com/Alexendoo).
## 🐛 Bug Fixes
* Fix `requestVerificationDM` with chronological `pendingEventOrdering` ([\#1943](https://github.com/matrix-org/matrix-js-sdk/pull/1943)). Contributed by [freaktechnik](https://github.com/freaktechnik).
Changes in [15.0.0-rc.1](https://github.com/vector-im/element-desktop/releases/tag/v15.0.0-rc.1) (2021-10-19)
=============================================================================================================
## 🚨 BREAKING CHANGES
* Use `ICallFeedOpts` in the `CallFeed` constructor. To construct a new `CallFeed` object you have to pass `ICallFeedOpts` e.g. `const callFeed = new CallFeed({client ([\#1964](https://github.com/matrix-org/matrix-js-sdk/pull/1964)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
## ✨ Features
* Make threads use 'm.thread' relation ([\#1980](https://github.com/matrix-org/matrix-js-sdk/pull/1980)).
* Try to answer a call without video if we can't access the camera ([\#1972](https://github.com/matrix-org/matrix-js-sdk/pull/1972)). Fixes vector-im/element-web#17975 and vector-im/element-web#17975. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Make `opts` in `importRoomKeys()` optional ([\#1974](https://github.com/matrix-org/matrix-js-sdk/pull/1974)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Enable TypeScript declaration maps ([\#1966](https://github.com/matrix-org/matrix-js-sdk/pull/1966)). Contributed by [Alexendoo](https://github.com/Alexendoo).
## 🐛 Bug Fixes
* Fix `requestVerificationDM` with chronological `pendingEventOrdering` ([\#1943](https://github.com/matrix-org/matrix-js-sdk/pull/1943)). Contributed by [freaktechnik](https://github.com/freaktechnik).
Changes in [14.0.1](https://github.com/vector-im/element-desktop/releases/tag/v14.0.1) (2021-10-12)
===================================================================================================
## 🚨 BREAKING CHANGES
* Support for call upgrades. `setLocalVideoMuted()` and `setMicrophoneMuted()` are now `async` and return the new mute state ([\#1827](https://github.com/matrix-org/matrix-js-sdk/pull/1827)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
## ✨ Features
* Implement file versioning for tree spaces ([\#1952](https://github.com/matrix-org/matrix-js-sdk/pull/1952)).
* Allow answering calls without audio/video ([\#1950](https://github.com/matrix-org/matrix-js-sdk/pull/1950)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Add `bound` to `IThreepid` ([\#1941](https://github.com/matrix-org/matrix-js-sdk/pull/1941)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Add `trusted_locally` to `TrustInfo` ([\#1942](https://github.com/matrix-org/matrix-js-sdk/pull/1942)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
## 🐛 Bug Fixes
* Fix incorrect return value type in getJoinedRooms() ([\#1959](https://github.com/matrix-org/matrix-js-sdk/pull/1959)). Contributed by [psrpinto](https://github.com/psrpinto).
* Make sure to set `callLengthInterval` only once ([\#1958](https://github.com/matrix-org/matrix-js-sdk/pull/1958)). Fixes vector-im/element-web#19221 and vector-im/element-web#19221. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Fix event partitioning from non threading ready clients ([\#1948](https://github.com/matrix-org/matrix-js-sdk/pull/1948)).
* Ensure unencrypted fields get exposed by getEffectiveEvent() ([\#1938](https://github.com/matrix-org/matrix-js-sdk/pull/1938)). Fixes vector-im/element-web#19062 and vector-im/element-web#19062.
Changes in [14.0.0-rc.1](https://github.com/vector-im/element-desktop/releases/tag/v14.0.0-rc.1) (2021-10-04)
=============================================================================================================
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "14.0.0-rc.2",
"version": "15.0.0",
"description": "Matrix Client-Server SDK for Javascript",
"scripts": {
"prepublishOnly": "yarn build",
+27
View File
@@ -94,6 +94,17 @@ class MockMediaStream {
addEventListener() {}
}
class MockMediaDeviceInfo {
constructor(
public kind: "audio" | "video",
) {}
}
class MockMediaHandler {
getUserMediaStream() { return new MockMediaStream("mock_stream_from_media_handler"); }
stopUserMediaStream() {}
}
describe('Call', function() {
let client;
let call;
@@ -110,6 +121,8 @@ describe('Call', function() {
mediaDevices: {
// @ts-ignore Mock
getUserMedia: () => new MockMediaStream("local_stream"),
// @ts-ignore Mock
enumerateDevices: async () => [new MockMediaDeviceInfo("audio"), new MockMediaDeviceInfo("video")],
},
};
@@ -129,6 +142,8 @@ describe('Call', function() {
// We just stub out sendEvent: we're not interested in testing the client's
// event sending code here
client.client.sendEvent = () => {};
client.client.mediaHandler = new MockMediaHandler;
client.client.getMediaHandler = () => client.client.mediaHandler;
client.httpBackend.when("GET", "/voip/turnServer").respond(200, {});
call = new MatrixCall({
client: client.client,
@@ -368,4 +383,16 @@ describe('Call', function() {
call.setScreensharingEnabled(true);
expect(call.setScreensharingEnabledWithoutMetadataSupport).toHaveBeenCalled();
});
it("should fallback to answering with no video", async () => {
await client.httpBackend.flush();
call.shouldAnswerWithMediaType = (wantedValue: boolean) => wantedValue;
client.client.mediaHandler.getUserMediaStream = jest.fn().mockRejectedValue("reject");
await call.answer(true, true);
expect(client.client.mediaHandler.getUserMediaStream).toHaveBeenNthCalledWith(1, true, true);
expect(client.client.mediaHandler.getUserMediaStream).toHaveBeenNthCalledWith(2, true, false);
});
});
+8 -5
View File
@@ -92,6 +92,14 @@ export enum EventType {
export enum RelationType {
Annotation = "m.annotation",
Replace = "m.replace",
/**
* Note, "io.element.thread" is hardcoded
* Should be replaced with "m.thread" once MSC3440 lands
* Can not use `UnstableValue` as TypeScript does not
* allow computed values in enums
* https://github.com/microsoft/TypeScript/issues/27976
*/
Thread = "io.element.thread",
}
export enum MsgType {
@@ -168,11 +176,6 @@ export const UNSTABLE_ELEMENT_FUNCTIONAL_USERS = new UnstableValue(
"io.element.functional_members",
"io.element.functional_members");
export const UNSTABLE_ELEMENT_REPLY_IN_THREAD = new UnstableValue(
"m.in_thread",
"io.element.in_thread",
);
export interface IEncryptedFile {
url: string;
mimetype?: string;
+1 -3
View File
@@ -2374,7 +2374,7 @@ export class MatrixClient extends EventEmitter {
* @return {Promise} a promise which resolves when the keys
* have been imported
*/
public importRoomKeys(keys: IMegolmSessionData[], opts: IImportRoomKeysOpts): Promise<void> {
public importRoomKeys(keys: IMegolmSessionData[], opts?: IImportRoomKeysOpts): Promise<void> {
if (!this.crypto) {
throw new Error("End-to-end encryption disabled");
}
@@ -4043,10 +4043,8 @@ export class MatrixClient extends EventEmitter {
// Work backwards first, looking at create events.
let createEvent = currentRoom.currentState.getStateEvents(EventType.RoomCreate, "");
while (createEvent) {
logger.log(`Looking at ${createEvent.getId()}`);
const predecessor = createEvent.getContent()['predecessor'];
if (predecessor && predecessor['room_id']) {
logger.log(`Looking at predecessor ${predecessor['room_id']}`);
const refRoom = this.getRoom(predecessor['room_id']);
if (!refRoom) break; // end of the chain
+24 -15
View File
@@ -341,12 +341,15 @@ class MegolmEncryption extends EncryptionAlgorithm {
await Promise.all([
(async () => {
// share keys with devices that we already have a session for
logger.debug(`Sharing keys with existing Olm sessions in ${this.roomId}`);
logger.debug(`Sharing keys with existing Olm sessions in ${this.roomId}`, olmSessions);
await this.shareKeyWithOlmSessions(session, key, payload, olmSessions);
logger.debug(`Shared keys with existing Olm sessions in ${this.roomId}`);
})(),
(async () => {
logger.debug(`Sharing keys (start phase 1) with new Olm sessions in ${this.roomId}`);
logger.debug(
`Sharing keys (start phase 1) with new Olm sessions in ${this.roomId}`,
devicesWithoutSession,
);
const errorDevices = [];
// meanwhile, establish olm sessions for devices that we don't
@@ -403,8 +406,11 @@ class MegolmEncryption extends EncryptionAlgorithm {
logger.debug(`Shared keys (all phases done) with new Olm sessions in ${this.roomId}`);
})(),
(async () => {
logger.debug(`Notifying blocked devices in ${this.roomId}`);
// also, notify blocked devices that they're blocked
logger.debug(`There are ${Object.entries(blocked).length} blocked devices in ${this.roomId}`,
Object.entries(blocked));
// also, notify newly blocked devices that they're blocked
logger.debug(`Notifying newly blocked devices in ${this.roomId}`);
const blockedMap: Record<string, Record<string, { device: IBlockedDevice }>> = {};
let blockedCount = 0;
for (const [userId, userBlockedDevices] of Object.entries(blocked)) {
@@ -421,7 +427,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
}
await this.notifyBlockedDevices(session, blockedMap);
logger.debug(`Notified ${blockedCount} blocked devices in ${this.roomId}`);
logger.debug(`Notified ${blockedCount} newly blocked devices in ${this.roomId}`, blockedMap);
})(),
]);
};
@@ -697,7 +703,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
await this.baseApis.sendToDevice("org.matrix.room_key.withheld", contentMap);
// store that we successfully uploaded the keys of the current slice
// record the fact that we notified these blocked devices
for (const userId of Object.keys(contentMap)) {
for (const deviceId of Object.keys(contentMap[userId])) {
session.markNotifiedBlockedDevice(userId, deviceId);
@@ -846,9 +852,9 @@ class MegolmEncryption extends EncryptionAlgorithm {
this.getDevicesWithoutSessions(devicemap, devicesByUser, errorDevices);
logger.debug(`Sharing keys with Olm sessions in ${this.roomId}`);
logger.debug(`Sharing keys with newly created Olm sessions in ${this.roomId}`);
await this.shareKeyWithOlmSessions(session, key, payload, devicemap);
logger.debug(`Shared keys with Olm sessions in ${this.roomId}`);
logger.debug(`Shared keys with newly created Olm sessions in ${this.roomId}`);
}
private async shareKeyWithOlmSessions(
@@ -864,7 +870,8 @@ class MegolmEncryption extends EncryptionAlgorithm {
`megolm keys for ${session.sessionId} ` +
`in ${this.roomId} (slice ${i + 1}/${userDeviceMaps.length})`;
try {
logger.debug(`Sharing ${taskDetail}`);
logger.debug(`Sharing ${taskDetail}`,
userDeviceMaps[i].map((d) => `${d.userId}/${d.deviceInfo.deviceId}`));
await this.encryptAndSendKeysToDevices(
session, key.chain_index, userDeviceMaps[i], payload,
);
@@ -906,14 +913,16 @@ class MegolmEncryption extends EncryptionAlgorithm {
);
}
const filteredFailedDevices =
await this.olmDevice.filterOutNotifiedErrorDevices(failedDevices);
const unnotifiedFailedDevices =
await this.olmDevice.filterOutNotifiedErrorDevices(
failedDevices,
);
logger.debug(
`Filtered down to ${filteredFailedDevices.length} error devices ` +
`in ${this.roomId}`,
`Need to notify ${unnotifiedFailedDevices.length} failed devices ` +
`which haven't been notified before in ${this.roomId}`,
);
const blockedMap: Record<string, Record<string, { device: IBlockedDevice }>> = {};
for (const { userId, deviceInfo } of filteredFailedDevices) {
for (const { userId, deviceInfo } of unnotifiedFailedDevices) {
blockedMap[userId] = blockedMap[userId] || {};
// we use a similar format to what
// olmlib.ensureOlmSessionsForDevices returns, so that
@@ -930,7 +939,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
// send the notifications
await this.notifyBlockedDevices(session, blockedMap);
logger.debug(
`Notified ${filteredFailedDevices.length} devices we failed to ` +
`Notified ${unnotifiedFailedDevices.length} devices we failed to ` +
`create Olm sessions in ${this.roomId}`,
);
}
+1 -1
View File
@@ -142,7 +142,7 @@ export interface IImportOpts {
}
export interface IImportRoomKeysOpts {
progressCallback: (stage: IImportOpts) => void;
progressCallback?: (stage: IImportOpts) => void;
untrusted?: boolean;
source?: string; // TODO: Enum
}
+26 -3
View File
@@ -40,7 +40,7 @@ import {
ISecretRequest,
SecretStorageKeyObject,
} from './SecretStorage';
import { IAddSecretStorageKeyOpts, ISecretStorageKeyInfo } from "./api";
import { IAddSecretStorageKeyOpts, IImportRoomKeysOpts, ISecretStorageKeyInfo } from "./api";
import { OutgoingRoomKeyRequestManager } from './OutgoingRoomKeyRequestManager';
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
import { ReciprocateQRCode, SCAN_QR_CODE_METHOD, SHOW_QR_CODE_METHOD } from './verification/QRCode';
@@ -58,7 +58,7 @@ import { BackupManager } from "./backup";
import { IStore } from "../store";
import { Room } from "../models/room";
import { RoomMember } from "../models/room-member";
import { MatrixEvent } from "../models/event";
import { MatrixEvent, EventStatus } from "../models/event";
import { MatrixClient, IKeysUploadResponse, SessionStore, ISignedKey } from "../client";
import type { EncryptionAlgorithm, DecryptionAlgorithm } from "./algorithms/base";
import type { IRoomEncryption, RoomList } from "./RoomList";
@@ -2658,7 +2658,7 @@ export class Crypto extends EventEmitter {
* @param {Function} opts.progressCallback called with an object which has a stage param
* @return {Promise} a promise which resolves once the keys have been imported
*/
public importRoomKeys(keys: IMegolmSessionData[], opts: any = {}): Promise<any> { // TODO types
public importRoomKeys(keys: IMegolmSessionData[], opts: IImportRoomKeysOpts = {}): Promise<any> { // TODO types
let successes = 0;
let failures = 0;
const total = keys.length;
@@ -3201,6 +3201,29 @@ export class Crypto extends EventEmitter {
createRequest: any, // TODO types
isLiveEvent = true,
): Promise<void> {
// Wait for event to get its final ID with pendingEventOrdering: "chronological", since DM channels depend on it.
if (event.isSending() && event.status != EventStatus.SENT) {
let eventIdListener;
let statusListener;
try {
await new Promise((resolve, reject) => {
eventIdListener = resolve;
statusListener = () => {
if (event.status == EventStatus.CANCELLED) {
reject(new Error("Event status set to CANCELLED."));
}
};
event.once("Event.localEventIdReplaced", eventIdListener);
event.on("Event.status", statusListener);
});
} catch (err) {
logger.error("error while waiting for the verification event to be sent: " + err.message);
return;
} finally {
event.removeListener("Event.localEventIdReplaced", eventIdListener);
event.removeListener("Event.status", statusListener);
}
}
let request = requestsMap.getRequest(event);
let isNewRequest = false;
if (!request) {
+24 -24
View File
@@ -28,7 +28,6 @@ import {
EventType,
MsgType,
RelationType,
UNSTABLE_ELEMENT_REPLY_IN_THREAD,
} from "../@types/event";
import { Crypto } from "../crypto";
import { deepSortedObjectEntries } from "../utils";
@@ -119,7 +118,7 @@ interface IAggregatedRelation {
key?: string;
}
interface IEventRelation {
export interface IEventRelation {
rel_type: RelationType | string;
event_id: string;
key?: string;
@@ -419,38 +418,39 @@ export class MatrixEvent extends EventEmitter {
/**
* @experimental
* Get the event ID of the replied event
* Get the event ID of the thread head
*/
public get replyEventId(): string {
const relations = this.getWireContent()["m.relates_to"];
return relations?.["m.in_reply_to"]?.["event_id"];
public get threadRootId(): string {
const relatesTo = this.getWireContent()?.["m.relates_to"];
if (relatesTo?.rel_type === RelationType.Thread) {
return relatesTo.event_id;
}
}
/**
* @experimental
* Determines whether a reply should be rendered in a thread
* or in the main room timeline
*/
public get replyInThread(): boolean {
/**
* UNSTABLE_ELEMENT_REPLY_IN_THREAD can live either
* at the m.relates_to and m.in_reply_to level
* This will likely change once we settle on a
* way to achieve threads
* TODO: Clean this up once we have a clear way forward
*/
public get isThreadRelation(): boolean {
return !!this.threadRootId;
}
const relatesTo = this.getWireContent()?.["m.relates_to"];
const replyTo = relatesTo?.["m.in_reply_to"];
return relatesTo?.[UNSTABLE_ELEMENT_REPLY_IN_THREAD.name]
|| (this.replyEventId && replyTo[UNSTABLE_ELEMENT_REPLY_IN_THREAD.name])
|| this.thread instanceof Thread;
/**
* @experimental
*/
public get isThreadRoot(): boolean {
const thread = this.getThread();
return thread?.id === this.getId();
}
public get parentEventId(): string {
return this.replyEventId
|| this.getWireContent()["m.relates_to"]?.event_id;
const relations = this.getWireContent()["m.relates_to"];
return relations?.["m.in_reply_to"]?.["event_id"]
|| relations?.event_id;
}
public get replyEventId(): string {
const relations = this.getWireContent()["m.relates_to"];
return relations?.["m.in_reply_to"]?.["event_id"];
}
/**
+29 -49
View File
@@ -149,7 +149,7 @@ export class Room extends EventEmitter {
/**
* @experimental
*/
public threads = new Set<Thread>();
public threads = new Map<string, Thread>();
/**
* Construct a new Room.
@@ -1068,19 +1068,6 @@ export class Room extends EventEmitter {
);
}
/**
* @experimental
*/
public addThread(thread: Thread): Set<Thread> {
this.threads.add(thread);
if (!thread.ready) {
thread.once(ThreadEvent.Ready, this.dedupeThreads);
this.emit(ThreadEvent.Update, thread);
this.reEmitter.reEmit(thread, [ThreadEvent.Update, ThreadEvent.Ready]);
}
return this.threads;
}
/**
* @experimental
*/
@@ -1097,26 +1084,6 @@ export class Room extends EventEmitter {
return Array.from(this.threads.values());
}
/**
* Two threads starting from a different child event can end up
* with the same event root. This method ensures that the duplicates
* are removed
* @experimental
*/
private dedupeThreads = (readyThread): void => {
const deduped = Array.from(this.threads).reduce((dedupedThreads, thread) => {
if (dedupedThreads.has(thread.id)) {
dedupedThreads.get(thread.id).merge(thread);
} else {
dedupedThreads.set(thread.id, thread);
}
return dedupedThreads;
}, new Map<string, Thread>());
this.threads = new Set<Thread>(deduped.values());
};
/**
* Get a member from the current room state.
* @param {string} userId The user ID of the member.
@@ -1293,21 +1260,38 @@ export class Room extends EventEmitter {
}
}
public findThreadForEvent(event: MatrixEvent): Thread {
if (!event) {
return null;
}
if (event.isThreadRelation) {
return this.threads.get(event.threadRootId);
} else {
const parentEvent = this.findEventById(event.parentEventId);
return this.findThreadForEvent(parentEvent);
}
}
/**
* Add an event to a thread's timeline. Will fire "Thread.update"
* @experimental
*/
public addThreadedEvent(event: MatrixEvent): void {
let thread = this.findEventById(event.parentEventId)?.getThread();
let thread = this.findThreadForEvent(event);
if (thread) {
thread.addEvent(event);
} else {
thread = new Thread([event], this, this.client);
}
if (!this.threads.has(thread)) {
this.addThread(thread);
const events = [event];
const rootEvent = this.findEventById(event.threadRootId);
if (rootEvent) {
events.unshift(rootEvent);
}
thread = new Thread(events, this, this.client);
this.reEmitter.reEmit(thread, [ThreadEvent.Update, ThreadEvent.Ready]);
this.threads.set(thread.id, thread);
this.emit(ThreadEvent.New, thread);
}
this.emit(ThreadEvent.Update, thread);
}
/**
@@ -1409,7 +1393,7 @@ export class Room extends EventEmitter {
// TODO: Enable "pending events" for threads
// There's a fair few things to update to make them work with Threads
// Will get back to it when the plan is to build a more polished UI ready for production
if (this.client?.supportsExperimentalThreads() && event.replyInThread) {
if (this.client?.supportsExperimentalThreads() && event.threadRootId) {
return;
}
@@ -1585,14 +1569,6 @@ export class Room extends EventEmitter {
oldEventId, oldStatus);
}
public findThreadByEventId(eventId: string): Thread {
for (const thread of this.threads) {
if (thread.has(eventId)) {
return thread;
}
}
}
/**
* Update the status / event id on a pending event, to reflect its transmission
* progress.
@@ -1733,6 +1709,10 @@ export class Room extends EventEmitter {
// TODO: We should have a filter to say "only add state event
// types X Y Z to the timeline".
this.addLiveEvent(events[i], duplicateStrategy, fromCache);
const thread = this.threads.get(events[i].getId());
if (thread && !thread.ready) {
thread.addEvent(events[i], true);
}
}
}
+13 -59
View File
@@ -22,13 +22,9 @@ import { EventTimelineSet } from './event-timeline-set';
import { Room } from './room';
export enum ThreadEvent {
New = "Thread.new",
Ready = "Thread.ready",
Update = "Thread.update"
}
interface ISerialisedThread {
id: string;
tails: string[];
Update = "Thread.update",
}
/**
@@ -42,7 +38,6 @@ export class Thread extends EventEmitter {
/**
* A reference to all the events ID at the bottom of the threads
*/
public readonly tail = new Set<string>();
public readonly timelineSet: EventTimelineSet;
constructor(
@@ -69,13 +64,12 @@ export class Thread extends EventEmitter {
return;
}
if (this.tail.has(event.replyEventId)) {
this.tail.delete(event.replyEventId);
}
this.tail.add(event.getId());
if (!event.replyEventId || !this.timelineSet.findEventById(event.replyEventId)) {
this.root = event.getId();
if (!this.root) {
if (event.isThreadRelation) {
this.root = event.threadRootId;
} else {
this.root = event.getId();
}
}
// all the relevant membership info to hydrate events with a sender
@@ -99,31 +93,6 @@ export class Thread extends EventEmitter {
this.emit(ThreadEvent.Update, this);
}
/**
* Completes the reply chain with all events
* missing from the current sync data
* Will fire "Thread.ready"
*/
public async fetchReplyChain(): Promise<void> {
if (!this.ready) {
let mxEvent = this.room.findEventById(this.rootEvent.replyEventId);
if (!mxEvent) {
mxEvent = await this.fetchEventById(
this.rootEvent.getRoomId(),
this.rootEvent.replyEventId,
);
}
this.addEvent(mxEvent, true);
if (mxEvent.replyEventId) {
await this.fetchReplyChain();
} else {
await this.decryptEvents();
this.emit(ThreadEvent.Ready, this);
}
}
}
private async decryptEvents(): Promise<void> {
await Promise.allSettled(
Array.from(this.timelineSet.getLiveTimeline().getEvents()).map(event => {
@@ -132,18 +101,6 @@ export class Thread extends EventEmitter {
);
}
/**
* Fetches an event over the network
*/
private async fetchEventById(roomId: string, eventId: string): Promise<MatrixEvent> {
const response = await this.client.http.authedRequest(
undefined,
"GET",
`/rooms/${roomId}/event/${eventId}`,
);
return new MatrixEvent(response);
}
/**
* Finds an event by ID in the current thread
*/
@@ -155,7 +112,7 @@ export class Thread extends EventEmitter {
* Determines thread's ready status
*/
public get ready(): boolean {
return this.rootEvent.replyEventId === undefined;
return this.rootEvent !== undefined;
}
/**
@@ -217,29 +174,26 @@ export class Thread extends EventEmitter {
return this.timelineSet.findEventById(eventId) instanceof MatrixEvent;
}
public toJson(): ISerialisedThread {
return {
id: this.id,
tails: Array.from(this.tail),
};
}
public on(event: ThreadEvent, listener: (...args: any[]) => void): this {
super.on(event, listener);
return this;
}
public once(event: ThreadEvent, listener: (...args: any[]) => void): this {
super.once(event, listener);
return this;
}
public off(event: ThreadEvent, listener: (...args: any[]) => void): this {
super.off(event, listener);
return this;
}
public addListener(event: ThreadEvent, listener: (...args: any[]) => void): this {
super.addListener(event, listener);
return this;
}
public removeListener(event: ThreadEvent, listener: (...args: any[]) => void): this {
super.removeListener(event, listener);
return this;
+2 -2
View File
@@ -319,13 +319,13 @@ export class SyncApi {
// An event should live in the thread timeline if
// - It's a reply in thread event
// - It's related to a reply in thread event
let shouldLiveInThreadTimeline = event.replyInThread;
let shouldLiveInThreadTimeline = event.isThreadRelation;
if (!shouldLiveInThreadTimeline) {
const parentEventId = event.parentEventId;
const parentEvent = room?.findEventById(parentEventId) || events.find((mxEv: MatrixEvent) => {
return mxEv.getId() === parentEventId;
});
shouldLiveInThreadTimeline = parentEvent?.replyInThread;
shouldLiveInThreadTimeline = parentEvent?.isThreadRelation;
}
memo[shouldLiveInThreadTimeline ? 1 : 0].push(event);
return memo;
+50 -7
View File
@@ -497,7 +497,15 @@ export class MatrixCall extends EventEmitter {
if (existingFeed) {
existingFeed.setNewStream(stream);
} else {
this.feeds.push(new CallFeed(stream, userId, purpose, this.client, this.roomId, audioMuted, videoMuted));
this.feeds.push(new CallFeed({
client: this.client,
roomId: this.roomId,
userId,
stream,
purpose,
audioMuted,
videoMuted,
}));
this.emit(CallEvent.FeedsChanged, this.feeds);
}
@@ -528,7 +536,15 @@ export class MatrixCall extends EventEmitter {
if (feed) {
feed.setNewStream(stream);
} else {
this.feeds.push(new CallFeed(stream, userId, purpose, this.client, this.roomId, false, false));
this.feeds.push(new CallFeed({
client: this.client,
roomId: this.roomId,
audioMuted: false,
videoMuted: false,
userId,
stream,
purpose,
}));
this.emit(CallEvent.FeedsChanged, this.feeds);
}
@@ -543,7 +559,15 @@ export class MatrixCall extends EventEmitter {
if (existingFeed) {
existingFeed.setNewStream(stream);
} else {
this.feeds.push(new CallFeed(stream, userId, purpose, this.client, this.roomId, false, false));
this.feeds.push(new CallFeed({
client: this.client,
roomId: this.roomId,
audioMuted: stream.getAudioTracks().length === 0,
videoMuted: stream.getVideoTracks().length === 0,
userId,
stream,
purpose,
}));
this.emit(CallEvent.FeedsChanged, this.feeds);
}
@@ -728,19 +752,30 @@ export class MatrixCall extends EventEmitter {
logger.debug(`Answering call ${this.callId}`);
if (!this.localUsermediaStream && !this.waitForLocalAVStream) {
const prevState = this.state;
const answerWithAudio = this.shouldAnswerWithMediaType(audio, this.hasRemoteUserMediaAudioTrack, "audio");
const answerWithVideo = this.shouldAnswerWithMediaType(video, this.hasRemoteUserMediaVideoTrack, "video");
this.setState(CallState.WaitLocalMedia);
this.waitForLocalAVStream = true;
try {
const mediaStream = await this.client.getMediaHandler().getUserMediaStream(
this.shouldAnswerWithMediaType(audio, this.hasRemoteUserMediaAudioTrack, "audio"),
this.shouldAnswerWithMediaType(video, this.hasRemoteUserMediaVideoTrack, "video"),
answerWithAudio, answerWithVideo,
);
this.waitForLocalAVStream = false;
this.gotUserMediaForAnswer(mediaStream);
} catch (e) {
this.getUserMediaFailed(e);
return;
if (answerWithVideo) {
// Try to answer without video
logger.warn("Failed to getUserMedia(), trying to getUserMedia() without video");
this.setState(prevState);
this.waitForLocalAVStream = false;
await this.answer(answerWithAudio, false);
} else {
this.getUserMediaFailed(e);
return;
}
}
} else if (this.waitForLocalAVStream) {
this.setState(CallState.WaitLocalMedia);
@@ -969,6 +1004,10 @@ export class MatrixCall extends EventEmitter {
* @returns the new mute state
*/
public async setLocalVideoMuted(muted: boolean): Promise<boolean> {
if (!await this.client.getMediaHandler().hasVideoDevice()) {
return this.isLocalVideoMuted();
}
if (!this.hasLocalUserMediaVideoTrack && !muted) {
await this.upgradeCall(false, true);
return this.isLocalVideoMuted();
@@ -997,6 +1036,10 @@ export class MatrixCall extends EventEmitter {
* @returns the new mute state
*/
public async setMicrophoneMuted(muted: boolean): Promise<boolean> {
if (!await this.client.getMediaHandler().hasAudioDevice()) {
return this.isMicrophoneMuted();
}
if (!this.hasLocalUserMediaAudioTrack && !muted) {
await this.upgradeCall(true, false);
return this.isMicrophoneMuted();
+28 -10
View File
@@ -22,6 +22,16 @@ import { RoomMember } from "../models/room-member";
const POLLING_INTERVAL = 250; // ms
const SPEAKING_THRESHOLD = -60; // dB
export interface ICallFeedOpts {
client: MatrixClient;
roomId: string;
userId: string;
stream: MediaStream;
purpose: SDPStreamMetadataPurpose;
audioMuted: boolean;
videoMuted: boolean;
}
export enum CallFeedEvent {
NewStream = "new_stream",
MuteStateChanged = "mute_state_changed",
@@ -30,6 +40,14 @@ export enum CallFeedEvent {
}
export class CallFeed extends EventEmitter {
public stream: MediaStream;
public userId: string;
public purpose: SDPStreamMetadataPurpose;
private client: MatrixClient;
private roomId: string;
private audioMuted: boolean;
private videoMuted: boolean;
private measuringVolumeActivity = false;
private audioContext: AudioContext;
private analyser: AnalyserNode;
@@ -38,17 +56,17 @@ export class CallFeed extends EventEmitter {
private speaking = false;
private volumeLooperTimeout: number;
constructor(
public stream: MediaStream,
public userId: string,
public purpose: SDPStreamMetadataPurpose,
private client: MatrixClient,
private roomId: string,
private audioMuted: boolean,
private videoMuted: boolean,
) {
constructor(opts: ICallFeedOpts) {
super();
this.updateStream(null, stream);
this.client = opts.client;
this.roomId = opts.roomId;
this.userId = opts.userId;
this.purpose = opts.purpose;
this.audioMuted = opts.audioMuted;
this.videoMuted = opts.videoMuted;
this.updateStream(null, opts.stream);
if (this.hasAudioTrack) {
this.initVolumeMeasuring();
+17 -3
View File
@@ -43,23 +43,37 @@ export class MediaHandler {
this.videoInput = deviceId;
}
public async hasAudioDevice(): Promise<boolean> {
const devices = await navigator.mediaDevices.enumerateDevices();
return devices.filter(device => device.kind === "audioinput").length > 0;
}
public async hasVideoDevice(): Promise<boolean> {
const devices = await navigator.mediaDevices.enumerateDevices();
return devices.filter(device => device.kind === "videoinput").length > 0;
}
/**
* @returns {MediaStream} based on passed parameters
*/
public async getUserMediaStream(audio: boolean, video: boolean): Promise<MediaStream> {
const shouldRequestAudio = audio && await this.hasAudioDevice();
const shouldRequestVideo = video && await this.hasVideoDevice();
let stream: MediaStream;
// Find a stream with matching tracks
const matchingStream = this.userMediaStreams.find((stream) => {
if (audio !== (stream.getAudioTracks().length > 0)) return false;
if (video !== (stream.getVideoTracks().length > 0)) return false;
if (shouldRequestAudio !== (stream.getAudioTracks().length > 0)) return false;
if (shouldRequestVideo !== (stream.getVideoTracks().length > 0)) return false;
return true;
});
if (matchingStream) {
logger.log("Cloning user media stream", matchingStream.id);
stream = matchingStream.clone();
} else {
const constraints = this.getUserMediaContraints(audio, video);
const constraints = this.getUserMediaContraints(shouldRequestAudio, shouldRequestVideo);
logger.log("Getting user media with constraints", constraints);
stream = await navigator.mediaDevices.getUserMedia(constraints);
}
+1
View File
@@ -1,6 +1,7 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"declarationMap": true,
"sourceMap": true,
"noEmit": false,
"emitDecoratorMetadata": true,
+266 -280
View File
File diff suppressed because it is too large Load Diff