Compare commits

...

55 Commits

Author SHA1 Message Date
RiotRobot d705a0ed9e v17.0.0-rc.2 2022-04-08 12:08:04 +01:00
RiotRobot 7023fb1c99 Prepare changelog for v17.0.0-rc.2 2022-04-08 12:08:03 +01:00
RiotRobot fe36dafcc7 v3.42.2-rc.3 2022-04-08 10:40:56 +01:00
RiotRobot 04a6dbbedf Prepare changelog for v3.42.2-rc.3 2022-04-08 10:40:56 +01:00
RiotRobot 29b427aab7 v17.0.0-rc.1 2022-04-08 10:36:14 +01:00
RiotRobot 643b783dec Prepare changelog for v17.0.0-rc.1 2022-04-08 10:36:13 +01:00
Germain 872033a552 Port #2283 to release (#2284) 2022-04-08 10:34:09 +01:00
RiotRobot d457fd6db0 v16.0.2-rc.1 2022-04-05 18:07:38 +01:00
RiotRobot 03f4700bd7 Prepare changelog for v16.0.2-rc.1 2022-04-05 18:07:37 +01:00
Robin b8321290f8 Add Element video room type (#2273) 2022-04-04 10:29:35 -04:00
Kerry 71b7521f42 Live location sharing - handle redacted beacons (#2269)
* emit beacon destroy event on destroy

Signed-off-by: Kerry Archibald <kerrya@element.io>

* handle redacted beacon events in room-state

Signed-off-by: Kerry Archibald <kerrya@element.io>

* empty line

Signed-off-by: Kerry Archibald <kerrya@element.io>
2022-04-04 10:17:49 +02:00
adamvy 106f7beb48 Fix getSessionsNeedingBackup() limit support (#2270) 2022-04-01 19:56:07 -06:00
Michael Telatynski d6f1c6cfdc Fix thread & main timeline partitioning logic (#2264) 2022-03-31 13:57:37 +01:00
Robin 4360ae7ff8 Fix coverage diffs for PRs that aren't up to date (#2263) 2022-03-30 08:02:14 -04:00
Michael Telatynski 26cbe02a7f Instantiate Thread objects when running fetchRoomThreads (#2262) 2022-03-29 15:28:08 +01:00
Michael Telatynski 85b8d4f83a Fix issues with /search and /context API handling for threads (#2261) 2022-03-29 09:24:45 +01:00
RiotRobot bdc3da1fac Resetting package fields for development 2022-03-28 14:38:44 +01:00
RiotRobot 95fcd5aa26 Merge branch 'master' into develop 2022-03-28 14:38:43 +01:00
RiotRobot 0ad83c43e4 v16.0.1 2022-03-28 14:35:08 +01:00
RiotRobot d6e4de4761 Prepare changelog for v16.0.1 2022-03-28 14:35:07 +01:00
Andy Balaam f03a391f80 Prevent exception 'Unable to set up secret storage' (#2260) 2022-03-28 11:48:34 +01:00
Andy Balaam e90f12ee32 Port codecov improvements matrix-react-sdk -> matrix-js-sdk (#2258) 2022-03-25 09:09:35 +00:00
Michael Telatynski c541b3f1ce Fix issues with duplicated MatrixEvent objects around threads (#2256) 2022-03-24 12:24:19 +00:00
Germain 6192325fe0 Thread list ordering by last reply (#2253) 2022-03-23 14:43:30 +00:00
Travis Ralston d0b964837f Remove groups (#2234)
This API is due for removal in Synapse and has been deprecated for a very long time. People should move away from it soon, but just in case we'll declare this as a breaking change.

There is no impact on sync storage here: we happen to store the data in a way that is backwards-compatible for group-supporting clients, and the code guards against missing data from the stores. So, if someone were to revert, they'd be "safe" (probably lose all their group info, but the app wouldn't crash).
2022-03-22 22:20:32 +00:00
Robin 65316ffb5c Voice rooms prototype (#2249)
* Support call room type from MSC3417

Signed-off-by: Robin Townsend <robin@robin.town>

* Make it more clear that call room type is unstable

Signed-off-by: Robin Townsend <robin@robin.town>
2022-03-22 16:14:23 -06:00
RiotRobot dadc19897c v16.0.1-rc.1 2022-03-22 21:40:04 +00:00
RiotRobot bd2f1858f4 Prepare changelog for v16.0.1-rc.1 2022-03-22 21:40:03 +00:00
Germain e32b8a75ee Lazy load thread list timeline set (#2254) 2022-03-22 21:34:20 +00:00
Kerry a6fe8797f0 Use beacon info event type as beacon identifier (#2251)
* use beacon info event type as beacon identifier

Signed-off-by: Kerry Archibald <kerrya@element.io>

* test cases

Signed-off-by: Kerry Archibald <kerrya@element.io>
2022-03-22 11:14:43 +01:00
Michael Telatynski 29e54806a4 Improve typing for MatrixEvent::getContent generic (#2252) 2022-03-21 17:22:21 +01:00
Germain 75674d961a Create threads event timeline set in the room model (#2244) 2022-03-18 11:58:59 +00:00
Germain 779afbcb39 Apply redaction logic to threaded events (#2246) 2022-03-18 09:30:08 +00:00
Kerry a3f5ec1ba2 reemit beacon events (#2245)
* reemit beacon events

Signed-off-by: Kerry Archibald <kerrya@element.io>

* use specific imports

Signed-off-by: Kerry Archibald <kerrya@element.io>

* Update src/models/room-state.ts

Co-authored-by: Travis Ralston <travisr@matrix.org>

Co-authored-by: Travis Ralston <travisr@matrix.org>
2022-03-18 08:56:53 +00:00
Kerry 524322280b add upsert function for updating beacon events (#2247)
* add upsert function for updating beacon events

Signed-off-by: Kerry Archibald <kerrya@element.io>

* expose event type on beacon model

Signed-off-by: Kerry Archibald <kerrya@element.io>

* allow setting timestamp in beaconinfo content helper

Signed-off-by: Kerry Archibald <kerrya@element.io>

* expose parsed beacon info

Signed-off-by: Kerry Archibald <kerrya@element.io>
2022-03-18 09:52:27 +01:00
James Salter 905a884f72 Formalise guidelines around writing tests (#2179) 2022-03-17 09:10:12 +00:00
Kerry 157635476b fix missed types for event emitter (#2243)
Signed-off-by: Kerry Archibald <kerrya@element.io>
2022-03-16 13:50:07 +01:00
Kerry 18943d6519 emit aggregate room beacon liveness (#2241)
* emit aggregate room beacon liveness

Signed-off-by: Kerry Archibald <kerrya@element.io>

* tidy and comment

Signed-off-by: Kerry Archibald <kerrya@element.io>

* add export for models/beacon

Signed-off-by: Kerry Archibald <kerrya@element.io>

* add owner and roomId

Signed-off-by: Kerry Archibald <kerrya@element.io>

* copyright

Signed-off-by: Kerry Archibald <kerrya@element.io>
2022-03-16 08:54:13 +00:00
Germain 42b3b73551 Fix threads reply count sometimes off by one (#2240) 2022-03-15 18:59:12 -06:00
RiotRobot ee9eccb85a Resetting package fields for development 2022-03-15 14:22:43 +00:00
RiotRobot 03a8d9edb6 Merge branch 'master' into develop 2022-03-15 14:22:42 +00:00
Germain 510833b2f2 Use stable value for fallback (#2239) 2022-03-15 13:52:33 +00:00
Kerry c2fdb4478d Live location sharing - create m.beacon_info events (#2238)
* add content helpers

Signed-off-by: Kerry Archibald <kerrya@element.io>

* stubbed Beacon class

Signed-off-by: Kerry Archibald <kerrya@element.io>

* beacon test utils

Signed-off-by: Kerry Archibald <kerrya@element.io>

* add beacon test utils

Signed-off-by: Kerry Archibald <kerrya@element.io>

* copyrights

Signed-off-by: Kerry Archibald <kerrya@element.io>

* add beacons to room state

Signed-off-by: Kerry Archibald <kerrya@element.io>

* tidy comments

Signed-off-by: Kerry Archibald <kerrya@element.io>

* unit test RoomState.setBeacon

Signed-off-by: Kerry Archibald <kerrya@element.io>
2022-03-15 10:31:32 +01:00
Kerry 57d71ccd0f Move test-utils into a directory (#2236)
* move test-utils.js into directory

Signed-off-by: Kerry Archibald <kerrya@element.io>

* fix imports

Signed-off-by: Kerry Archibald <kerrya@element.io>
2022-03-14 14:55:11 +01:00
Kerry d064d82fcc Beacon event types from MSC3489 (#2230)
* ASSET_NODE_TYPE -> M_ASSET

Signed-off-by: Kerry Archibald <kerrya@element.io>

* export const M_TIMESTAMP = new UnstableValue("m.ts", "org.matrix.msc3488.ts");

Signed-off-by: Kerry Archibald <kerrya@element.io>

* LOCATION_EVENT_TYPE -> M_LOCATION

Signed-off-by: Kerry Archibald <kerrya@element.io>

* extensible event types for location

Signed-off-by: Kerry Archibald <kerrya@element.io>

* add locationevent parsing helpers

Signed-off-by: Kerry Archibald <kerrya@element.io>

* rename

Signed-off-by: Kerry Archibald <kerrya@element.io>

* comment

Signed-off-by: Kerry Archibald <kerrya@element.io>

* revert makelocationcontent signature

Signed-off-by: Kerry Archibald <kerrya@element.io>

* add beacon event types

Signed-off-by: Kerry Archibald <kerrya@element.io>

* add variable* to type and comment

Signed-off-by: Kerry Archibald <kerrya@element.io>

* add content helper functions for beacon_info and beacon

Signed-off-by: Kerry Archibald <kerrya@element.io>

* copyright

Signed-off-by: Kerry Archibald <kerrya@element.io>

* add m.beacon_info.live from msc3672

Signed-off-by: Kerry Archibald <kerrya@element.io>
2022-03-14 12:13:28 +00:00
Hubert Chathi 17f3920ddd Send and handle stable name for withheld codes (#2232)
since MSC2399 is finished FCP and it's in the spec, we can use the stable name
now
2022-03-11 16:21:06 -05:00
Germain 9fc8048c30 Fix incorrect toJSON for filter-component (#2231) 2022-03-11 10:46:18 +00:00
Germain 9058dbf289 Switch to using stable values for Threads (#2228) 2022-03-11 09:04:17 +00:00
Kerry e16e7bc098 Location event helper functions (#2229)
* ASSET_NODE_TYPE -> M_ASSET

Signed-off-by: Kerry Archibald <kerrya@element.io>

* export const M_TIMESTAMP = new UnstableValue("m.ts", "org.matrix.msc3488.ts");

Signed-off-by: Kerry Archibald <kerrya@element.io>

* LOCATION_EVENT_TYPE -> M_LOCATION

Signed-off-by: Kerry Archibald <kerrya@element.io>

* extensible event types for location

Signed-off-by: Kerry Archibald <kerrya@element.io>

* add locationevent parsing helpers

Signed-off-by: Kerry Archibald <kerrya@element.io>

* rename

Signed-off-by: Kerry Archibald <kerrya@element.io>

* comment

Signed-off-by: Kerry Archibald <kerrya@element.io>

* revert makelocationcontent signature

Signed-off-by: Kerry Archibald <kerrya@element.io>
2022-03-10 18:40:13 +01:00
Germain dbcd01bb43 Fix missing threads in thread list (#2226)
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-03-10 16:44:42 +00:00
Michael Telatynski 40d1674a5c Fix incorrect usage of unstable variant of is_falling_back (#2227) 2022-03-10 15:31:57 +00:00
Michael Telatynski 35a375e3d2 Update thread relation fields to match MSC3440 changes (#2218) 2022-03-09 16:15:44 +00:00
Germain 460f4f9254 fix thread fallback targeting only thread relations (#2224) 2022-03-09 11:25:13 +00:00
J. Ryan Stinnett dbd6af745c Add lint for unused locals (#2223) 2022-03-09 10:09:00 +00:00
Germain 2ac9448646 Disable pending events for thread list when server supports threads (#2222) 2022-03-08 15:51:19 +00:00
70 changed files with 3919 additions and 2271 deletions
+6
View File
@@ -1,3 +1,9 @@
codecov:
allow_coverage_offsets: True
coverage:
status:
project: off
patch: off
comment:
layout: "diff, files"
behavior: default
+12
View File
@@ -6,9 +6,20 @@ on:
jobs:
test-coverage:
runs-on: ubuntu-latest
env:
# This must be set for fetchdep.sh to get the right branch
PR_NUMBER: ${{github.event.number}}
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
# If this is a pull request, make sure we check out its head rather than the
# automatically generated merge commit, so that the coverage diff excludes
# unrelated changes in the base branch
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || '' }}
- name: Yarn cache
uses: c-hive/gha-yarn-cache@v2
- name: Run tests with coverage
run: "yarn install && yarn build && yarn coverage"
@@ -16,4 +27,5 @@ jobs:
- name: Upload coverage
uses: codecov/codecov-action@v2
with:
fail_ci_if_error: false
verbose: true
+41
View File
@@ -1,3 +1,38 @@
Changes in [17.0.0-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v17.0.0-rc.2) (2022-04-08)
============================================================================================================
Changes in [17.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v17.0.0-rc.1) (2022-04-08)
============================================================================================================
Changes in [16.0.2-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v16.0.2-rc.1) (2022-04-05)
============================================================================================================
## 🚨 BREAKING CHANGES
* Remove groups and groups-related APIs ([\#2234](https://github.com/matrix-org/matrix-js-sdk/pull/2234)).
## ✨ Features
* Add Element video room type ([\#2273](https://github.com/matrix-org/matrix-js-sdk/pull/2273)).
* Live location sharing - handle redacted beacons ([\#2269](https://github.com/matrix-org/matrix-js-sdk/pull/2269)).
## 🐛 Bug Fixes
* Fix getSessionsNeedingBackup() limit support ([\#2270](https://github.com/matrix-org/matrix-js-sdk/pull/2270)). Contributed by @adamvy.
* Fix issues with /search and /context API handling for threads ([\#2261](https://github.com/matrix-org/matrix-js-sdk/pull/2261)). Fixes vector-im/element-web#21543.
* Prevent exception 'Unable to set up secret storage' ([\#2260](https://github.com/matrix-org/matrix-js-sdk/pull/2260)).
Changes in [16.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v16.0.1) (2022-03-28)
==================================================================================================
## ✨ Features
* emit aggregate room beacon liveness ([\#2241](https://github.com/matrix-org/matrix-js-sdk/pull/2241)).
* Live location sharing - create m.beacon_info events ([\#2238](https://github.com/matrix-org/matrix-js-sdk/pull/2238)).
* Beacon event types from MSC3489 ([\#2230](https://github.com/matrix-org/matrix-js-sdk/pull/2230)).
## 🐛 Bug Fixes
* Fix incorrect usage of unstable variant of `is_falling_back` ([\#2227](https://github.com/matrix-org/matrix-js-sdk/pull/2227)).
Changes in [16.0.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v16.0.1-rc.1) (2022-03-22)
============================================================================================================
Changes in [16.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v16.0.0) (2022-03-15)
==================================================================================================
@@ -1974,6 +2009,12 @@ All Changes
* [BREAKING] Refactor the entire build process
[\#1113](https://github.com/matrix-org/matrix-js-sdk/pull/1113)
Changes in [3.42.2-rc.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v3.42.2-rc.3) (2022-04-08)
============================================================================================================
## 🐛 Bug Fixes
* Make self membership less prone to races ([\#2277](https://github.com/matrix-org/matrix-js-sdk/pull/2277)). Fixes vector-im/element-web#21661.
Changes in [3.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v3.0.0) (2020-01-13)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v3.0.0-rc.1...v3.0.0)
+40 -10
View File
@@ -100,18 +100,48 @@ checks, so please check back after a few minutes.
Tests
-----
If your PR is a feature (ie. if it's being labelled with the 'T-Enhancement'
label) then we require that the PR also includes tests. These need to test that
your feature works as expected and ideally test edge cases too. For the js-sdk
itself, your tests should generally be unit tests. matrix-react-sdk also uses
these guidelines, so for that your tests can be unit tests using
react-test-utils, snapshot tests or screenshot tests.
Your PR should include tests.
We don't require tests for bug fixes (T-Defect) but strongly encourage regression
tests for the bug itself wherever possible.
For new user facing features in `matrix-react-sdk` or `element-web`, you
must include:
In the future we may formalise this more with a minimum test coverage
percentage for the diff.
1. Comprehensive unit tests written in Jest. These are located in `/test`.
2. "happy path" end-to-end tests.
These are located in `/test/end-to-end-tests` in `matrix-react-sdk`, and
are run using `element-web`. Ideally, you would also include tests for edge
and error cases.
Unit tests are expected even when the feature is in labs. It's good practice
to write tests alongside the code as it ensures the code is testable from
the start, and gives you a fast feedback loop while you're developing the
functionality. End-to-end tests should be added prior to the feature
leaving labs, but don't have to be present from the start (although it might
be beneficial to have some running early, so you can test things faster).
For bugs in those repos, your change must include at least one unit test or
end-to-end test; which is best depends on what sort of test most concisely
exercises the area.
Changes to `matrix-js-sdk` must be accompanied by unit tests written in Jest.
These are located in `/spec/`.
When writing unit tests, please aim for a high level of test coverage
for new code - 80% or greater. If you cannot achieve that, please document
why it's not possible in your PR.
Tests validate that your change works as intended and also document
concisely what is being changed. Ideally, your new tests fail
prior to your change, and succeed once it has been applied. You may
find this simpler to achieve if you write the tests first.
If you're spiking some code that's experimental and not being used to support
production features, exceptions can be made to requirements for tests.
Note that tests will still be required in order to ship the feature, and it's
strongly encouraged to think about tests early in the process, as adding
tests later will become progressively more difficult.
If you're not sure how to approach writing tests for your change, ask for help
in [#element-dev](https://matrix.to/#/#element-dev:matrix.org).
Code style
----------
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "16.0.0",
"version": "17.0.0-rc.2",
"description": "Matrix Client-Server SDK for Javascript",
"scripts": {
"prepublishOnly": "yarn build",
+2 -2
View File
@@ -24,7 +24,7 @@ import MockHttpBackend from 'matrix-mock-request';
import { LocalStorageCryptoStore } from '../src/crypto/store/localStorage-crypto-store';
import { logger } from '../src/logger';
import { WebStorageSessionStore } from "../src/store/session/webstorage";
import { syncPromise } from "./test-utils";
import { syncPromise } from "./test-utils/test-utils";
import { createClient } from "../src/matrix";
import { MockStorageApi } from "./MockStorageApi";
@@ -86,7 +86,7 @@ TestClient.prototype.toString = function() {
*/
TestClient.prototype.start = function() {
logger.log(this + ': starting');
this.httpBackend.when("GET", "/capabilities").respond(200, { capabilities: {} });
this.httpBackend.when("GET", "/versions").respond(200, {});
this.httpBackend.when("GET", "/pushrules").respond(200, {});
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
this.expectDeviceKeyUpload();
+2 -2
View File
@@ -17,7 +17,7 @@ limitations under the License.
// load XmlHttpRequest mock
import "./setupTests";
import "../../dist/browser-matrix"; // uses browser-matrix instead of the src
import * as utils from "../test-utils";
import * as utils from "../test-utils/test-utils";
import { TestClient } from "../TestClient";
const USER_ID = "@user:test.server";
@@ -35,7 +35,7 @@ describe("Browserify Test", function() {
client = testClient.client;
httpBackend = testClient.httpBackend;
httpBackend.when("GET", "/capabilities").respond(200, { capabilities: {} });
httpBackend.when("GET", "/versions").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
+1 -1
View File
@@ -17,7 +17,7 @@ limitations under the License.
*/
import { TestClient } from '../TestClient';
import * as testUtils from '../test-utils';
import * as testUtils from '../test-utils/test-utils';
import { logger } from '../../src/logger';
const ROOM_ID = "!room:id";
+2 -2
View File
@@ -29,7 +29,7 @@ limitations under the License.
import '../olm-loader';
import { logger } from '../../src/logger';
import * as testUtils from "../test-utils";
import * as testUtils from "../test-utils/test-utils";
import { TestClient } from "../TestClient";
import { CRYPTO_ENABLED } from "../../src/client";
@@ -722,7 +722,7 @@ describe("MatrixClient crypto", function() {
return Promise.resolve()
.then(() => {
logger.log(aliTestClient + ': starting');
httpBackend.when("GET", "/capabilities").respond(200, {});
httpBackend.when("GET", "/versions").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
aliTestClient.expectDeviceKeyUpload();
@@ -1,4 +1,4 @@
import * as utils from "../test-utils";
import * as utils from "../test-utils/test-utils";
import { TestClient } from "../TestClient";
describe("MatrixClient events", function() {
@@ -11,9 +11,9 @@ describe("MatrixClient events", function() {
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
client = testClient.client;
httpBackend = testClient.httpBackend;
httpBackend.when("GET", "/versions").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
httpBackend.when("GET", "/capabilities").respond(200, { capabilities: {} });
});
afterEach(function() {
+73 -13
View File
@@ -1,7 +1,8 @@
import * as utils from "../test-utils";
import * as utils from "../test-utils/test-utils";
import { EventTimeline } from "../../src/matrix";
import { logger } from "../../src/logger";
import { TestClient } from "../TestClient";
import { Thread, THREAD_RELATION_TYPE } from "../../src/models/thread";
const userId = "@alice:localhost";
const userName = "Alice";
@@ -69,9 +70,30 @@ const EVENTS = [
}),
];
const THREAD_ROOT = utils.mkMessage({
room: roomId,
user: userId,
msg: "thread root",
});
const THREAD_REPLY = utils.mkEvent({
room: roomId,
user: userId,
type: "m.room.message",
content: {
"body": "thread reply",
"msgtype": "m.text",
"m.relates_to": {
// We can't use the const here because we change server support mode for test
rel_type: "io.element.thread",
event_id: THREAD_ROOT.event_id,
},
},
});
// start the client, and wait for it to initialise
function startClient(httpBackend, client) {
httpBackend.when("GET", "/capabilities").respond(200, { capabilities: {} });
httpBackend.when("GET", "/versions").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
httpBackend.when("GET", "/sync").respond(200, INITIAL_SYNC_DATA);
@@ -116,9 +138,7 @@ describe("getEventTimeline support", function() {
return startClient(httpBackend, client).then(function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
expect(function() {
client.getEventTimeline(timelineSet, "event");
}).toThrow();
expect(client.getEventTimeline(timelineSet, "event")).rejects.toBeTruthy();
});
});
@@ -136,16 +156,12 @@ describe("getEventTimeline support", function() {
return startClient(httpBackend, client).then(() => {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
expect(function() {
client.getEventTimeline(timelineSet, "event");
}).not.toThrow();
expect(client.getEventTimeline(timelineSet, "event")).rejects.toBeFalsy();
});
});
it("scrollback should be able to scroll back to before a gappy /sync",
function() {
it("scrollback should be able to scroll back to before a gappy /sync", function() {
// need a client with timelineSupport disabled to make this work
let room;
return startClient(httpBackend, client).then(function() {
@@ -229,6 +245,7 @@ describe("MatrixClient event timelines", function() {
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
Thread.setServerSideSupport(false);
});
describe("getEventTimeline", function() {
@@ -355,8 +372,7 @@ describe("MatrixClient event timelines", function() {
]);
});
it("should join timelines where they overlap a previous /context",
function() {
it("should join timelines where they overlap a previous /context", function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
@@ -478,6 +494,50 @@ describe("MatrixClient event timelines", function() {
httpBackend.flushAllExpected(),
]);
});
it("should handle thread replies with server support by fetching a contiguous thread timeline", async () => {
Thread.setServerSideSupport(true);
client.stopClient(); // we don't need the client to be syncing at this time
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_REPLY.event_id))
.respond(200, function() {
return {
start: "start_token0",
events_before: [],
event: THREAD_REPLY,
events_after: [],
end: "end_token0",
state: [],
};
});
httpBackend.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id))
.respond(200, function() {
return THREAD_ROOT;
});
httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" +
encodeURIComponent(THREAD_ROOT.event_id) + "/" +
encodeURIComponent(THREAD_RELATION_TYPE.name) + "?limit=20")
.respond(200, function() {
return {
original_event: THREAD_ROOT,
chunk: [THREAD_REPLY],
next_batch: "next_batch_token0",
prev_batch: "prev_batch_token0",
};
});
const timelinePromise = client.getEventTimeline(timelineSet, THREAD_REPLY.event_id);
await httpBackend.flushAllExpected();
const timeline = await timelinePromise;
expect(timeline.getEvents().find(e => e.getId() === THREAD_ROOT.event_id));
expect(timeline.getEvents().find(e => e.getId() === THREAD_REPLY.event_id));
});
});
describe("paginateEventTimeline", function() {
+342 -53
View File
@@ -1,8 +1,9 @@
import * as utils from "../test-utils";
import * as utils from "../test-utils/test-utils";
import { CRYPTO_ENABLED } from "../../src/client";
import { MatrixEvent } from "../../src/models/event";
import { Filter, MemoryStore, Room } from "../../src/matrix";
import { TestClient } from "../TestClient";
import { THREAD_RELATION_TYPE } from "../../src/models/thread";
describe("MatrixClient", function() {
let client = null;
@@ -14,9 +15,7 @@ describe("MatrixClient", function() {
beforeEach(function() {
store = new MemoryStore();
const testClient = new TestClient(userId, "aliceDevice", accessToken, undefined, {
store: store,
});
const testClient = new TestClient(userId, "aliceDevice", accessToken, undefined, { store });
httpBackend = testClient.httpBackend;
client = testClient.client;
});
@@ -244,14 +243,15 @@ describe("MatrixClient", function() {
});
describe("searching", function() {
const response = {
search_categories: {
room_events: {
count: 24,
results: {
"$flibble:localhost": {
it("searchMessageText should perform a /search for room_events", function() {
const response = {
search_categories: {
room_events: {
count: 24,
results: [{
rank: 0.1,
result: {
event_id: "$flibble:localhost",
type: "m.room.message",
user_id: "@alice:localhost",
room_id: "!feuiwhf:localhost",
@@ -260,13 +260,11 @@ describe("MatrixClient", function() {
msgtype: "m.text",
},
},
},
}],
},
},
},
};
};
it("searchMessageText should perform a /search for room_events", function(done) {
client.searchMessageText({
query: "monkeys",
});
@@ -280,8 +278,171 @@ describe("MatrixClient", function() {
});
}).respond(200, response);
httpBackend.flush().then(function() {
done();
return httpBackend.flush();
});
describe("should filter out context from different timelines (threads)", () => {
it("filters out thread replies when result is in the main timeline", async () => {
const response = {
search_categories: {
room_events: {
count: 24,
results: [{
rank: 0.1,
result: {
event_id: "$flibble:localhost",
type: "m.room.message",
user_id: "@alice:localhost",
room_id: "!feuiwhf:localhost",
content: {
body: "main timeline",
msgtype: "m.text",
},
},
context: {
events_after: [{
event_id: "$ev-after:server",
type: "m.room.message",
user_id: "@alice:localhost",
room_id: "!feuiwhf:localhost",
content: {
"body": "thread reply",
"msgtype": "m.text",
"m.relates_to": {
"event_id": "$some-thread:server",
"rel_type": THREAD_RELATION_TYPE.name,
},
},
}],
events_before: [{
event_id: "$ev-before:server",
type: "m.room.message",
user_id: "@alice:localhost",
room_id: "!feuiwhf:localhost",
content: {
body: "main timeline again",
msgtype: "m.text",
},
}],
},
}],
},
},
};
const data = {
results: [],
highlights: [],
};
client.processRoomEventsSearch(data, response);
expect(data.results).toHaveLength(1);
expect(data.results[0].context.timeline).toHaveLength(2);
expect(data.results[0].context.timeline.find(e => e.getId() === "$ev-after:server")).toBeFalsy();
});
it("filters out thread replies from threads other than the thread the result replied to", () => {
const response = {
search_categories: {
room_events: {
count: 24,
results: [{
rank: 0.1,
result: {
event_id: "$flibble:localhost",
type: "m.room.message",
user_id: "@alice:localhost",
room_id: "!feuiwhf:localhost",
content: {
"body": "thread 1 reply 1",
"msgtype": "m.text",
"m.relates_to": {
"event_id": "$thread1:server",
"rel_type": THREAD_RELATION_TYPE.name,
},
},
},
context: {
events_after: [{
event_id: "$ev-after:server",
type: "m.room.message",
user_id: "@alice:localhost",
room_id: "!feuiwhf:localhost",
content: {
"body": "thread 2 reply 2",
"msgtype": "m.text",
"m.relates_to": {
"event_id": "$thread2:server",
"rel_type": THREAD_RELATION_TYPE.name,
},
},
}],
events_before: [],
},
}],
},
},
};
const data = {
results: [],
highlights: [],
};
client.processRoomEventsSearch(data, response);
expect(data.results).toHaveLength(1);
expect(data.results[0].context.timeline).toHaveLength(1);
expect(data.results[0].context.timeline.find(e => e.getId() === "$flibble:localhost")).toBeTruthy();
});
it("filters out main timeline events when result is a thread reply", () => {
const response = {
search_categories: {
room_events: {
count: 24,
results: [{
rank: 0.1,
result: {
event_id: "$flibble:localhost",
type: "m.room.message",
user_id: "@alice:localhost",
room_id: "!feuiwhf:localhost",
content: {
"body": "thread 1 reply 1",
"msgtype": "m.text",
"m.relates_to": {
"event_id": "$thread1:server",
"rel_type": THREAD_RELATION_TYPE.name,
},
},
},
context: {
events_after: [{
event_id: "$ev-after:server",
type: "m.room.message",
user_id: "@alice:localhost",
room_id: "!feuiwhf:localhost",
content: {
"body": "main timeline",
"msgtype": "m.text",
},
}],
events_before: [],
},
}],
},
},
};
const data = {
results: [],
highlights: [],
};
client.processRoomEventsSearch(data, response);
expect(data.results).toHaveLength(1);
expect(data.results[0].context.timeline).toHaveLength(1);
expect(data.results[0].context.timeline.find(e => e.getId() === "$flibble:localhost")).toBeTruthy();
});
});
});
@@ -395,9 +556,11 @@ describe("MatrixClient", function() {
});
describe("partitionThreadedEvents", function() {
const room = new Room("!STrMRsukXHtqQdSeHa:matrix.org", client, userId);
it("returns empty arrays when given an empty arrays", function() {
const events = [];
const [timeline, threaded] = client.partitionThreadedEvents(events);
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
expect(timeline).toEqual([]);
expect(threaded).toEqual([]);
});
@@ -405,24 +568,24 @@ describe("MatrixClient", function() {
it("copies pre-thread in-timeline vote events onto both timelines", function() {
client.clientOpts = { experimentalThreadSupport: true };
const eventMessageInThread = buildEventMessageInThread();
const eventPollResponseReference = buildEventPollResponseReference();
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
const eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot);
const events = [
eventPollStartThreadRoot,
eventMessageInThread,
eventPollResponseReference,
eventPollStartThreadRoot,
];
// Vote has no threadId yet
expect(eventPollResponseReference.threadId).toBeFalsy();
const [timeline, threaded] = client.partitionThreadedEvents(events);
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
expect(timeline).toEqual([
// The message that was sent in a thread is missing
eventPollResponseReference,
eventPollStartThreadRoot,
eventPollResponseReference,
]);
// The vote event has been copied into the thread
@@ -431,33 +594,34 @@ describe("MatrixClient", function() {
expect(eventRefWithThreadId.threadId).toBeTruthy();
expect(threaded).toEqual([
eventPollStartThreadRoot,
eventMessageInThread,
eventRefWithThreadId,
// Thread does not see thread root
]);
});
it("copies pre-thread in-timeline reactions onto both timelines", function() {
client.clientOpts = { experimentalThreadSupport: true };
const eventMessageInThread = buildEventMessageInThread();
const eventReaction = buildEventReaction();
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
const eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot);
const eventReaction = buildEventReaction(eventPollStartThreadRoot);
const events = [
eventPollStartThreadRoot,
eventMessageInThread,
eventReaction,
eventPollStartThreadRoot,
];
const [timeline, threaded] = client.partitionThreadedEvents(events);
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
expect(timeline).toEqual([
eventReaction,
eventPollStartThreadRoot,
eventReaction,
]);
expect(threaded).toEqual([
eventPollStartThreadRoot,
eventMessageInThread,
withThreadId(eventReaction, eventPollStartThreadRoot.getId()),
]);
@@ -467,23 +631,24 @@ describe("MatrixClient", function() {
client.clientOpts = { experimentalThreadSupport: true };
const eventPollResponseReference = buildEventPollResponseReference();
const eventMessageInThread = buildEventMessageInThread();
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
const eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot);
const events = [
eventPollStartThreadRoot,
eventPollResponseReference,
eventMessageInThread,
eventPollStartThreadRoot,
];
const [timeline, threaded] = client.partitionThreadedEvents(events);
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
expect(timeline).toEqual([
eventPollResponseReference,
eventPollStartThreadRoot,
eventPollResponseReference,
]);
expect(threaded).toEqual([
eventPollStartThreadRoot,
withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()),
eventMessageInThread,
]);
@@ -492,26 +657,27 @@ describe("MatrixClient", function() {
it("copies post-thread in-timeline reactions onto both timelines", function() {
client.clientOpts = { experimentalThreadSupport: true };
const eventReaction = buildEventReaction();
const eventMessageInThread = buildEventMessageInThread();
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
const eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot);
const eventReaction = buildEventReaction(eventPollStartThreadRoot);
const events = [
eventReaction,
eventMessageInThread,
eventPollStartThreadRoot,
eventMessageInThread,
eventReaction,
];
const [timeline, threaded] = client.partitionThreadedEvents(events);
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
expect(timeline).toEqual([
eventReaction,
eventPollStartThreadRoot,
eventReaction,
]);
expect(threaded).toEqual([
withThreadId(eventReaction, eventPollStartThreadRoot.getId()),
eventPollStartThreadRoot,
eventMessageInThread,
withThreadId(eventReaction, eventPollStartThreadRoot.getId()),
]);
});
@@ -519,9 +685,9 @@ describe("MatrixClient", function() {
client.clientOpts = { experimentalThreadSupport: true };
// This is based on recording the events in a real room:
const eventMessageInThread = buildEventMessageInThread();
const eventPollResponseReference = buildEventPollResponseReference();
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
const eventPollResponseReference = buildEventPollResponseReference();
const eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot);
const eventRoomName = buildEventRoomName();
const eventEncryption = buildEventEncryption();
const eventGuestAccess = buildEventGuestAccess();
@@ -532,9 +698,9 @@ describe("MatrixClient", function() {
const eventCreate = buildEventCreate();
const events = [
eventMessageInThread,
eventPollResponseReference,
eventPollStartThreadRoot,
eventPollResponseReference,
eventMessageInThread,
eventRoomName,
eventEncryption,
eventGuestAccess,
@@ -544,12 +710,12 @@ describe("MatrixClient", function() {
eventMember,
eventCreate,
];
const [timeline, threaded] = client.partitionThreadedEvents(events);
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
expect(timeline).toEqual([
// The message that was sent in a thread is missing
eventPollResponseReference,
eventPollStartThreadRoot,
eventPollResponseReference,
eventRoomName,
eventEncryption,
eventGuestAccess,
@@ -560,11 +726,95 @@ describe("MatrixClient", function() {
eventCreate,
]);
// Thread should contain only stuff that happened in the thread -
// no thread root, and no room state events
// Thread should contain only stuff that happened in the thread - no room state events
expect(threaded).toEqual([
eventMessageInThread,
eventPollStartThreadRoot,
withThreadId(eventPollResponseReference, eventPollStartThreadRoot.getId()),
eventMessageInThread,
]);
});
it("sends redactions of reactions to thread responses to thread timeline only", () => {
client.clientOpts = { experimentalThreadSupport: true };
const threadRootEvent = buildEventPollStartThreadRoot();
const eventMessageInThread = buildEventMessageInThread(threadRootEvent);
const threadedReaction = buildEventReaction(eventMessageInThread);
const threadedReactionRedaction = buildEventRedaction(threadedReaction);
const events = [
threadRootEvent,
eventMessageInThread,
threadedReaction,
threadedReactionRedaction,
];
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
expect(timeline).toEqual([
threadRootEvent,
]);
expect(threaded).toEqual([
threadRootEvent,
eventMessageInThread,
threadedReaction,
threadedReactionRedaction,
]);
});
it("sends reply to reply to thread root outside of thread to main timeline only", () => {
client.clientOpts = { experimentalThreadSupport: true };
const threadRootEvent = buildEventPollStartThreadRoot();
const eventMessageInThread = buildEventMessageInThread(threadRootEvent);
const directReplyToThreadRoot = buildEventReply(threadRootEvent);
const replyToReply = buildEventReply(directReplyToThreadRoot);
const events = [
threadRootEvent,
eventMessageInThread,
directReplyToThreadRoot,
replyToReply,
];
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
expect(timeline).toEqual([
threadRootEvent,
directReplyToThreadRoot,
replyToReply,
]);
expect(threaded).toEqual([
threadRootEvent,
eventMessageInThread,
]);
});
it("sends reply to thread responses to thread timeline only", () => {
client.clientOpts = { experimentalThreadSupport: true };
const threadRootEvent = buildEventPollStartThreadRoot();
const eventMessageInThread = buildEventMessageInThread(threadRootEvent);
const replyToThreadResponse = buildEventReply(eventMessageInThread);
const events = [
threadRootEvent,
eventMessageInThread,
replyToThreadResponse,
];
const [timeline, threaded] = client.partitionThreadedEvents(room, events);
expect(timeline).toEqual([
threadRootEvent,
]);
expect(threaded).toEqual([
threadRootEvent,
eventMessageInThread,
replyToThreadResponse,
]);
});
});
@@ -576,18 +826,18 @@ function withThreadId(event, newThreadId) {
return ret;
}
const buildEventMessageInThread = () => new MatrixEvent({
const buildEventMessageInThread = (root) => new MatrixEvent({
"age": 80098509,
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
"ciphertext": "ENCRYPTEDSTUFF",
"device_id": "XISFUZSKHH",
"m.relates_to": {
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
"event_id": root.getId(),
"m.in_reply_to": {
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
"event_id": root.getId(),
},
"rel_type": "io.element.thread",
"rel_type": "m.thread",
},
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
@@ -623,10 +873,10 @@ const buildEventPollResponseReference = () => new MatrixEvent({
"user_id": "@andybalaam-test1:matrix.org",
});
const buildEventReaction = () => new MatrixEvent({
const buildEventReaction = (event) => new MatrixEvent({
"content": {
"m.relates_to": {
"event_id": "$VLS2ojbPmxb6x8ECetn45hmND6cRDcjgv-j-to9m7Vo",
"event_id": event.getId(),
"key": "🤗",
"rel_type": "m.annotation",
},
@@ -642,6 +892,22 @@ const buildEventReaction = () => new MatrixEvent({
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
});
const buildEventRedaction = (event) => new MatrixEvent({
"content": {
},
"origin_server_ts": 1643977249239,
"sender": "@andybalaam-test1:matrix.org",
"redacts": event.getId(),
"type": "m.room.redaction",
"unsigned": {
"age": 22597,
"transaction_id": "m1643977249073.17",
},
"event_id": "$86B2b-x3LgE4DlV4y24b7UHnt72LIA3rzjvMysTtAfB",
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
});
const buildEventPollStartThreadRoot = () => new MatrixEvent({
"age": 80108647,
"content": {
@@ -660,6 +926,29 @@ const buildEventPollStartThreadRoot = () => new MatrixEvent({
"user_id": "@andybalaam-test1:matrix.org",
});
const buildEventReply = (target) => new MatrixEvent({
"age": 80098509,
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
"ciphertext": "ENCRYPTEDSTUFF",
"device_id": "XISFUZSKHH",
"m.relates_to": {
"m.in_reply_to": {
"event_id": target.getId(),
},
},
"sender_key": "i3N3CtG/CD2bGB8rA9fW6adLYSDvlUhf2iuU73L65Vg",
"session_id": "Ja11R/KG6ua0wdk8zAzognrxjio1Gm/RK2Gn6lFL804",
},
"event_id": target.getId() + Math.random(),
"origin_server_ts": 1643815466378,
"room_id": "!STrMRsukXHtqQdSeHa:matrix.org",
"sender": "@andybalaam-test1:matrix.org",
"type": "m.room.encrypted",
"unsigned": { "age": 80098509 },
"user_id": "@andybalaam-test1:matrix.org",
});
const buildEventRoomName = () => new MatrixEvent({
"age": 80123249,
"content": {
+3 -3
View File
@@ -1,6 +1,6 @@
import HttpBackend from "matrix-mock-request";
import * as utils from "../test-utils";
import * as utils from "../test-utils/test-utils";
import { MatrixClient } from "../../src/matrix";
import { MatrixScheduler } from "../../src/scheduler";
import { MemoryStore } from "../../src/store/memory";
@@ -105,12 +105,12 @@ describe("MatrixClient opts", function() {
expectedEventTypes.indexOf(event.getType()), 1,
);
});
httpBackend.when("GET", "/capabilities").respond(200, { capabilities: {} });
httpBackend.when("GET", "/versions").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "foo" });
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
await httpBackend.flush("/capabilities", 1);
await httpBackend.flush("/versions", 1);
await httpBackend.flush("/pushrules", 1);
await httpBackend.flush("/filter", 1);
await Promise.all([
@@ -1,4 +1,4 @@
import * as utils from "../test-utils";
import * as utils from "../test-utils/test-utils";
import { EventStatus } from "../../src/models/event";
import { TestClient } from "../TestClient";
@@ -109,7 +109,7 @@ describe("MatrixClient room timelines", function() {
client = testClient.client;
setNextSyncData();
httpBackend.when("GET", "/capabilities").respond(200, { capabilities: {} });
httpBackend.when("GET", "/versions").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
@@ -118,7 +118,7 @@ describe("MatrixClient room timelines", function() {
});
client.startClient();
await httpBackend.flush("/capabilities");
await httpBackend.flush("/versions");
await httpBackend.flush("/pushrules");
await httpBackend.flush("/filter");
});
@@ -553,6 +553,7 @@ describe("MatrixClient room timelines", function() {
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
return Promise.all([
httpBackend.flush("/versions", 1),
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(() => {
+3 -4
View File
@@ -1,6 +1,6 @@
import { MatrixEvent } from "../../src/models/event";
import { EventTimeline } from "../../src/models/event-timeline";
import * as utils from "../test-utils";
import * as utils from "../test-utils/test-utils";
import { TestClient } from "../TestClient";
describe("MatrixClient syncing", function() {
@@ -19,7 +19,7 @@ describe("MatrixClient syncing", function() {
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
httpBackend = testClient.httpBackend;
client = testClient.client;
httpBackend.when("GET", "/capabilities").respond(200, { capabilities: {} });
httpBackend.when("GET", "/versions").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
});
@@ -735,8 +735,7 @@ describe("MatrixClient syncing", function() {
expect(tok).toEqual("pagTok");
}),
// first flush the filter request; this will make syncLeftRooms
// make its /sync call
// first flush the filter request; this will make syncLeftRooms make its /sync call
httpBackend.flush("/filter").then(function() {
return httpBackend.flushAllExpected();
}),
+7 -1
View File
@@ -17,7 +17,7 @@ limitations under the License.
import anotherjson from "another-json";
import * as testUtils from "../test-utils";
import * as testUtils from "../test-utils/test-utils";
import { TestClient } from "../TestClient";
import { logger } from "../../src/logger";
@@ -618,6 +618,9 @@ describe("megolm", function() {
aliceTestClient.httpBackend.when(
'PUT', '/sendToDevice/org.matrix.room_key.withheld/',
).respond(200, {});
aliceTestClient.httpBackend.when(
'PUT', '/sendToDevice/m.room_key.withheld/',
).respond(200, {});
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
@@ -718,6 +721,9 @@ describe("megolm", function() {
aliceTestClient.httpBackend.when(
'PUT', '/sendToDevice/org.matrix.room_key.withheld/',
).respond(200, {});
aliceTestClient.httpBackend.when(
'PUT', '/sendToDevice/m.room_key.withheld/',
).respond(200, {});
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test2'),
-370
View File
@@ -1,370 +0,0 @@
// load olm before the sdk if possible
import './olm-loader';
import { logger } from '../src/logger';
import { MatrixEvent } from "../src/models/event";
/**
* Return a promise that is resolved when the client next emits a
* SYNCING event.
* @param {Object} client The client
* @param {Number=} count Number of syncs to wait for (default 1)
* @return {Promise} Resolves once the client has emitted a SYNCING event
*/
export function syncPromise(client, count) {
if (count === undefined) {
count = 1;
}
if (count <= 0) {
return Promise.resolve();
}
const p = new Promise((resolve, reject) => {
const cb = (state) => {
logger.log(`${Date.now()} syncPromise(${count}): ${state}`);
if (state === 'SYNCING') {
resolve();
} else {
client.once('sync', cb);
}
};
client.once('sync', cb);
});
return p.then(() => {
return syncPromise(client, count-1);
});
}
/**
* Create a spy for an object and automatically spy its methods.
* @param {*} constr The class constructor (used with 'new')
* @param {string} name The name of the class
* @return {Object} An instantiated object with spied methods/properties.
*/
export function mock(constr, name) {
// Based on
// http://eclipsesource.com/blogs/2014/03/27/mocks-in-jasmine-tests/
const HelperConstr = new Function(); // jshint ignore:line
HelperConstr.prototype = constr.prototype;
const result = new HelperConstr();
result.toString = function() {
return "mock" + (name ? " of " + name : "");
};
for (const key of Object.getOwnPropertyNames(constr.prototype)) { // eslint-disable-line guard-for-in
try {
if (constr.prototype[key] instanceof Function) {
result[key] = jest.fn();
}
} catch (ex) {
// Direct access to some non-function fields of DOM prototypes may
// cause exceptions.
// Overwriting will not work either in that case.
}
}
return result;
}
/**
* Create an Event.
* @param {Object} opts Values for the event.
* @param {string} opts.type The event.type
* @param {string} opts.room The event.room_id
* @param {string} opts.sender The event.sender
* @param {string} opts.skey Optional. The state key (auto inserts empty string)
* @param {Object} opts.content The event.content
* @param {boolean} opts.event True to make a MatrixEvent.
* @return {Object} a JSON object representing this event.
*/
export function mkEvent(opts) {
if (!opts.type || !opts.content) {
throw new Error("Missing .type or .content =>" + JSON.stringify(opts));
}
const event = {
type: opts.type,
room_id: opts.room,
sender: opts.sender || opts.user, // opts.user for backwards-compat
content: opts.content,
unsigned: opts.unsigned,
event_id: "$" + Math.random() + "-" + Math.random(),
};
if (opts.skey !== undefined) {
event.state_key = opts.skey;
} else if (["m.room.name", "m.room.topic", "m.room.create", "m.room.join_rules",
"m.room.power_levels", "m.room.topic",
"com.example.state"].includes(opts.type)) {
event.state_key = "";
}
return opts.event ? new MatrixEvent(event) : event;
}
/**
* Create an m.presence event.
* @param {Object} opts Values for the presence.
* @return {Object|MatrixEvent} The event
*/
export function mkPresence(opts) {
if (!opts.user) {
throw new Error("Missing user");
}
const event = {
event_id: "$" + Math.random() + "-" + Math.random(),
type: "m.presence",
sender: opts.sender || opts.user, // opts.user for backwards-compat
content: {
avatar_url: opts.url,
displayname: opts.name,
last_active_ago: opts.ago,
presence: opts.presence || "offline",
},
};
return opts.event ? new MatrixEvent(event) : event;
}
/**
* Create an m.room.member event.
* @param {Object} opts Values for the membership.
* @param {string} opts.room The room ID for the event.
* @param {string} opts.mship The content.membership for the event.
* @param {string} opts.sender The sender user ID for the event.
* @param {string} opts.skey The target user ID for the event if applicable
* e.g. for invites/bans.
* @param {string} opts.name The content.displayname for the event.
* @param {string} opts.url The content.avatar_url for the event.
* @param {boolean} opts.event True to make a MatrixEvent.
* @return {Object|MatrixEvent} The event
*/
export function mkMembership(opts) {
opts.type = "m.room.member";
if (!opts.skey) {
opts.skey = opts.sender || opts.user;
}
if (!opts.mship) {
throw new Error("Missing .mship => " + JSON.stringify(opts));
}
opts.content = {
membership: opts.mship,
};
if (opts.name) {
opts.content.displayname = opts.name;
}
if (opts.url) {
opts.content.avatar_url = opts.url;
}
return mkEvent(opts);
}
/**
* Create an m.room.message event.
* @param {Object} opts Values for the message
* @param {string} opts.room The room ID for the event.
* @param {string} opts.user The user ID for the event.
* @param {string} opts.msg Optional. The content.body for the event.
* @param {boolean} opts.event True to make a MatrixEvent.
* @return {Object|MatrixEvent} The event
*/
export function mkMessage(opts) {
opts.type = "m.room.message";
if (!opts.msg) {
opts.msg = "Random->" + Math.random();
}
if (!opts.room || !opts.user) {
throw new Error("Missing .room or .user from %s", opts);
}
opts.content = {
msgtype: "m.text",
body: opts.msg,
};
return mkEvent(opts);
}
/**
* A mock implementation of webstorage
*
* @constructor
*/
export function MockStorageApi() {
this.data = {};
}
MockStorageApi.prototype = {
get length() {
return Object.keys(this.data).length;
},
key: function(i) {
return Object.keys(this.data)[i];
},
setItem: function(k, v) {
this.data[k] = v;
},
getItem: function(k) {
return this.data[k] || null;
},
removeItem: function(k) {
delete this.data[k];
},
};
/**
* If an event is being decrypted, wait for it to finish being decrypted.
*
* @param {MatrixEvent} event
* @returns {Promise} promise which resolves (to `event`) when the event has been decrypted
*/
export function awaitDecryption(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`);
return new Promise((resolve, reject) => {
event.once('Event.decrypted', (ev) => {
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
resolve(ev);
});
});
}
}
export function HttpResponse(
httpLookups, acceptKeepalives, ignoreUnhandledSync,
) {
this.httpLookups = httpLookups;
this.acceptKeepalives = acceptKeepalives === undefined ? true : acceptKeepalives;
this.ignoreUnhandledSync = ignoreUnhandledSync;
this.pendingLookup = null;
}
HttpResponse.prototype.request = function(
cb, method, path, qp, data, prefix,
) {
if (path === HttpResponse.KEEP_ALIVE_PATH && this.acceptKeepalives) {
return Promise.resolve();
}
const next = this.httpLookups.shift();
const logLine = (
"MatrixClient[UT] RECV " + method + " " + path + " " +
"EXPECT " + (next ? next.method : next) + " " + (next ? next.path : next)
);
logger.log(logLine);
if (!next) { // no more things to return
if (method === "GET" && path === "/sync" && this.ignoreUnhandledSync) {
logger.log("MatrixClient[UT] Ignoring.");
return new Promise(() => {});
}
if (this.pendingLookup) {
if (this.pendingLookup.method === method
&& this.pendingLookup.path === path) {
return this.pendingLookup.promise;
}
// >1 pending thing, and they are different, whine.
expect(false).toBe(
true, ">1 pending request. You should probably handle them. " +
"PENDING: " + JSON.stringify(this.pendingLookup) + " JUST GOT: " +
method + " " + path,
);
}
this.pendingLookup = {
promise: new Promise(() => {}),
method: method,
path: path,
};
return this.pendingLookup.promise;
}
if (next.path === path && next.method === method) {
logger.log(
"MatrixClient[UT] Matched. Returning " +
(next.error ? "BAD" : "GOOD") + " response",
);
if (next.expectBody) {
expect(next.expectBody).toEqual(data);
}
if (next.expectQueryParams) {
Object.keys(next.expectQueryParams).forEach(function(k) {
expect(qp[k]).toEqual(next.expectQueryParams[k]);
});
}
if (next.thenCall) {
process.nextTick(next.thenCall, 0); // next tick so we return first.
}
if (next.error) {
return Promise.reject({
errcode: next.error.errcode,
httpStatus: next.error.httpStatus,
name: next.error.errcode,
message: "Expected testing error",
data: next.error,
});
}
return Promise.resolve(next.data);
} else if (method === "GET" && path === "/sync" && this.ignoreUnhandledSync) {
logger.log("MatrixClient[UT] Ignoring.");
this.httpLookups.unshift(next);
return new Promise(() => {});
}
expect(true).toBe(false, "Expected different request. " + logLine);
return new Promise(() => {});
};
HttpResponse.KEEP_ALIVE_PATH = "/_matrix/client/versions";
HttpResponse.PUSH_RULES_RESPONSE = {
method: "GET",
path: "/pushrules/",
data: {},
};
HttpResponse.USER_ID = "@alice:bar";
HttpResponse.filterResponse = function(userId) {
const filterPath = "/user/" + encodeURIComponent(userId) + "/filter";
return {
method: "POST",
path: filterPath,
data: { filter_id: "f1lt3r" },
};
};
HttpResponse.SYNC_DATA = {
next_batch: "s_5_3",
presence: { events: [] },
rooms: {},
};
HttpResponse.SYNC_RESPONSE = {
method: "GET",
path: "/sync",
data: HttpResponse.SYNC_DATA,
};
HttpResponse.CAPABILITIES_RESPONSE = {
method: "GET",
path: "/capabilities",
data: { capabilities: {} },
};
HttpResponse.defaultResponses = function(userId) {
return [
HttpResponse.CAPABILITIES_RESPONSE,
HttpResponse.PUSH_RULES_RESPONSE,
HttpResponse.filterResponse(userId),
HttpResponse.SYNC_RESPONSE,
];
};
export function setHttpResponses(
httpBackend, responses,
) {
responses.forEach(response => {
httpBackend
.when(response.method, response.path)
.respond(200, response.data);
});
}
export const emitPromise = (e, k) => new Promise(r => e.once(k, r));
+119
View File
@@ -0,0 +1,119 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixEvent } from "../../src";
import { M_BEACON, M_BEACON_INFO } from "../../src/@types/beacon";
import { LocationAssetType } from "../../src/@types/location";
import {
makeBeaconContent,
makeBeaconInfoContent,
} from "../../src/content-helpers";
type InfoContentProps = {
timeout: number;
isLive?: boolean;
assetType?: LocationAssetType;
description?: string;
};
const DEFAULT_INFO_CONTENT_PROPS: InfoContentProps = {
timeout: 3600000,
};
/**
* Create an m.beacon_info event
* all required properties are mocked
* override with contentProps
*/
export const makeBeaconInfoEvent = (
sender: string,
roomId: string,
contentProps: Partial<InfoContentProps> = {},
eventId?: string,
eventTypeSuffix?: string,
): MatrixEvent => {
const {
timeout, isLive, description, assetType,
} = {
...DEFAULT_INFO_CONTENT_PROPS,
...contentProps,
};
const event = new MatrixEvent({
type: `${M_BEACON_INFO.name}.${sender}.${eventTypeSuffix || Date.now()}`,
room_id: roomId,
state_key: sender,
content: makeBeaconInfoContent(timeout, isLive, description, assetType),
});
// live beacons use the beacon_info event id
// set or default this
event.replaceLocalEventId(eventId || `$${Math.random()}-${Math.random()}`);
return event;
};
type ContentProps = {
uri: string;
timestamp: number;
beaconInfoId: string;
description?: string;
};
const DEFAULT_CONTENT_PROPS: ContentProps = {
uri: 'geo:-36.24484561954707,175.46884959563613;u=10',
timestamp: 123,
beaconInfoId: '$123',
};
/**
* Create an m.beacon event
* all required properties are mocked
* override with contentProps
*/
export const makeBeaconEvent = (
sender: string,
contentProps: Partial<ContentProps> = {},
): MatrixEvent => {
const { uri, timestamp, beaconInfoId, description } = {
...DEFAULT_CONTENT_PROPS,
...contentProps,
};
return new MatrixEvent({
type: M_BEACON.name,
sender,
content: makeBeaconContent(uri, timestamp, beaconInfoId, description),
});
};
/**
* Create a mock geolocation position
* defaults all required properties
*/
export const makeGeolocationPosition = (
{ timestamp, coords }:
{ timestamp?: number, coords: Partial<GeolocationCoordinates> },
): GeolocationPosition => ({
timestamp: timestamp ?? 1647256791840,
coords: {
accuracy: 1,
latitude: 54.001927,
longitude: -8.253491,
altitude: null,
altitudeAccuracy: null,
heading: null,
speed: null,
...coords,
},
});
+28
View File
@@ -0,0 +1,28 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Filter emitter.emit mock calls to find relevant events
* eg:
* ```
* const emitSpy = jest.spyOn(state, 'emit');
* << actions >>
* const beaconLivenessEmits = emitCallsByEventType(BeaconEvent.New, emitSpy);
* expect(beaconLivenessEmits.length).toBe(1);
* ```
*/
export const filterEmitCallsByEventType = (eventType: string, spy: jest.SpyInstance<any, unknown[]>) =>
spy.mock.calls.filter((args) => args[0] === eventType);
+282
View File
@@ -0,0 +1,282 @@
// eslint-disable-next-line no-restricted-imports
import EventEmitter from "events";
// load olm before the sdk if possible
import '../olm-loader';
import { logger } from '../../src/logger';
import { IContent, IEvent, IUnsigned, MatrixEvent, MatrixEventEvent } from "../../src/models/event";
import { ClientEvent, EventType, MatrixClient } from "../../src";
import { SyncState } from "../../src/sync";
/**
* Return a promise that is resolved when the client next emits a
* SYNCING event.
* @param {Object} client The client
* @param {Number=} count Number of syncs to wait for (default 1)
* @return {Promise} Resolves once the client has emitted a SYNCING event
*/
export function syncPromise(client: MatrixClient, count = 1): Promise<void> {
if (count <= 0) {
return Promise.resolve();
}
const p = new Promise<void>((resolve) => {
const cb = (state: SyncState) => {
logger.log(`${Date.now()} syncPromise(${count}): ${state}`);
if (state === SyncState.Syncing) {
resolve();
} else {
client.once(ClientEvent.Sync, cb);
}
};
client.once(ClientEvent.Sync, cb);
});
return p.then(() => {
return syncPromise(client, count - 1);
});
}
/**
* Create a spy for an object and automatically spy its methods.
* @param {*} constr The class constructor (used with 'new')
* @param {string} name The name of the class
* @return {Object} An instantiated object with spied methods/properties.
*/
export function mock<T>(constr: { new(...args: any[]): T }, name: string): T {
// Based on http://eclipsesource.com/blogs/2014/03/27/mocks-in-jasmine-tests/
const HelperConstr = new Function(); // jshint ignore:line
HelperConstr.prototype = constr.prototype;
// @ts-ignore
const result = new HelperConstr();
result.toString = function() {
return "mock" + (name ? " of " + name : "");
};
for (const key of Object.getOwnPropertyNames(constr.prototype)) { // eslint-disable-line guard-for-in
try {
if (constr.prototype[key] instanceof Function) {
result[key] = jest.fn();
}
} catch (ex) {
// Direct access to some non-function fields of DOM prototypes may
// cause exceptions.
// Overwriting will not work either in that case.
}
}
return result;
}
interface IEventOpts {
type: EventType | string;
room: string;
sender?: string;
skey?: string;
content: IContent;
event?: boolean;
user?: string;
unsigned?: IUnsigned;
redacts?: string;
}
/**
* Create an Event.
* @param {Object} opts Values for the event.
* @param {string} opts.type The event.type
* @param {string} opts.room The event.room_id
* @param {string} opts.sender The event.sender
* @param {string} opts.skey Optional. The state key (auto inserts empty string)
* @param {Object} opts.content The event.content
* @param {boolean} opts.event True to make a MatrixEvent.
* @return {Object} a JSON object representing this event.
*/
export function mkEvent(opts: IEventOpts): object | MatrixEvent {
if (!opts.type || !opts.content) {
throw new Error("Missing .type or .content =>" + JSON.stringify(opts));
}
const event: Partial<IEvent> = {
type: opts.type as string,
room_id: opts.room,
sender: opts.sender || opts.user, // opts.user for backwards-compat
content: opts.content,
unsigned: opts.unsigned || {},
event_id: "$" + Math.random() + "-" + Math.random(),
redacts: opts.redacts,
};
if (opts.skey !== undefined) {
event.state_key = opts.skey;
} else if ([
EventType.RoomName,
EventType.RoomTopic,
EventType.RoomCreate,
EventType.RoomJoinRules,
EventType.RoomPowerLevels,
EventType.RoomTopic,
"com.example.state",
].includes(opts.type)) {
event.state_key = "";
}
return opts.event ? new MatrixEvent(event) : event;
}
interface IPresenceOpts {
user?: string;
sender?: string;
url: string;
name: string;
ago: number;
presence?: string;
event?: boolean;
}
/**
* Create an m.presence event.
* @param {Object} opts Values for the presence.
* @return {Object|MatrixEvent} The event
*/
export function mkPresence(opts: IPresenceOpts): object | MatrixEvent {
const event = {
event_id: "$" + Math.random() + "-" + Math.random(),
type: "m.presence",
sender: opts.sender || opts.user, // opts.user for backwards-compat
content: {
avatar_url: opts.url,
displayname: opts.name,
last_active_ago: opts.ago,
presence: opts.presence || "offline",
},
};
return opts.event ? new MatrixEvent(event) : event;
}
interface IMembershipOpts {
room: string;
mship: string;
sender?: string;
user?: string;
skey?: string;
name?: string;
url?: string;
event?: boolean;
}
/**
* Create an m.room.member event.
* @param {Object} opts Values for the membership.
* @param {string} opts.room The room ID for the event.
* @param {string} opts.mship The content.membership for the event.
* @param {string} opts.sender The sender user ID for the event.
* @param {string} opts.skey The target user ID for the event if applicable
* e.g. for invites/bans.
* @param {string} opts.name The content.displayname for the event.
* @param {string} opts.url The content.avatar_url for the event.
* @param {boolean} opts.event True to make a MatrixEvent.
* @return {Object|MatrixEvent} The event
*/
export function mkMembership(opts: IMembershipOpts): object | MatrixEvent {
const eventOpts: IEventOpts = {
...opts,
type: EventType.RoomMember,
content: {
membership: opts.mship,
},
};
if (!opts.skey) {
eventOpts.skey = opts.sender || opts.user;
}
if (opts.name) {
eventOpts.content.displayname = opts.name;
}
if (opts.url) {
eventOpts.content.avatar_url = opts.url;
}
return mkEvent(eventOpts);
}
interface IMessageOpts {
room: string;
user: string;
msg?: string;
event?: boolean;
}
/**
* Create an m.room.message event.
* @param {Object} opts Values for the message
* @param {string} opts.room The room ID for the event.
* @param {string} opts.user The user ID for the event.
* @param {string} opts.msg Optional. The content.body for the event.
* @param {boolean} opts.event True to make a MatrixEvent.
* @return {Object|MatrixEvent} The event
*/
export function mkMessage(opts: IMessageOpts): object | MatrixEvent {
const eventOpts: IEventOpts = {
...opts,
type: EventType.RoomMessage,
content: {
msgtype: "m.text",
body: opts.msg,
},
};
if (!eventOpts.content.body) {
eventOpts.content.body = "Random->" + Math.random();
}
return mkEvent(eventOpts);
}
/**
* A mock implementation of webstorage
*
* @constructor
*/
export class MockStorageApi {
private data: Record<string, any> = {};
public get length() {
return Object.keys(this.data).length;
}
public key(i: number): any {
return Object.keys(this.data)[i];
}
public setItem(k: string, v: any): void {
this.data[k] = v;
}
public getItem(k: string): any {
return this.data[k] || null;
}
public removeItem(k: string): void {
delete this.data[k];
}
}
/**
* If an event is being decrypted, wait for it to finish being decrypted.
*
* @param {MatrixEvent} event
* @returns {Promise} promise which resolves (to `event`) when the event has been decrypted
*/
export async function awaitDecryption(event: MatrixEvent): Promise<MatrixEvent> {
// 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`);
return new Promise((resolve) => {
event.once(MatrixEventEvent.Decrypted, (ev) => {
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
resolve(ev);
});
});
}
}
export const emitPromise = (e: EventEmitter, k: string): Promise<any> => new Promise(r => e.once(k, r));
+127
View File
@@ -0,0 +1,127 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { REFERENCE_RELATION } from "matrix-events-sdk";
import { M_BEACON_INFO } from "../../src/@types/beacon";
import { LocationAssetType, M_ASSET, M_LOCATION, M_TIMESTAMP } from "../../src/@types/location";
import { makeBeaconContent, makeBeaconInfoContent } from "../../src/content-helpers";
describe('Beacon content helpers', () => {
describe('makeBeaconInfoContent()', () => {
const mockDateNow = 123456789;
beforeEach(() => {
jest.spyOn(global.Date, 'now').mockReturnValue(mockDateNow);
});
afterAll(() => {
jest.spyOn(global.Date, 'now').mockRestore();
});
it('create fully defined event content', () => {
expect(makeBeaconInfoContent(
1234,
true,
'nice beacon_info',
LocationAssetType.Pin,
)).toEqual({
[M_BEACON_INFO.name]: {
description: 'nice beacon_info',
timeout: 1234,
live: true,
},
[M_TIMESTAMP.name]: mockDateNow,
[M_ASSET.name]: {
type: LocationAssetType.Pin,
},
});
});
it('defaults timestamp to current time', () => {
expect(makeBeaconInfoContent(
1234,
true,
'nice beacon_info',
LocationAssetType.Pin,
)).toEqual(expect.objectContaining({
[M_TIMESTAMP.name]: mockDateNow,
}));
});
it('uses timestamp when provided', () => {
expect(makeBeaconInfoContent(
1234,
true,
'nice beacon_info',
LocationAssetType.Pin,
99999,
)).toEqual(expect.objectContaining({
[M_TIMESTAMP.name]: 99999,
}));
});
it('defaults asset type to self when not set', () => {
expect(makeBeaconInfoContent(
1234,
true,
'nice beacon_info',
// no assetType passed
)).toEqual(expect.objectContaining({
[M_ASSET.name]: {
type: LocationAssetType.Self,
},
}));
});
});
describe('makeBeaconContent()', () => {
it('creates event content without description', () => {
expect(makeBeaconContent(
'geo:foo',
123,
'$1234',
// no description
)).toEqual({
[M_LOCATION.name]: {
description: undefined,
uri: 'geo:foo',
},
[M_TIMESTAMP.name]: 123,
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: '$1234',
},
});
});
it('creates event content with description', () => {
expect(makeBeaconContent(
'geo:foo',
123,
'$1234',
'test description',
)).toEqual({
[M_LOCATION.name]: {
description: 'test description',
uri: 'geo:foo',
},
[M_TIMESTAMP.name]: 123,
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: '$1234',
},
});
});
});
});
+25
View File
@@ -13,6 +13,7 @@ import * as olmlib from "../../src/crypto/olmlib";
import { sleep } from "../../src/utils";
import { CRYPTO_ENABLED } from "../../src/client";
import { DeviceInfo } from "../../src/crypto/deviceinfo";
import { logger } from '../../src/logger';
const Olm = global.Olm;
@@ -400,4 +401,28 @@ describe("Crypto", function() {
expect(aliceClient.sendToDevice.mock.calls[2][2]).not.toBe(txnId);
});
});
describe('Secret storage', function() {
it("creates secret storage even if there is no keyInfo", async function() {
jest.spyOn(logger, 'log').mockImplementation(() => {});
jest.setTimeout(10000);
const client = (new TestClient("@a:example.com", "dev")).client;
await client.initCrypto();
client.crypto.getSecretStorageKey = async () => null;
client.crypto.isCrossSigningReady = async () => false;
client.crypto.baseApis.uploadDeviceSigningKeys = () => null;
client.crypto.baseApis.setAccountData = () => null;
client.crypto.baseApis.uploadKeySignatures = () => null;
client.crypto.baseApis.http.authedRequest = () => null;
const createSecretStorageKey = async () => {
return {
keyInfo: undefined, // Returning undefined here used to cause a crash
privateKey: Uint8Array.of(32, 33),
};
};
await client.crypto.bootstrapSecretStorage({
createSecretStorageKey,
});
});
});
});
+66 -7
View File
@@ -2,7 +2,7 @@ import '../../../olm-loader';
import * as algorithms from "../../../../src/crypto/algorithms";
import { MemoryCryptoStore } from "../../../../src/crypto/store/memory-crypto-store";
import { MockStorageApi } from "../../../MockStorageApi";
import * as testUtils from "../../../test-utils";
import * as testUtils from "../../../test-utils/test-utils";
import { OlmDevice } from "../../../../src/crypto/OlmDevice";
import { Crypto } from "../../../../src/crypto";
import { logger } from "../../../../src/logger";
@@ -462,7 +462,7 @@ describe("MegolmDecryption", function() {
let run = false;
aliceClient.sendToDevice = async (msgtype, contentMap) => {
run = true;
expect(msgtype).toBe("org.matrix.room_key.withheld");
expect(msgtype).toMatch(/^(org.matrix|m).room_key.withheld$/);
delete contentMap["@bob:example.com"].bobdevice1.session_id;
delete contentMap["@bob:example.com"].bobdevice2.session_id;
expect(contentMap).toStrictEqual({
@@ -572,7 +572,7 @@ describe("MegolmDecryption", function() {
const sendPromise = new Promise((resolve, reject) => {
aliceClient.sendToDevice = async (msgtype, contentMap) => {
expect(msgtype).toBe("org.matrix.room_key.withheld");
expect(msgtype).toMatch(/^(org.matrix|m).room_key.withheld$/);
expect(contentMap).toStrictEqual({
'@bob:example.com': {
bobdevice: {
@@ -619,7 +619,7 @@ describe("MegolmDecryption", function() {
content: {
algorithm: "m.megolm.v1.aes-sha2",
room_id: roomId,
session_id: "session_id",
session_id: "session_id1",
sender_key: bobDevice.deviceCurve25519Key,
code: "m.blacklisted",
reason: "You have been blocked",
@@ -636,7 +636,34 @@ describe("MegolmDecryption", function() {
ciphertext: "blablabla",
device_id: "bobdevice",
sender_key: bobDevice.deviceCurve25519Key,
session_id: "session_id",
session_id: "session_id1",
},
}))).rejects.toThrow("The sender has blocked you.");
aliceClient.crypto.onToDeviceEvent(new MatrixEvent({
type: "m.room_key.withheld",
sender: "@bob:example.com",
content: {
algorithm: "m.megolm.v1.aes-sha2",
room_id: roomId,
session_id: "session_id2",
sender_key: bobDevice.deviceCurve25519Key,
code: "m.blacklisted",
reason: "You have been blocked",
},
}));
await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
type: "m.room.encrypted",
sender: "@bob:example.com",
event_id: "$event",
room_id: roomId,
content: {
algorithm: "m.megolm.v1.aes-sha2",
ciphertext: "blablabla",
device_id: "bobdevice",
sender_key: bobDevice.deviceCurve25519Key,
session_id: "session_id2",
},
}))).rejects.toThrow("The sender has blocked you.");
});
@@ -665,7 +692,7 @@ describe("MegolmDecryption", function() {
content: {
algorithm: "m.megolm.v1.aes-sha2",
room_id: roomId,
session_id: "session_id",
session_id: "session_id1",
sender_key: bobDevice.deviceCurve25519Key,
code: "m.no_olm",
reason: "Unable to establish a secure channel.",
@@ -686,7 +713,39 @@ describe("MegolmDecryption", function() {
ciphertext: "blablabla",
device_id: "bobdevice",
sender_key: bobDevice.deviceCurve25519Key,
session_id: "session_id",
session_id: "session_id1",
},
origin_server_ts: now,
}))).rejects.toThrow("The sender was unable to establish a secure channel.");
aliceClient.crypto.onToDeviceEvent(new MatrixEvent({
type: "m.room_key.withheld",
sender: "@bob:example.com",
content: {
algorithm: "m.megolm.v1.aes-sha2",
room_id: roomId,
session_id: "session_id2",
sender_key: bobDevice.deviceCurve25519Key,
code: "m.no_olm",
reason: "Unable to establish a secure channel.",
},
}));
await new Promise((resolve) => {
setTimeout(resolve, 100);
});
await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
type: "m.room.encrypted",
sender: "@bob:example.com",
event_id: "$event",
room_id: roomId,
content: {
algorithm: "m.megolm.v1.aes-sha2",
ciphertext: "blablabla",
device_id: "bobdevice",
sender_key: bobDevice.deviceCurve25519Key,
session_id: "session_id2",
},
origin_server_ts: now,
}))).rejects.toThrow("The sender was unable to establish a secure channel.");
+1 -1
View File
@@ -24,7 +24,7 @@ import * as algorithms from "../../../src/crypto/algorithms";
import { WebStorageSessionStore } from "../../../src/store/session/webstorage";
import { MemoryCryptoStore } from "../../../src/crypto/store/memory-crypto-store";
import { MockStorageApi } from "../../MockStorageApi";
import * as testUtils from "../../test-utils";
import * as testUtils from "../../test-utils/test-utils";
import { OlmDevice } from "../../../src/crypto/OlmDevice";
import { Crypto } from "../../../src/crypto";
import { resetCrossSigningKeys } from "./crypto-utils";
+27 -7
View File
@@ -20,11 +20,33 @@ import anotherjson from 'another-json';
import * as olmlib from "../../../src/crypto/olmlib";
import { TestClient } from '../../TestClient';
import { HttpResponse, setHttpResponses } from '../../test-utils';
import { resetCrossSigningKeys } from "./crypto-utils";
import { MatrixError } from '../../../src/http-api';
import { logger } from '../../../src/logger';
const PUSH_RULES_RESPONSE = {
method: "GET",
path: "/pushrules/",
data: {},
};
const filterResponse = function(userId) {
const filterPath = "/user/" + encodeURIComponent(userId) + "/filter";
return {
method: "POST",
path: filterPath,
data: { filter_id: "f1lt3r" },
};
};
function setHttpResponses(httpBackend, responses) {
responses.forEach(response => {
httpBackend
.when(response.method, response.path)
.respond(200, response.data);
});
}
async function makeTestClient(userInfo, options, keys) {
if (!keys) keys = {};
@@ -237,8 +259,7 @@ describe("Cross Signing", function() {
// feed sync result that includes master key, ssk, device key
const responses = [
HttpResponse.CAPABILITIES_RESPONSE,
HttpResponse.PUSH_RULES_RESPONSE,
PUSH_RULES_RESPONSE,
{
method: "POST",
path: "/keys/upload",
@@ -249,7 +270,7 @@ describe("Cross Signing", function() {
},
},
},
HttpResponse.filterResponse("@alice:example.com"),
filterResponse("@alice:example.com"),
{
method: "GET",
path: "/sync",
@@ -494,8 +515,7 @@ describe("Cross Signing", function() {
// - master key signed by her usk (pretend that it was signed by another
// of Alice's devices)
const responses = [
HttpResponse.CAPABILITIES_RESPONSE,
HttpResponse.PUSH_RULES_RESPONSE,
PUSH_RULES_RESPONSE,
{
method: "POST",
path: "/keys/upload",
@@ -506,7 +526,7 @@ describe("Cross Signing", function() {
},
},
},
HttpResponse.filterResponse("@alice:example.com"),
filterResponse("@alice:example.com"),
{
method: "GET",
path: "/sync",
+180
View File
@@ -0,0 +1,180 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixClient, MatrixEvent, MatrixEventEvent, MatrixScheduler, Room } from "../../src";
import { eventMapperFor } from "../../src/event-mapper";
import { IStore } from "../../src/store";
describe("eventMapperFor", function() {
let rooms: Room[] = [];
const userId = "@test:example.org";
let client: MatrixClient;
beforeEach(() => {
client = new MatrixClient({
baseUrl: "https://my.home.server",
accessToken: "my.access.token",
request: function() {} as any, // NOP
store: {
getRoom(roomId: string): Room | null {
return rooms.find(r => r.roomId === roomId);
},
} as IStore,
scheduler: {
setProcessFunction: jest.fn(),
} as unknown as MatrixScheduler,
userId: userId,
});
rooms = [];
});
it("should de-duplicate MatrixEvent instances by means of findEventById on the room object", async () => {
const roomId = "!room:example.org";
const room = new Room(roomId, client, userId);
rooms.push(room);
const mapper = eventMapperFor(client, {
preventReEmit: true,
decrypt: false,
});
const eventId = "$event1:server";
const eventDefinition = {
type: "m.room.message",
room_id: roomId,
sender: userId,
content: {
body: "body",
},
unsigned: {},
event_id: eventId,
};
const event = mapper(eventDefinition);
expect(event).toBeInstanceOf(MatrixEvent);
room.addLiveEvents([event]);
expect(room.findEventById(eventId)).toBe(event);
const event2 = mapper(eventDefinition);
expect(event).toBe(event2);
});
it("should not de-duplicate state events due to directionality of sentinel members", async () => {
const roomId = "!room:example.org";
const room = new Room(roomId, client, userId);
rooms.push(room);
const mapper = eventMapperFor(client, {
preventReEmit: true,
decrypt: false,
});
const eventId = "$event1:server";
const eventDefinition = {
type: "m.room.name",
room_id: roomId,
sender: userId,
content: {
name: "Room name",
},
unsigned: {},
event_id: eventId,
state_key: "",
};
const event = mapper(eventDefinition);
expect(event).toBeInstanceOf(MatrixEvent);
room.oldState.setStateEvents([event]);
room.currentState.setStateEvents([event]);
room.addLiveEvents([event]);
expect(room.findEventById(eventId)).toBe(event);
const event2 = mapper(eventDefinition);
expect(event).not.toBe(event2);
});
it("should decrypt appropriately", async () => {
const roomId = "!room:example.org";
const room = new Room(roomId, client, userId);
rooms.push(room);
const eventId = "$event1:server";
const eventDefinition = {
type: "m.room.encrypted",
room_id: roomId,
sender: userId,
content: {
ciphertext: "",
},
unsigned: {},
event_id: eventId,
};
const decryptEventIfNeededSpy = jest.spyOn(client, "decryptEventIfNeeded");
decryptEventIfNeededSpy.mockResolvedValue(); // stub it out
const mapper = eventMapperFor(client, {
decrypt: true,
});
const event = mapper(eventDefinition);
expect(event).toBeInstanceOf(MatrixEvent);
expect(decryptEventIfNeededSpy).toHaveBeenCalledWith(event);
});
it("should configure re-emitter appropriately", async () => {
const roomId = "!room:example.org";
const room = new Room(roomId, client, userId);
rooms.push(room);
const eventId = "$event1:server";
const eventDefinition = {
type: "m.room.message",
room_id: roomId,
sender: userId,
content: {
body: "body",
},
unsigned: {},
event_id: eventId,
};
const evListener = jest.fn();
client.on(MatrixEventEvent.Replaced, evListener);
const noReEmitMapper = eventMapperFor(client, {
preventReEmit: true,
});
const event1 = noReEmitMapper(eventDefinition);
expect(event1).toBeInstanceOf(MatrixEvent);
event1.emit(MatrixEventEvent.Replaced, event1);
expect(evListener).not.toHaveBeenCalled();
const reEmitMapper = eventMapperFor(client, {
preventReEmit: false,
});
const event2 = reEmitMapper(eventDefinition);
expect(event2).toBeInstanceOf(MatrixEvent);
event2.emit(MatrixEventEvent.Replaced, event2);
expect(evListener.mock.calls[0][0]).toEqual(event2);
expect(event1).not.toBe(event2); // the event wasn't added to a room so de-duplication wouldn't occur
});
});
+1 -1
View File
@@ -1,4 +1,4 @@
import * as utils from "../test-utils";
import * as utils from "../test-utils/test-utils";
import { EventTimeline } from "../../src/models/event-timeline";
import { RoomState } from "../../src/models/room-state";
+1 -1
View File
@@ -1,6 +1,6 @@
/*
Copyright 2017 New Vector Ltd
Copyright 2019 The Matrix.org Foundaction C.I.C.
Copyright 2019 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.
+54 -16
View File
@@ -1,10 +1,9 @@
import {
MatrixEvent,
RelationType,
UNSTABLE_FILTER_RELATED_BY_REL_TYPES,
UNSTABLE_FILTER_RELATED_BY_SENDERS,
} from "../../src";
import { FilterComponent } from "../../src/filter-component";
import { mkEvent } from '../test-utils';
import { mkEvent } from '../test-utils/test-utils';
describe("Filter Component", function() {
describe("types", function() {
@@ -15,7 +14,7 @@ describe("Filter Component", function() {
content: { },
room: 'roomId',
event: true,
});
}) as MatrixEvent;
const checkResult = filter.check(event);
@@ -29,7 +28,7 @@ describe("Filter Component", function() {
content: { },
room: 'roomId',
event: true,
});
}) as MatrixEvent;
const checkResult = filter.check(event);
@@ -39,7 +38,7 @@ describe("Filter Component", function() {
it("should filter out events by relation participation", function() {
const currentUserId = '@me:server.org';
const filter = new FilterComponent({
[UNSTABLE_FILTER_RELATED_BY_SENDERS.name]: [currentUserId],
related_by_senders: [currentUserId],
}, currentUserId);
const threadRootNotParticipated = mkEvent({
@@ -50,13 +49,13 @@ describe("Filter Component", function() {
event: true,
unsigned: {
"m.relations": {
[RelationType.Thread]: {
"m.thread": {
count: 2,
current_user_participated: false,
},
},
},
});
}) as MatrixEvent;
expect(filter.check(threadRootNotParticipated)).toBe(false);
});
@@ -64,7 +63,7 @@ describe("Filter Component", function() {
it("should keep events by relation participation", function() {
const currentUserId = '@me:server.org';
const filter = new FilterComponent({
[UNSTABLE_FILTER_RELATED_BY_SENDERS.name]: [currentUserId],
related_by_senders: [currentUserId],
}, currentUserId);
const threadRootParticipated = mkEvent({
@@ -72,7 +71,7 @@ describe("Filter Component", function() {
content: {},
unsigned: {
"m.relations": {
[RelationType.Thread]: {
"m.thread": {
count: 2,
current_user_participated: true,
},
@@ -81,14 +80,14 @@ describe("Filter Component", function() {
user: '@someone-else:server.org',
room: 'roomId',
event: true,
});
}) as MatrixEvent;
expect(filter.check(threadRootParticipated)).toBe(true);
});
it("should filter out events by relation type", function() {
const filter = new FilterComponent({
[UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name]: [RelationType.Thread],
related_by_rel_types: ["m.thread"],
});
const referenceRelationEvent = mkEvent({
@@ -101,14 +100,14 @@ describe("Filter Component", function() {
[RelationType.Reference]: {},
},
},
});
}) as MatrixEvent;
expect(filter.check(referenceRelationEvent)).toBe(false);
});
it("should keep events by relation type", function() {
const filter = new FilterComponent({
[UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name]: [RelationType.Thread],
related_by_rel_types: ["m.thread"],
});
const threadRootEvent = mkEvent({
@@ -116,7 +115,7 @@ describe("Filter Component", function() {
content: {},
unsigned: {
"m.relations": {
[RelationType.Thread]: {
"m.thread": {
count: 2,
current_user_participated: true,
},
@@ -124,9 +123,48 @@ describe("Filter Component", function() {
},
room: 'roomId',
event: true,
});
}) as MatrixEvent;
const eventWithMultipleRelations = mkEvent({
"type": "m.room.message",
"content": {},
"unsigned": {
"m.relations": {
"testtesttest": {},
"m.annotation": {
"chunk": [
{
"type": "m.reaction",
"key": "🤫",
"count": 1,
},
],
},
"m.thread": {
count: 2,
current_user_participated: true,
},
},
},
"room": 'roomId',
"event": true,
}) as MatrixEvent;
const noMatchEvent = mkEvent({
"type": "m.room.message",
"content": {},
"unsigned": {
"m.relations": {
"testtesttest": {},
},
},
"room": 'roomId',
"event": true,
}) as MatrixEvent;
expect(filter.check(threadRootEvent)).toBe(true);
expect(filter.check(eventWithMultipleRelations)).toBe(true);
expect(filter.check(noMatchEvent)).toBe(false);
});
});
});
+73 -18
View File
@@ -14,43 +14,98 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { makeLocationContent } from "../../src/content-helpers";
import { makeLocationContent, parseLocationEvent } from "../../src/content-helpers";
import {
ASSET_NODE_TYPE,
M_ASSET,
LocationAssetType,
LOCATION_EVENT_TYPE,
TIMESTAMP_NODE_TYPE,
M_LOCATION,
M_TIMESTAMP,
LocationEventWireContent,
} from "../../src/@types/location";
import { TEXT_NODE_TYPE } from "../../src/@types/extensible_events";
import { MsgType } from "../../src/@types/event";
describe("Location", function() {
const defaultContent = {
"body": "Location geo:-36.24484561954707,175.46884959563613;u=10 at 2022-03-09T11:01:52.443Z",
"msgtype": "m.location",
"geo_uri": "geo:-36.24484561954707,175.46884959563613;u=10",
[M_LOCATION.name]: { "uri": "geo:-36.24484561954707,175.46884959563613;u=10", "description": null },
[M_ASSET.name]: { "type": "m.self" },
[TEXT_NODE_TYPE.name]: "Location geo:-36.24484561954707,175.46884959563613;u=10 at 2022-03-09T11:01:52.443Z",
[M_TIMESTAMP.name]: 1646823712443,
} as any;
const backwardsCompatibleEventContent = { ...defaultContent };
// eslint-disable-next-line camelcase
const { body, msgtype, geo_uri, ...modernProperties } = defaultContent;
const modernEventContent = { ...modernProperties };
const legacyEventContent = {
// eslint-disable-next-line camelcase
body, msgtype, geo_uri,
} as LocationEventWireContent;
it("should create a valid location with defaults", function() {
const loc = makeLocationContent("txt", "geo:foo", 134235435);
expect(loc.body).toEqual("txt");
expect(loc.msgtype).toEqual("m.location");
const loc = makeLocationContent(undefined, "geo:foo", 134235435);
expect(loc.body).toEqual('User Location geo:foo at 1970-01-02T13:17:15.435Z');
expect(loc.msgtype).toEqual(MsgType.Location);
expect(loc.geo_uri).toEqual("geo:foo");
expect(LOCATION_EVENT_TYPE.findIn(loc)).toEqual({
expect(M_LOCATION.findIn(loc)).toEqual({
uri: "geo:foo",
description: undefined,
});
expect(ASSET_NODE_TYPE.findIn(loc)).toEqual({ type: LocationAssetType.Self });
expect(TEXT_NODE_TYPE.findIn(loc)).toEqual("txt");
expect(TIMESTAMP_NODE_TYPE.findIn(loc)).toEqual(134235435);
expect(M_ASSET.findIn(loc)).toEqual({ type: LocationAssetType.Self });
expect(TEXT_NODE_TYPE.findIn(loc)).toEqual('User Location geo:foo at 1970-01-02T13:17:15.435Z');
expect(M_TIMESTAMP.findIn(loc)).toEqual(134235435);
});
it("should create a valid location with explicit properties", function() {
const loc = makeLocationContent(
"txxt", "geo:bar", 134235436, "desc", LocationAssetType.Pin);
undefined, "geo:bar", 134235436, "desc", LocationAssetType.Pin);
expect(loc.body).toEqual("txxt");
expect(loc.msgtype).toEqual("m.location");
expect(loc.body).toEqual('Location "desc" geo:bar at 1970-01-02T13:17:15.436Z');
expect(loc.msgtype).toEqual(MsgType.Location);
expect(loc.geo_uri).toEqual("geo:bar");
expect(LOCATION_EVENT_TYPE.findIn(loc)).toEqual({
expect(M_LOCATION.findIn(loc)).toEqual({
uri: "geo:bar",
description: "desc",
});
expect(ASSET_NODE_TYPE.findIn(loc)).toEqual({ type: LocationAssetType.Pin });
expect(TEXT_NODE_TYPE.findIn(loc)).toEqual("txxt");
expect(TIMESTAMP_NODE_TYPE.findIn(loc)).toEqual(134235436);
expect(M_ASSET.findIn(loc)).toEqual({ type: LocationAssetType.Pin });
expect(TEXT_NODE_TYPE.findIn(loc)).toEqual('Location "desc" geo:bar at 1970-01-02T13:17:15.436Z');
expect(M_TIMESTAMP.findIn(loc)).toEqual(134235436);
});
it('parses backwards compatible event correctly', () => {
const eventContent = parseLocationEvent(backwardsCompatibleEventContent);
expect(eventContent).toEqual(backwardsCompatibleEventContent);
});
it('parses modern correctly', () => {
const eventContent = parseLocationEvent(modernEventContent);
expect(eventContent).toEqual(backwardsCompatibleEventContent);
});
it('parses legacy event correctly', () => {
const eventContent = parseLocationEvent(legacyEventContent);
const {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
[M_TIMESTAMP.name]: timestamp,
...expectedResult
} = defaultContent;
expect(eventContent).toEqual({
...expectedResult,
[M_LOCATION.name]: {
...expectedResult[M_LOCATION.name],
description: undefined,
},
});
// don't infer timestamp from legacy event
expect(M_TIMESTAMP.findIn(eventContent)).toBeFalsy();
});
});
+70 -22
View File
@@ -1,3 +1,19 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { logger } from "../../src/logger";
import { MatrixClient } from "../../src/client";
import { Filter } from "../../src/filter";
@@ -13,7 +29,10 @@ import {
import { MEGOLM_ALGORITHM } from "../../src/crypto/olmlib";
import { EventStatus, MatrixEvent } from "../../src/models/event";
import { Preset } from "../../src/@types/partials";
import * as testUtils from "../test-utils";
import * as testUtils from "../test-utils/test-utils";
import { makeBeaconInfoContent } from "../../src/content-helpers";
import { M_BEACON_INFO } from "../../src/@types/beacon";
import { Room } from "../../src";
jest.useFakeTimers();
@@ -53,12 +72,6 @@ describe("MatrixClient", function() {
data: SYNC_DATA,
};
const CAPABILITIES_RESPONSE = {
method: "GET",
path: "/capabilities",
data: { capabilities: {} },
};
let httpLookups = [
// items are objects which look like:
// {
@@ -171,7 +184,6 @@ describe("MatrixClient", function() {
acceptKeepalives = true;
pendingLookup = null;
httpLookups = [];
httpLookups.push(CAPABILITIES_RESPONSE);
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(FILTER_RESPONSE);
httpLookups.push(SYNC_RESPONSE);
@@ -370,7 +382,6 @@ describe("MatrixClient", function() {
it("should not POST /filter if a matching filter already exists", async function() {
httpLookups = [
CAPABILITIES_RESPONSE,
PUSH_RULES_RESPONSE,
SYNC_RESPONSE,
];
@@ -455,14 +466,12 @@ describe("MatrixClient", function() {
describe("retryImmediately", function() {
it("should return false if there is no request waiting", async function() {
httpLookups = [];
httpLookups.push(CAPABILITIES_RESPONSE);
await client.startClient();
expect(client.retryImmediately()).toBe(false);
});
it("should work on /filter", function(done) {
httpLookups = [];
httpLookups.push(CAPABILITIES_RESPONSE);
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push({
method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" },
@@ -513,7 +522,6 @@ describe("MatrixClient", function() {
it("should work on /pushrules", function(done) {
httpLookups = [];
httpLookups.push(CAPABILITIES_RESPONSE);
httpLookups.push({
method: "GET", path: "/pushrules/", error: { errcode: "NOPE_NOPE_NOPE" },
});
@@ -570,7 +578,6 @@ describe("MatrixClient", function() {
it("should transition null -> ERROR after a failed /filter", function(done) {
const expectedStates = [];
httpLookups = [];
httpLookups.push(CAPABILITIES_RESPONSE);
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push({
method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" },
@@ -580,12 +587,14 @@ describe("MatrixClient", function() {
client.startClient();
});
it("should transition ERROR -> CATCHUP after /sync if prev failed",
// Disabled because now `startClient` makes a legit call to `/versions`
// And those tests are really unhappy about it... Not possible to figure
// out what a good resolution would look like
xit("should transition ERROR -> CATCHUP after /sync if prev failed",
function(done) {
const expectedStates = [];
acceptKeepalives = false;
httpLookups = [];
httpLookups.push(CAPABILITIES_RESPONSE);
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(FILTER_RESPONSE);
httpLookups.push({
@@ -617,7 +626,7 @@ describe("MatrixClient", function() {
client.startClient();
});
it("should transition SYNCING -> ERROR after a failed /sync", function(done) {
xit("should transition SYNCING -> ERROR after a failed /sync", function(done) {
acceptKeepalives = false;
const expectedStates = [];
httpLookups.push({
@@ -664,7 +673,7 @@ describe("MatrixClient", function() {
client.startClient();
});
it("should transition ERROR -> ERROR if keepalive keeps failing", function(done) {
xit("should transition ERROR -> ERROR if keepalive keeps failing", function(done) {
acceptKeepalives = false;
const expectedStates = [];
httpLookups.push({
@@ -711,7 +720,6 @@ describe("MatrixClient", function() {
describe("guest rooms", function() {
it("should only do /sync calls (without filter/pushrules)", function(done) {
httpLookups = []; // no /pushrules or /filterw
httpLookups.push(CAPABILITIES_RESPONSE);
httpLookups.push({
method: "GET",
path: "/sync",
@@ -950,6 +958,7 @@ describe("MatrixClient", function() {
it("partitions root events to room timeline and thread timeline", () => {
const supportsExperimentalThreads = client.supportsExperimentalThreads;
client.supportsExperimentalThreads = () => true;
const room = new Room("!room1:matrix.org", client, userId);
const rootEvent = new MatrixEvent({
"content": {},
@@ -959,7 +968,7 @@ describe("MatrixClient", function() {
"type": "m.room.message",
"unsigned": {
"m.relations": {
"io.element.thread": {
"m.thread": {
"latest_event": {},
"count": 33,
"current_user_participated": false,
@@ -972,12 +981,51 @@ describe("MatrixClient", function() {
expect(rootEvent.isThreadRoot).toBe(true);
const [room, threads] = client.partitionThreadedEvents([rootEvent]);
expect(room).toHaveLength(1);
expect(threads).toHaveLength(1);
const [roomEvents, threadEvents] = client.partitionThreadedEvents(room, [rootEvent]);
expect(roomEvents).toHaveLength(1);
expect(threadEvents).toHaveLength(1);
// Restore method
client.supportsExperimentalThreads = supportsExperimentalThreads;
});
});
describe("beacons", () => {
const roomId = '!room:server.org';
const content = makeBeaconInfoContent(100, true);
beforeEach(() => {
client.http.authedRequest.mockClear().mockResolvedValue({});
});
it("creates new beacon info", async () => {
await client.unstable_createLiveBeacon(roomId, content, '123');
// event type combined
const expectedEventType = `${M_BEACON_INFO.name}.${userId}.123`;
const [callback, method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0];
expect(callback).toBeFalsy();
expect(method).toBe('PUT');
expect(path).toEqual(
`/rooms/${encodeURIComponent(roomId)}/state/` +
`${encodeURIComponent(expectedEventType)}/${encodeURIComponent(userId)}`,
);
expect(queryParams).toBeFalsy();
expect(requestContent).toEqual(content);
});
it("updates beacon info with specific event type", async () => {
const eventType = `${M_BEACON_INFO.name}.${userId}.456`;
await client.unstable_setLiveBeacon(roomId, eventType, content);
// event type combined
const [, , path, , requestContent] = client.http.authedRequest.mock.calls[0];
expect(path).toEqual(
`/rooms/${encodeURIComponent(roomId)}/state/` +
`${encodeURIComponent(eventType)}/${encodeURIComponent(userId)}`,
);
expect(requestContent).toEqual(content);
});
});
});
+281
View File
@@ -0,0 +1,281 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { EventType } from "../../../src";
import { M_BEACON_INFO } from "../../../src/@types/beacon";
import {
isTimestampInDuration,
isBeaconInfoEventType,
Beacon,
BeaconEvent,
} from "../../../src/models/beacon";
import { makeBeaconInfoEvent } from "../../test-utils/beacon";
jest.useFakeTimers();
describe('Beacon', () => {
describe('isTimestampInDuration()', () => {
const startTs = new Date('2022-03-11T12:07:47.592Z').getTime();
const HOUR_MS = 3600000;
it('returns false when timestamp is before start time', () => {
// day before
const timestamp = new Date('2022-03-10T12:07:47.592Z').getTime();
expect(isTimestampInDuration(startTs, HOUR_MS, timestamp)).toBe(false);
});
it('returns false when timestamp is after start time + duration', () => {
// 1 second later
const timestamp = new Date('2022-03-10T12:07:48.592Z').getTime();
expect(isTimestampInDuration(startTs, HOUR_MS, timestamp)).toBe(false);
});
it('returns true when timestamp is exactly start time', () => {
expect(isTimestampInDuration(startTs, HOUR_MS, startTs)).toBe(true);
});
it('returns true when timestamp is exactly the end of the duration', () => {
expect(isTimestampInDuration(startTs, HOUR_MS, startTs + HOUR_MS)).toBe(true);
});
it('returns true when timestamp is within the duration', () => {
const twoHourDuration = HOUR_MS * 2;
const now = startTs + HOUR_MS;
expect(isTimestampInDuration(startTs, twoHourDuration, now)).toBe(true);
});
});
describe('isBeaconInfoEventType', () => {
it.each([
EventType.CallAnswer,
`prefix.${M_BEACON_INFO.name}`,
`prefix.${M_BEACON_INFO.altName}`,
])('returns false for %s', (type) => {
expect(isBeaconInfoEventType(type)).toBe(false);
});
it.each([
M_BEACON_INFO.name,
M_BEACON_INFO.altName,
`${M_BEACON_INFO.name}.@test:server.org.12345`,
`${M_BEACON_INFO.altName}.@test:server.org.12345`,
])('returns true for %s', (type) => {
expect(isBeaconInfoEventType(type)).toBe(true);
});
});
describe('Beacon', () => {
const userId = '@user:server.org';
const roomId = '$room:server.org';
// 14.03.2022 16:15
const now = 1647270879403;
const HOUR_MS = 3600000;
// beacon_info events
// created 'an hour ago'
// without timeout of 3 hours
let liveBeaconEvent;
let notLiveBeaconEvent;
const advanceDateAndTime = (ms: number) => {
// bc liveness check uses Date.now we have to advance this mock
jest.spyOn(global.Date, 'now').mockReturnValue(now + ms);
// then advance time for the interval by the same amount
jest.advanceTimersByTime(ms);
};
beforeEach(() => {
// go back in time to create the beacon
jest.spyOn(global.Date, 'now').mockReturnValue(now - HOUR_MS);
liveBeaconEvent = makeBeaconInfoEvent(
userId,
roomId,
{
timeout: HOUR_MS * 3,
isLive: true,
},
'$live123',
'$live123',
);
notLiveBeaconEvent = makeBeaconInfoEvent(
userId,
roomId,
{ timeout: HOUR_MS * 3, isLive: false },
'$dead123',
'$dead123',
);
// back to now
jest.spyOn(global.Date, 'now').mockReturnValue(now);
});
afterAll(() => {
jest.spyOn(global.Date, 'now').mockRestore();
});
it('creates beacon from event', () => {
const beacon = new Beacon(liveBeaconEvent);
expect(beacon.beaconInfoId).toEqual(liveBeaconEvent.getId());
expect(beacon.roomId).toEqual(roomId);
expect(beacon.isLive).toEqual(true);
expect(beacon.beaconInfoOwner).toEqual(userId);
expect(beacon.beaconInfoEventType).toEqual(liveBeaconEvent.getType());
expect(beacon.identifier).toEqual(liveBeaconEvent.getType());
expect(beacon.beaconInfo).toBeTruthy();
});
describe('isLive()', () => {
it('returns false when beacon is explicitly set to not live', () => {
const beacon = new Beacon(notLiveBeaconEvent);
expect(beacon.isLive).toEqual(false);
});
it('returns false when beacon is expired', () => {
// time travel to beacon creation + 3 hours
jest.spyOn(global.Date, 'now').mockReturnValue(now - 3 * HOUR_MS);
const beacon = new Beacon(liveBeaconEvent);
expect(beacon.isLive).toEqual(false);
});
it('returns false when beacon timestamp is in future', () => {
// time travel to before beacon events timestamp
// event was created now - 1 hour
jest.spyOn(global.Date, 'now').mockReturnValue(now - HOUR_MS - HOUR_MS);
const beacon = new Beacon(liveBeaconEvent);
expect(beacon.isLive).toEqual(false);
});
it('returns true when beacon was created in past and not yet expired', () => {
// liveBeaconEvent was created 1 hour ago
const beacon = new Beacon(liveBeaconEvent);
expect(beacon.isLive).toEqual(true);
});
});
describe('update()', () => {
it('does not update with different event', () => {
const beacon = new Beacon(liveBeaconEvent);
expect(beacon.beaconInfoId).toEqual(liveBeaconEvent.getId());
expect(() => beacon.update(notLiveBeaconEvent)).toThrow();
expect(beacon.isLive).toEqual(true);
});
it('updates event', () => {
const beacon = new Beacon(liveBeaconEvent);
const emitSpy = jest.spyOn(beacon, 'emit');
expect(beacon.isLive).toEqual(true);
const updatedBeaconEvent = makeBeaconInfoEvent(
userId, roomId, { timeout: HOUR_MS * 3, isLive: false }, '$live123', '$live123');
beacon.update(updatedBeaconEvent);
expect(beacon.isLive).toEqual(false);
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.Update, updatedBeaconEvent, beacon);
});
it('emits livenesschange event when beacon liveness changes', () => {
const beacon = new Beacon(liveBeaconEvent);
const emitSpy = jest.spyOn(beacon, 'emit');
expect(beacon.isLive).toEqual(true);
const updatedBeaconEvent = makeBeaconInfoEvent(
userId,
roomId,
{ timeout: HOUR_MS * 3, isLive: false },
beacon.beaconInfoId,
'$live123',
);
beacon.update(updatedBeaconEvent);
expect(beacon.isLive).toEqual(false);
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.LivenessChange, false, beacon);
});
});
describe('monitorLiveness()', () => {
it('does not set a monitor interval when beacon is not live', () => {
// beacon was created an hour ago
// and has a 3hr duration
const beacon = new Beacon(notLiveBeaconEvent);
const emitSpy = jest.spyOn(beacon, 'emit');
beacon.monitorLiveness();
// @ts-ignore
expect(beacon.livenessWatchInterval).toBeFalsy();
advanceDateAndTime(HOUR_MS * 2 + 1);
// no emit
expect(emitSpy).not.toHaveBeenCalled();
});
it('checks liveness of beacon at expected expiry time', () => {
// live beacon was created an hour ago
// and has a 3hr duration
const beacon = new Beacon(liveBeaconEvent);
expect(beacon.isLive).toBeTruthy();
const emitSpy = jest.spyOn(beacon, 'emit');
beacon.monitorLiveness();
advanceDateAndTime(HOUR_MS * 2 + 1);
expect(emitSpy).toHaveBeenCalledTimes(1);
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.LivenessChange, false, beacon);
});
it('clears monitor interval when re-monitoring liveness', () => {
// live beacon was created an hour ago
// and has a 3hr duration
const beacon = new Beacon(liveBeaconEvent);
expect(beacon.isLive).toBeTruthy();
beacon.monitorLiveness();
// @ts-ignore
const oldMonitor = beacon.livenessWatchInterval;
beacon.monitorLiveness();
// @ts-ignore
expect(beacon.livenessWatchInterval).not.toEqual(oldMonitor);
});
it('destroy kills liveness monitor and emits', () => {
// live beacon was created an hour ago
// and has a 3hr duration
const beacon = new Beacon(liveBeaconEvent);
expect(beacon.isLive).toBeTruthy();
const emitSpy = jest.spyOn(beacon, 'emit');
beacon.monitorLiveness();
// destroy the beacon
beacon.destroy();
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.Destroy, beacon.identifier);
// live forced to false
expect(beacon.isLive).toBe(false);
advanceDateAndTime(HOUR_MS * 2 + 1);
// no additional calls
expect(emitSpy).toHaveBeenCalledTimes(1);
});
});
});
});
+1 -1
View File
@@ -1,4 +1,4 @@
import * as utils from "../test-utils";
import * as utils from "../test-utils/test-utils";
import { PushProcessor } from "../../src/pushprocessor";
describe('NotificationService', function() {
+1 -1
View File
@@ -1,4 +1,4 @@
import * as utils from "../test-utils";
import * as utils from "../test-utils/test-utils";
import { RoomMember } from "../../src/models/room-member";
describe("RoomMember", function() {
+92 -2
View File
@@ -1,5 +1,8 @@
import * as utils from "../test-utils";
import { RoomState } from "../../src/models/room-state";
import * as utils from "../test-utils/test-utils";
import { makeBeaconInfoEvent } from "../test-utils/beacon";
import { filterEmitCallsByEventType } from "../test-utils/emitter";
import { RoomState, RoomStateEvent } from "../../src/models/room-state";
import { BeaconEvent } from "../../src/models/beacon";
describe("RoomState", function() {
const roomId = "!foo:bar";
@@ -248,6 +251,93 @@ describe("RoomState", function() {
memberEvent, state,
);
});
describe('beacon events', () => {
it('adds new beacon info events to state and emits', () => {
const beaconEvent = makeBeaconInfoEvent(userA, roomId);
const emitSpy = jest.spyOn(state, 'emit');
state.setStateEvents([beaconEvent]);
expect(state.beacons.size).toEqual(1);
const beaconInstance = state.beacons.get(beaconEvent.getType());
expect(beaconInstance).toBeTruthy();
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.New, beaconEvent, beaconInstance);
});
it('does not add redacted beacon info events to state', () => {
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId);
const redactionEvent = { event: { type: 'm.room.redaction' } };
redactedBeaconEvent.makeRedacted(redactionEvent);
const emitSpy = jest.spyOn(state, 'emit');
state.setStateEvents([redactedBeaconEvent]);
// no beacon added
expect(state.beacons.size).toEqual(0);
expect(state.beacons.get(redactedBeaconEvent.getType)).toBeFalsy();
// no new beacon emit
expect(filterEmitCallsByEventType(BeaconEvent.New, emitSpy).length).toBeFalsy();
});
it('updates existing beacon info events in state', () => {
const beaconId = '$beacon1';
const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId, beaconId);
const updatedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: false }, beaconId, beaconId);
state.setStateEvents([beaconEvent]);
const beaconInstance = state.beacons.get(beaconEvent.getType());
expect(beaconInstance.isLive).toEqual(true);
state.setStateEvents([updatedBeaconEvent]);
// same Beacon
expect(state.beacons.get(beaconEvent.getType())).toBe(beaconInstance);
// updated liveness
expect(state.beacons.get(beaconEvent.getType()).isLive).toEqual(false);
});
it('destroys and removes redacted beacon events', () => {
const beaconId = '$beacon1';
const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId, beaconId);
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId, beaconId);
const redactionEvent = { event: { type: 'm.room.redaction' } };
redactedBeaconEvent.makeRedacted(redactionEvent);
state.setStateEvents([beaconEvent]);
const beaconInstance = state.beacons.get(beaconEvent.getType());
const destroySpy = jest.spyOn(beaconInstance, 'destroy');
expect(beaconInstance.isLive).toEqual(true);
state.setStateEvents([redactedBeaconEvent]);
expect(destroySpy).toHaveBeenCalled();
expect(state.beacons.get(beaconEvent.getType())).toBe(undefined);
});
it('updates live beacon ids once after setting state events', () => {
const liveBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, '$beacon1', '$beacon1');
const deadBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: false }, '$beacon2', '$beacon2');
const emitSpy = jest.spyOn(state, 'emit');
state.setStateEvents([liveBeaconEvent, deadBeaconEvent]);
// called once
expect(filterEmitCallsByEventType(RoomStateEvent.BeaconLiveness, emitSpy).length).toBe(1);
// live beacon is now not live
const updatedLiveBeaconEvent = makeBeaconInfoEvent(
userA, roomId, { isLive: false }, liveBeaconEvent.getId(), '$beacon1',
);
state.setStateEvents([updatedLiveBeaconEvent]);
expect(state.hasLiveBeacons).toBe(false);
expect(filterEmitCallsByEventType(RoomStateEvent.BeaconLiveness, emitSpy).length).toBe(2);
expect(emitSpy).toHaveBeenCalledWith(RoomStateEvent.BeaconLiveness, state, false);
});
});
});
describe("setOutOfBandMembers", function() {
+656 -537
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -4,7 +4,7 @@
import { defer } from '../../src/utils';
import { MatrixError } from "../../src/http-api";
import { MatrixScheduler } from "../../src/scheduler";
import * as utils from "../test-utils";
import * as utils from "../test-utils/test-utils";
jest.useFakeTimers();
+1 -1
View File
@@ -1,6 +1,6 @@
import { EventTimeline } from "../../src/models/event-timeline";
import { TimelineIndex, TimelineWindow } from "../../src/timeline-window";
import * as utils from "../test-utils";
import * as utils from "../test-utils/test-utils";
const ROOM_ID = "roomId";
const USER_ID = "userId";
+1 -1
View File
@@ -1,5 +1,5 @@
import { User } from "../../src/models/user";
import * as utils from "../test-utils";
import * as utils from "../test-utils/test-utils";
describe("User", function() {
const userId = "@alice:bar";
+147
View File
@@ -0,0 +1,147 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { EitherAnd, RELATES_TO_RELATIONSHIP, REFERENCE_RELATION } from "matrix-events-sdk";
import { UnstableValue } from "../NamespacedValue";
import { MAssetEvent, MLocationEvent, MTimestampEvent } from "./location";
/**
* Beacon info and beacon event types as described in MSC3489
* https://github.com/matrix-org/matrix-spec-proposals/pull/3489
*/
/**
* Beacon info events are state events.
* We have two requirements for these events:
* 1. they can only be written by their owner
* 2. a user can have an arbitrary number of beacon_info events
*
* 1. is achieved by setting the state_key to the owners mxid.
* Event keys in room state are a combination of `type` + `state_key`.
* To achieve an arbitrary number of only owner-writable state events
* we introduce a variable suffix to the event type
*
* Eg
* {
* "type": "m.beacon_info.@matthew:matrix.org.1",
* "state_key": "@matthew:matrix.org",
* "content": {
* "m.beacon_info": {
* "description": "The Matthew Tracker",
* "timeout": 86400000,
* },
* // more content as described below
* }
* },
* {
* "type": "m.beacon_info.@matthew:matrix.org.2",
* "state_key": "@matthew:matrix.org",
* "content": {
* "m.beacon_info": {
* "description": "Another different Matthew tracker",
* "timeout": 400000,
* },
* // more content as described below
* }
* }
*/
/**
* Variable event type for m.beacon_info
*/
export const M_BEACON_INFO_VARIABLE = new UnstableValue("m.beacon_info.*", "org.matrix.msc3489.beacon_info.*");
/**
* Non-variable type for m.beacon_info event content
*/
export const M_BEACON_INFO = new UnstableValue("m.beacon_info", "org.matrix.msc3489.beacon_info");
export const M_BEACON = new UnstableValue("m.beacon", "org.matrix.msc3489.beacon");
export type MBeaconInfoContent = {
description?: string;
// how long from the last event until we consider the beacon inactive in milliseconds
timeout: number;
// true when this is a live location beacon
// https://github.com/matrix-org/matrix-spec-proposals/pull/3672
live?: boolean;
};
export type MBeaconInfoEvent = EitherAnd<
{ [M_BEACON_INFO.name]: MBeaconInfoContent },
{ [M_BEACON_INFO.altName]: MBeaconInfoContent }
>;
/**
* m.beacon_info Event example from the spec
* https://github.com/matrix-org/matrix-spec-proposals/pull/3489
* {
"type": "m.beacon_info.@matthew:matrix.org.1",
"state_key": "@matthew:matrix.org",
"content": {
"m.beacon_info": {
"description": "The Matthew Tracker", // same as an `m.location` description
"timeout": 86400000, // how long from the last event until we consider the beacon inactive in milliseconds
},
"m.ts": 1436829458432, // creation timestamp of the beacon on the client
"m.asset": {
"type": "m.self" // the type of asset being tracked as per MSC3488
}
}
}
*/
/**
* m.beacon_info.* event content
*/
export type MBeaconInfoEventContent = &
MBeaconInfoEvent &
// creation timestamp of the beacon on the client
MTimestampEvent &
// the type of asset being tracked as per MSC3488
MAssetEvent;
/**
* m.beacon event example
* https://github.com/matrix-org/matrix-spec-proposals/pull/3489
*
* {
"type": "m.beacon",
"sender": "@matthew:matrix.org",
"content": {
"m.relates_to": { // from MSC2674: https://github.com/matrix-org/matrix-doc/pull/2674
"rel_type": "m.reference", // from MSC3267: https://github.com/matrix-org/matrix-doc/pull/3267
"event_id": "$beacon_info"
},
"m.location": {
"uri": "geo:51.5008,0.1247;u=35",
"description": "Arbitrary beacon information"
},
"m.ts": 1636829458432,
}
}
*/
/**
* Content of an m.beacon event
*/
export type MBeaconEventContent = &
MLocationEvent &
// timestamp when location was taken
MTimestampEvent &
// relates to a beacon_info event
RELATES_TO_RELATIONSHIP<typeof REFERENCE_RELATION>;
+3 -8
View File
@@ -93,14 +93,7 @@ export enum RelationType {
Annotation = "m.annotation",
Replace = "m.replace",
Reference = "m.reference",
/**
* 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",
Thread = "m.thread",
}
export enum MsgType {
@@ -119,6 +112,8 @@ export const RoomCreateTypeField = "type";
export enum RoomType {
Space = "m.space",
UnstableCall = "org.matrix.msc3417.call",
ElementVideo = "io.element.video",
}
/**
+47 -21
View File
@@ -15,23 +15,44 @@ limitations under the License.
*/
// Types for MSC3488 - m.location: Extending events with location data
import { EitherAnd } from "matrix-events-sdk";
import { UnstableValue } from "../NamespacedValue";
import { IContent } from "../models/event";
import { TEXT_NODE_TYPE } from "./extensible_events";
export const LOCATION_EVENT_TYPE = new UnstableValue(
"m.location", "org.matrix.msc3488.location");
export const ASSET_NODE_TYPE = new UnstableValue("m.asset", "org.matrix.msc3488.asset");
export const TIMESTAMP_NODE_TYPE = new UnstableValue("m.ts", "org.matrix.msc3488.ts");
export enum LocationAssetType {
Self = "m.self",
Pin = "m.pin",
}
export const M_ASSET = new UnstableValue("m.asset", "org.matrix.msc3488.asset");
export type MAssetContent = { type: LocationAssetType };
/**
* The event definition for an m.asset event (in content)
*/
export type MAssetEvent = EitherAnd<{ [M_ASSET.name]: MAssetContent }, { [M_ASSET.altName]: MAssetContent }>;
export const M_TIMESTAMP = new UnstableValue("m.ts", "org.matrix.msc3488.ts");
/**
* The event definition for an m.ts event (in content)
*/
export type MTimestampEvent = EitherAnd<{ [M_TIMESTAMP.name]: number }, { [M_TIMESTAMP.altName]: number }>;
export const M_LOCATION = new UnstableValue(
"m.location", "org.matrix.msc3488.location");
export type MLocationContent = {
uri: string;
description?: string | null;
};
export type MLocationEvent = EitherAnd<
{ [M_LOCATION.name]: MLocationContent },
{ [M_LOCATION.altName]: MLocationContent }
>;
export type MTextEvent = EitherAnd<{ [TEXT_NODE_TYPE.name]: string }, { [TEXT_NODE_TYPE.altName]: string }>;
/* From the spec at:
* https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md
{
@@ -52,20 +73,25 @@ export enum LocationAssetType {
}
}
*/
type OptionalTimestampEvent = MTimestampEvent | undefined;
/**
* The content for an m.location event
*/
export type MLocationEventContent = &
MLocationEvent &
MAssetEvent &
MTextEvent &
OptionalTimestampEvent;
/* eslint-disable camelcase */
export interface ILocationContent extends IContent {
export type LegacyLocationEventContent = {
body: string;
msgtype: string;
geo_uri: string;
[LOCATION_EVENT_TYPE.name]: {
uri: string;
description?: string;
};
[ASSET_NODE_TYPE.name]: {
type: LocationAssetType;
};
[TEXT_NODE_TYPE.name]: string;
[TIMESTAMP_NODE_TYPE.name]: number;
}
/* eslint-enable camelcase */
};
/**
* Possible content for location events as sent over the wire
*/
export type LocationEventWireContent = Partial<LegacyLocationEventContent & MLocationEventContent>;
export type ILocationContent = MLocationEventContent & LegacyLocationEventContent;
+16
View File
@@ -70,6 +70,22 @@ export class NamespacedValue<S extends string, U extends string> {
}
}
export class ServerControlledNamespacedValue<S extends string, U extends string>
extends NamespacedValue<S, U> {
private preferUnstable = false;
public setPreferUnstable(preferUnstable: boolean): void {
this.preferUnstable = preferUnstable;
}
public get name(): U | S {
if (this.stable && !this.preferUnstable) {
return this.stable;
}
return this.unstable;
}
}
/**
* Represents a namespaced value which prioritizes the unstable value over the stable
* value.
+162 -564
View File
@@ -37,7 +37,6 @@ import { Filter, IFilterDefinition } from "./filter";
import { CallEventHandlerEvent, CallEventHandler, CallEventHandlerEventHandlerMap } from './webrtc/callEventHandler';
import * as utils from './utils';
import { sleep } from './utils';
import { Group } from "./models/group";
import { Direction, EventTimeline } from "./models/event-timeline";
import { IActionsObject, PushProcessor } from "./pushprocessor";
import { AutoDiscovery, AutoDiscoveryAction } from "./autodiscovery";
@@ -104,6 +103,8 @@ import {
IRoomEvent,
IStateEvent,
NotificationCountType,
BeaconEvent,
BeaconEventHandlerMap,
RoomEvent,
RoomEventHandlerMap,
RoomMemberEvent,
@@ -178,6 +179,8 @@ import { CryptoStore } from "./crypto/store/base";
import { MediaHandler } from "./webrtc/mediaHandler";
import { IRefreshTokenResponse } from "./@types/auth";
import { TypedEventEmitter } from "./models/typed-event-emitter";
import { Thread, THREAD_RELATION_TYPE } from "./models/thread";
import { MBeaconInfoEventContent, M_BEACON_INFO_VARIABLE } from "./@types/beacon";
export type Store = IStore;
export type SessionStore = WebStorageSessionStore;
@@ -779,11 +782,6 @@ export enum ClientEvent {
DeleteRoom = "deleteRoom",
SyncUnexpectedError = "sync.unexpectedError",
ClientWellKnown = "WellKnown.client",
/* @deprecated */
Group = "Group",
// The following enum members are both deprecated and in the wrong place, Groups haven't been TSified
GroupProfile = "Group.profile",
GroupMyMembership = "Group.myMembership",
}
type RoomEvents = RoomEvent.Name
@@ -840,7 +838,8 @@ type EmittedEvents = ClientEvent
| CallEvent // re-emitted by call.ts using Object.values
| CallEventHandlerEvent.Incoming
| HttpApiEvent.SessionLoggedOut
| HttpApiEvent.NoConsent;
| HttpApiEvent.NoConsent
| BeaconEvent;
export type ClientEventHandlerMap = {
[ClientEvent.Sync]: (state: SyncState, lastState?: SyncState, data?: ISyncStateData) => void;
@@ -851,9 +850,6 @@ export type ClientEventHandlerMap = {
[ClientEvent.DeleteRoom]: (roomId: string) => void;
[ClientEvent.SyncUnexpectedError]: (error: Error) => void;
[ClientEvent.ClientWellKnown]: (data: IClientWellKnown) => void;
[ClientEvent.Group]: (group: Group) => void;
[ClientEvent.GroupProfile]: (group: Group) => void;
[ClientEvent.GroupMyMembership]: (group: Group) => void;
} & RoomEventHandlerMap
& RoomStateEventHandlerMap
& CryptoEventHandlerMap
@@ -862,7 +858,8 @@ export type ClientEventHandlerMap = {
& UserEventHandlerMap
& CallEventHandlerEventHandlerMap
& CallEventHandlerMap
& HttpApiEventHandlerMap;
& HttpApiEventHandlerMap
& BeaconEventHandlerMap;
/**
* Represents a Matrix Client. Only directly construct this if you want to use
@@ -1161,7 +1158,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
this.syncApi.stop();
}
await this.getCapabilities(true);
try {
const { serverSupport, stable } = await this.doesServerSupportThread();
Thread.setServerSideSupport(serverSupport, stable);
} catch (e) {
Thread.setServerSideSupport(false, true);
}
// shallow-copy the opts dict before modifying and storing it
this.clientOpts = Object.assign({}, opts) as IStoredClientOpts;
@@ -3230,27 +3232,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
}
/**
* Get the group for the given group ID.
* This function will return a valid group for any group for which a Group event
* has been emitted.
* @param {string} groupId The group ID
* @return {Group} The Group or null if the group is not known or there is no data store.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public getGroup(groupId: string): Group {
return this.store.getGroup(groupId);
}
/**
* Retrieve all known groups.
* @return {Group[]} A list of groups, or an empty list if there is no data store.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public getGroups(): Group[] {
return this.store.getGroups();
}
/**
* Get the config for the media repository.
* @param {module:client.callback} callback Optional.
@@ -3664,6 +3645,45 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return this.http.authedRequest(callback, Method.Put, path, undefined, content);
}
/**
* Create an m.beacon_info event
* @param {string} roomId
* @param {MBeaconInfoEventContent} beaconInfoContent
* @param {string} eventTypeSuffix - string to suffix event type
* to make event type unique.
* See MSC3489 for more context
* https://github.com/matrix-org/matrix-spec-proposals/pull/3489
* @returns {ISendEventResponse}
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
public async unstable_createLiveBeacon(
roomId: Room["roomId"],
beaconInfoContent: MBeaconInfoEventContent,
eventTypeSuffix: string,
) {
const userId = this.getUserId();
const eventType = M_BEACON_INFO_VARIABLE.name.replace('*', `${userId}.${eventTypeSuffix}`);
return this.unstable_setLiveBeacon(roomId, eventType, beaconInfoContent);
}
/**
* Upsert a live beacon event
* using a specific m.beacon_info.* event variable type
* @param {string} roomId string
* @param {string} beaconInfoEventType event type including variable suffix
* @param {MBeaconInfoEventContent} beaconInfoContent
* @returns {ISendEventResponse}
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
public async unstable_setLiveBeacon(
roomId: string,
beaconInfoEventType: string,
beaconInfoContent: MBeaconInfoEventContent,
) {
const userId = this.getUserId();
return this.sendStateEvent(roomId, beaconInfoEventType, beaconInfoContent, userId);
}
/**
* @param {string} roomId
* @param {string} threadId
@@ -3710,14 +3730,14 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
if (threadId && !content["m.relates_to"]?.rel_type) {
content["m.relates_to"] = {
...content["m.relates_to"],
"rel_type": RelationType.Thread,
"rel_type": THREAD_RELATION_TYPE.name,
"event_id": threadId,
};
const thread = this.getRoom(roomId)?.threads.get(threadId);
if (thread) {
content["m.relates_to"]["m.in_reply_to"] = {
"event_id": thread.lastReply((ev: MatrixEvent) => {
return ev.isThreadRelation && !ev.status;
return ev.isRelation(THREAD_RELATION_TYPE.name) && !ev.status;
})?.getId(),
};
}
@@ -3931,7 +3951,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
/**
* Returns the eventType that should be used taking encryption into account
* for a given eventType.
* @param {MatrixClient} client the client
* @param {string} roomId the room for the events `eventType` relates to
* @param {string} eventType the event type
* @return {string} the event type taking encryption into account
@@ -4924,40 +4943,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
);
}
/**
* This is an internal method.
* @param {MatrixClient} client
* @param {string} roomId
* @param {string} userId
* @param {string} membershipValue
* @param {string} reason
* @param {module:client.callback} callback Optional.
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
private setMembershipState(
roomId: string,
userId: string,
membershipValue: string,
reason?: string,
callback?: Callback,
) {
if (utils.isFunction(reason)) {
callback = reason as any as Callback; // legacy
reason = undefined;
}
const path = utils.encodeUri(
"/rooms/$roomId/state/m.room.member/$userId",
{ $roomId: roomId, $userId: userId },
);
return this.http.authedRequest(callback, Method.Put, path, undefined, {
membership: membershipValue,
reason: reason,
});
}
private membershipChange(
roomId: string,
userId: string,
@@ -5194,7 +5179,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
room.currentState.setUnknownStateEvents(stateEvents);
}
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(matrixEvents);
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(room, matrixEvents);
room.addEventsToTimeline(timelineEvents, true, room.getLiveTimeline());
await this.processThreadEvents(room, threadedEvents, true);
@@ -5246,19 +5231,17 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param {string} eventId The ID of the event to look for
*
* @return {Promise} Resolves:
* {@link module:models/event-timeline~EventTimeline} including the given
* event
* {@link module:models/event-timeline~EventTimeline} including the given event
*/
public getEventTimeline(timelineSet: EventTimelineSet, eventId: string): Promise<EventTimeline> {
public async getEventTimeline(timelineSet: EventTimelineSet, eventId: string): Promise<EventTimeline> {
// don't allow any timeline support unless it's been enabled.
if (!this.timelineSupport) {
throw new Error("timeline support is disabled. Set the 'timelineSupport'" +
" parameter to true when creating MatrixClient to enable" +
" it.");
" parameter to true when creating MatrixClient to enable it.");
}
if (timelineSet.getTimelineForEvent(eventId)) {
return Promise.resolve(timelineSet.getTimelineForEvent(eventId));
return timelineSet.getTimelineForEvent(eventId);
}
const path = utils.encodeUri(
@@ -5268,56 +5251,82 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
},
);
let params = undefined;
let params: Record<string, string | string[]> = undefined;
if (this.clientOpts.lazyLoadMembers) {
params = { filter: JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER) };
}
// TODO: we should implement a backoff (as per scrollback()) to deal more
// nicely with HTTP errors.
const promise = this.http.authedRequest<any>(undefined, Method.Get, path, params).then(async (res) => { // TODO types
if (!res.event) {
throw new Error("'event' not in '/context' result - homeserver too old?");
}
// TODO: we should implement a backoff (as per scrollback()) to deal more nicely with HTTP errors.
const res = await this.http.authedRequest<any>(undefined, Method.Get, path, params); // TODO types
if (!res.event) {
throw new Error("'event' not in '/context' result - homeserver too old?");
}
// by the time the request completes, the event might have ended up in
// the timeline.
if (timelineSet.getTimelineForEvent(eventId)) {
return timelineSet.getTimelineForEvent(eventId);
}
// by the time the request completes, the event might have ended up in the timeline.
if (timelineSet.getTimelineForEvent(eventId)) {
return timelineSet.getTimelineForEvent(eventId);
}
// we start with the last event, since that's the point at which we
// have known state.
const mapper = this.getEventMapper();
const event = mapper(res.event);
const events = [
// we start with the last event, since that's the point at which we have known state.
// events_after is already backwards; events_before is forwards.
res.events_after.reverse();
const events = res.events_after
.concat([res.event])
.concat(res.events_before);
const matrixEvents = events.map(this.getEventMapper());
...res.events_after.reverse().map(mapper),
event,
...res.events_before.map(mapper),
];
let timeline = timelineSet.getTimelineForEvent(matrixEvents[0].getId());
if (!timeline) {
timeline = timelineSet.addTimeline();
timeline.initialiseState(res.state.map(this.getEventMapper()));
timeline.getState(EventTimeline.FORWARDS).paginationToken = res.end;
} else {
const stateEvents = res.state.map(this.getEventMapper());
timeline.getState(EventTimeline.BACKWARDS).setUnknownStateEvents(stateEvents);
// Where the event is a thread reply (not a root) and running in MSC-enabled mode the Thread timeline only
// functions contiguously, so we have to jump through some hoops to get our target event in it.
// XXX: workaround for https://github.com/vector-im/element-meta/issues/150
if (Thread.hasServerSideSupport && event.isRelation(THREAD_RELATION_TYPE.name)) {
const [, threadedEvents] = this.partitionThreadedEvents(timelineSet.room, events);
const thread = await timelineSet.room.createThreadFetchRoot(event.threadRootId, threadedEvents, true);
let nextBatch: string;
const response = await thread.fetchInitialEvents();
if (response?.nextBatch) {
nextBatch = response.nextBatch;
}
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(matrixEvents);
const opts: IRelationsRequestOpts = {
limit: 50,
};
timelineSet.addEventsToTimeline(timelineEvents, true, timeline, res.start);
await this.processThreadEvents(timelineSet.room, threadedEvents, true);
// Fetch events until we find the one we were asked for
while (!thread.findEventById(eventId)) {
if (nextBatch) {
opts.from = nextBatch;
}
// there is no guarantee that the event ended up in "timeline" (we
// might have switched to a neighbouring timeline) - so check the
// room's index again. On the other hand, there's no guarantee the
// event ended up anywhere, if it was later redacted, so we just
// return the timeline we first thought of.
return timelineSet.getTimelineForEvent(eventId) || timeline;
});
return promise;
({ nextBatch } = await thread.fetchEvents(opts));
}
return thread.liveTimeline;
}
// Here we handle non-thread timelines only, but still process any thread events to populate thread summaries.
let timeline = timelineSet.getTimelineForEvent(events[0].getId());
if (timeline) {
timeline.getState(EventTimeline.BACKWARDS).setUnknownStateEvents(res.state.map(mapper));
} else {
timeline = timelineSet.addTimeline();
timeline.initialiseState(res.state.map(mapper));
timeline.getState(EventTimeline.FORWARDS).paginationToken = res.end;
}
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(timelineSet.room, events);
timelineSet.addEventsToTimeline(timelineEvents, true, timeline, res.start);
// The target event is not in a thread but process the contextual events, so we can show any threads around it.
await this.processThreadEvents(timelineSet.room, threadedEvents, true);
// There is no guarantee that the event ended up in "timeline" (we might have switched to a neighbouring
// timeline) - so check the room's index again. On the other hand, there's no guarantee the event ended up
// anywhere, if it was later redacted, so we just return the timeline we first thought of.
return timelineSet.getTimelineForEvent(eventId)
?? timelineSet.room.findThreadForEvent(event)?.liveTimeline // for Threads degraded support
?? timeline;
}
/**
@@ -5417,7 +5426,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
only: 'highlight',
};
if (token && token !== "end") {
if (token !== "end") {
params.from = token;
}
@@ -5437,11 +5446,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
matrixEvents[i] = event;
}
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(matrixEvents);
const timelineSet = eventTimeline.getTimelineSet();
timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
await this.processThreadEvents(timelineSet.room, threadedEvents, backwards);
timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
// if we've hit the end of the timeline, we need to stop trying to
// paginate. We need to keep the 'forwards' token though, to make sure
@@ -5475,10 +5481,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const token = res.end;
const matrixEvents = res.chunk.map(this.getEventMapper());
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(matrixEvents);
eventTimeline.getTimelineSet()
.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
const timelineSet = eventTimeline.getTimelineSet();
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(timelineSet.room, matrixEvents);
timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
await this.processThreadEvents(room, threadedEvents, backwards);
// if we've hit the end of the timeline, we need to stop trying to
@@ -5955,7 +5960,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
public searchRoomEvents(opts: IEventSearchOpts): Promise<ISearchResults> {
// TODO: support groups
// TODO: support search groups
const body = {
search_categories: {
@@ -6041,10 +6046,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// turn it back into a list.
searchResults.highlights = Array.from(highlights);
const mapper = this.getEventMapper();
// append the new results to our existing results
const resultsLength = roomEvents.results ? roomEvents.results.length : 0;
const resultsLength = roomEvents.results?.length ?? 0;
for (let i = 0; i < resultsLength; i++) {
const sr = SearchResult.fromJson(roomEvents.results[i], this.getEventMapper());
const sr = SearchResult.fromJson(roomEvents.results[i], mapper);
const room = this.getRoom(sr.context.getEvent().getRoomId());
if (room) {
// Copy over a known event sender if we can
@@ -6557,6 +6564,24 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return unstableFeatures && !!unstableFeatures[`io.element.e2ee_forced.${versionsPresetName}`];
}
public async doesServerSupportThread(): Promise<{
serverSupport: boolean;
stable: boolean;
} | null> {
try {
const hasUnstableSupport = await this.doesServerSupportUnstableFeature("org.matrix.msc3440");
const hasStableSupport = await this.doesServerSupportUnstableFeature("org.matrix.msc3440.stable")
|| await this.isVersionSupported("v1.3");
return {
serverSupport: hasUnstableSupport || hasStableSupport,
stable: hasStableSupport,
};
} catch (e) {
return null;
}
}
/**
* Get if lazy loading members is being used.
* @return {boolean} Whether or not members are lazy loaded by this client
@@ -6617,11 +6642,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
fetchedEventType,
opts);
const mapper = this.getEventMapper();
let originalEvent: MatrixEvent;
if (result.original_event) {
originalEvent = mapper(result.original_event);
}
const originalEvent = result.original_event ? mapper(result.original_event) : undefined;
let events = result.chunk.map(mapper);
if (fetchedEventType === EventType.RoomMessageEncrypted) {
const allEvents = originalEvent ? events.concat(originalEvent) : events;
await Promise.all(allEvents.map(e => this.decryptEventIfNeeded(e)));
@@ -6629,6 +6653,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
events = events.filter(e => e.getType() === eventType);
}
}
if (originalEvent && relationType === RelationType.Replace) {
events = events.filter(e => e.getSender() === originalEvent.getSender());
}
@@ -8804,368 +8829,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return new MSC3089TreeSpace(this, roomId);
}
// TODO: Remove this warning, alongside the functions
// See https://github.com/vector-im/element-web/issues/17532
// ======================================================
// ** ANCIENT APIS BELOW **
// ======================================================
/**
* @param {string} groupId
* @return {Promise} Resolves: Group summary object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public getGroupSummary(groupId: string): Promise<any> {
const path = utils.encodeUri("/groups/$groupId/summary", { $groupId: groupId });
return this.http.authedRequest(undefined, Method.Get, path);
}
/**
* @param {string} groupId
* @return {Promise} Resolves: Group profile object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public getGroupProfile(groupId: string): Promise<any> {
const path = utils.encodeUri("/groups/$groupId/profile", { $groupId: groupId });
return this.http.authedRequest(undefined, Method.Get, path);
}
/**
* @param {string} groupId
* @param {Object} profile The group profile object
* @param {string=} profile.name Name of the group
* @param {string=} profile.avatar_url MXC avatar URL
* @param {string=} profile.short_description A short description of the room
* @param {string=} profile.long_description A longer HTML description of the room
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public setGroupProfile(groupId: string, profile: any): Promise<any> {
const path = utils.encodeUri("/groups/$groupId/profile", { $groupId: groupId });
return this.http.authedRequest(
undefined, Method.Post, path, undefined, profile,
);
}
/**
* @param {string} groupId
* @param {object} policy The join policy for the group. Must include at
* least a 'type' field which is 'open' if anyone can join the group
* the group without prior approval, or 'invite' if an invite is
* required to join.
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public setGroupJoinPolicy(groupId: string, policy: any): Promise<any> {
const path = utils.encodeUri(
"/groups/$groupId/settings/m.join_policy",
{ $groupId: groupId },
);
return this.http.authedRequest(
undefined, Method.Put, path, undefined, {
'm.join_policy': policy,
},
);
}
/**
* @param {string} groupId
* @return {Promise} Resolves: Group users list object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public getGroupUsers(groupId: string): Promise<any> {
const path = utils.encodeUri("/groups/$groupId/users", { $groupId: groupId });
return this.http.authedRequest(undefined, Method.Get, path);
}
/**
* @param {string} groupId
* @return {Promise} Resolves: Group users list object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public getGroupInvitedUsers(groupId: string): Promise<any> {
const path = utils.encodeUri("/groups/$groupId/invited_users", { $groupId: groupId });
return this.http.authedRequest(undefined, Method.Get, path);
}
/**
* @param {string} groupId
* @return {Promise} Resolves: Group rooms list object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public getGroupRooms(groupId: string): Promise<any> {
const path = utils.encodeUri("/groups/$groupId/rooms", { $groupId: groupId });
return this.http.authedRequest(undefined, Method.Get, path);
}
/**
* @param {string} groupId
* @param {string} userId
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public inviteUserToGroup(groupId: string, userId: string): Promise<any> {
const path = utils.encodeUri(
"/groups/$groupId/admin/users/invite/$userId",
{ $groupId: groupId, $userId: userId },
);
return this.http.authedRequest(undefined, Method.Put, path, undefined, {});
}
/**
* @param {string} groupId
* @param {string} userId
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public removeUserFromGroup(groupId: string, userId: string): Promise<any> {
const path = utils.encodeUri(
"/groups/$groupId/admin/users/remove/$userId",
{ $groupId: groupId, $userId: userId },
);
return this.http.authedRequest(undefined, Method.Put, path, undefined, {});
}
/**
* @param {string} groupId
* @param {string} userId
* @param {string} roleId Optional.
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public addUserToGroupSummary(groupId: string, userId: string, roleId: string): Promise<any> {
const path = utils.encodeUri(
roleId ?
"/groups/$groupId/summary/$roleId/users/$userId" :
"/groups/$groupId/summary/users/$userId",
{ $groupId: groupId, $roleId: roleId, $userId: userId },
);
return this.http.authedRequest(undefined, Method.Put, path, undefined, {});
}
/**
* @param {string} groupId
* @param {string} userId
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public removeUserFromGroupSummary(groupId: string, userId: string): Promise<any> {
const path = utils.encodeUri(
"/groups/$groupId/summary/users/$userId",
{ $groupId: groupId, $userId: userId },
);
return this.http.authedRequest(undefined, Method.Delete, path, undefined, {});
}
/**
* @param {string} groupId
* @param {string} roomId
* @param {string} categoryId Optional.
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public addRoomToGroupSummary(groupId: string, roomId: string, categoryId: string): Promise<any> {
const path = utils.encodeUri(
categoryId ?
"/groups/$groupId/summary/$categoryId/rooms/$roomId" :
"/groups/$groupId/summary/rooms/$roomId",
{ $groupId: groupId, $categoryId: categoryId, $roomId: roomId },
);
return this.http.authedRequest(undefined, Method.Put, path, undefined, {});
}
/**
* @param {string} groupId
* @param {string} roomId
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public removeRoomFromGroupSummary(groupId: string, roomId: string): Promise<any> {
const path = utils.encodeUri(
"/groups/$groupId/summary/rooms/$roomId",
{ $groupId: groupId, $roomId: roomId },
);
return this.http.authedRequest(undefined, Method.Delete, path, undefined, {});
}
/**
* @param {string} groupId
* @param {string} roomId
* @param {boolean} isPublic Whether the room-group association is visible to non-members
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public addRoomToGroup(groupId: string, roomId: string, isPublic: boolean): Promise<any> {
if (isPublic === undefined) {
isPublic = true;
}
const path = utils.encodeUri(
"/groups/$groupId/admin/rooms/$roomId",
{ $groupId: groupId, $roomId: roomId },
);
return this.http.authedRequest(undefined, Method.Put, path, undefined,
{ "m.visibility": { type: isPublic ? "public" : "private" } },
);
}
/**
* Configure the visibility of a room-group association.
* @param {string} groupId
* @param {string} roomId
* @param {boolean} isPublic Whether the room-group association is visible to non-members
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public updateGroupRoomVisibility(groupId: string, roomId: string, isPublic: boolean): Promise<any> {
// NB: The /config API is generic but there's not much point in exposing this yet as synapse
// is the only server to implement this. In future we should consider an API that allows
// arbitrary configuration, i.e. "config/$configKey".
const path = utils.encodeUri(
"/groups/$groupId/admin/rooms/$roomId/config/m.visibility",
{ $groupId: groupId, $roomId: roomId },
);
return this.http.authedRequest(undefined, Method.Put, path, undefined,
{ type: isPublic ? "public" : "private" },
);
}
/**
* @param {string} groupId
* @param {string} roomId
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public removeRoomFromGroup(groupId: string, roomId: string): Promise<any> {
const path = utils.encodeUri(
"/groups/$groupId/admin/rooms/$roomId",
{ $groupId: groupId, $roomId: roomId },
);
return this.http.authedRequest(undefined, Method.Delete, path, undefined, {});
}
/**
* @param {string} groupId
* @param {Object} opts Additional options to send alongside the acceptance.
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public acceptGroupInvite(groupId: string, opts = null): Promise<any> {
const path = utils.encodeUri(
"/groups/$groupId/self/accept_invite",
{ $groupId: groupId },
);
return this.http.authedRequest(undefined, Method.Put, path, undefined, opts || {});
}
/**
* @param {string} groupId
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public joinGroup(groupId: string): Promise<any> {
const path = utils.encodeUri(
"/groups/$groupId/self/join",
{ $groupId: groupId },
);
return this.http.authedRequest(undefined, Method.Put, path, undefined, {});
}
/**
* @param {string} groupId
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public leaveGroup(groupId: string): Promise<any> {
const path = utils.encodeUri(
"/groups/$groupId/self/leave",
{ $groupId: groupId },
);
return this.http.authedRequest(undefined, Method.Put, path, undefined, {});
}
/**
* @return {Promise} Resolves: The groups to which the user is joined
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public getJoinedGroups(): Promise<any> {
const path = utils.encodeUri("/joined_groups", {});
return this.http.authedRequest(undefined, Method.Get, path);
}
/**
* @param {Object} content Request content
* @param {string} content.localpart The local part of the desired group ID
* @param {Object} content.profile Group profile object
* @return {Promise} Resolves: Object with key group_id: id of the created group
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public createGroup(content: any): Promise<any> {
const path = utils.encodeUri("/create_group", {});
return this.http.authedRequest(
undefined, Method.Post, path, undefined, content,
);
}
/**
* @param {string[]} userIds List of user IDs
* @return {Promise} Resolves: Object as exmaple below
*
* {
* "users": {
* "@bob:example.com": {
* "+example:example.com"
* }
* }
* }
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public getPublicisedGroups(userIds: string[]): Promise<any> {
const path = utils.encodeUri("/publicised_groups", {});
return this.http.authedRequest(
undefined, Method.Post, path, undefined, { user_ids: userIds },
);
}
/**
* @param {string} groupId
* @param {boolean} isPublic Whether the user's membership of this group is made public
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public setGroupPublicity(groupId: string, isPublic: boolean): Promise<any> {
const path = utils.encodeUri(
"/groups/$groupId/self/update_publicity",
{ $groupId: groupId },
);
return this.http.authedRequest(undefined, Method.Put, path, undefined, {
publicise: isPublic,
});
}
/**
* @experimental
*/
@@ -9201,74 +8864,21 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return threadRoots;
}
private eventShouldLiveIn(event: MatrixEvent, room: Room, events: MatrixEvent[], roots: Set<string>): {
shouldLiveInRoom: boolean;
shouldLiveInThread: boolean;
threadId?: string;
} {
if (event.isThreadRoot) {
return {
shouldLiveInRoom: true,
shouldLiveInThread: true,
threadId: event.getId(),
};
}
// A thread relation is always only shown in a thread
if (event.isThreadRelation) {
return {
shouldLiveInRoom: false,
shouldLiveInThread: true,
threadId: event.relationEventId,
};
}
const parentEventId = event.getAssociatedId();
const parentEvent = room?.findEventById(parentEventId) || events.find((mxEv: MatrixEvent) => (
mxEv.getId() === parentEventId
));
// A reaction targetting the thread root needs to be routed to both the
// the main timeline and the associated thread
const targetingThreadRoot = parentEvent?.isThreadRoot || roots.has(event.relationEventId);
if (targetingThreadRoot) {
return {
shouldLiveInRoom: true,
shouldLiveInThread: true,
threadId: event.relationEventId,
};
}
// If the parent event also has an associated ID we want to re-run the
// computation for that parent event.
// In the case of the redaction of a reaction that targets a root event
// we want that redaction to be pushed to both timeline
if (parentEvent?.getAssociatedId()) {
return this.eventShouldLiveIn(parentEvent, room, events, roots);
} else {
// We've exhausted all scenarios, can safely assume that this event
// should live in the room timeline
return {
shouldLiveInRoom: true,
shouldLiveInThread: false,
};
}
}
public partitionThreadedEvents(events: MatrixEvent[]): [MatrixEvent[], MatrixEvent[]] {
// Indices to the events array, for readibility
public partitionThreadedEvents(room: Room, events: MatrixEvent[]): [
timelineEvents: MatrixEvent[],
threadedEvents: MatrixEvent[],
] {
// Indices to the events array, for readability
const ROOM = 0;
const THREAD = 1;
if (this.supportsExperimentalThreads()) {
const threadRoots = this.findThreadRoots(events);
return events.reduce((memo, event: MatrixEvent) => {
const room = this.getRoom(event.getRoomId());
const {
shouldLiveInRoom,
shouldLiveInThread,
threadId,
} = this.eventShouldLiveIn(event, room, events, threadRoots);
} = room.eventShouldLiveIn(event, events, threadRoots);
if (shouldLiveInRoom) {
memo[ROOM].push(event);
@@ -9489,18 +9099,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* });
*/
/**
* Fires whenever the sdk learns about a new group. <strong>This event
* is experimental and may change.</strong>
* @event module:client~MatrixClient#"Group"
* @param {Group} group The newly created, fully populated group.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
* @example
* matrixClient.on("Group", function(group){
* var groupId = group.groupId;
* });
*/
/**
* Fires whenever a new Room is added. This will fire when you are invited to a
* room, as well as when you join a room. <strong>This event is experimental and
+144 -20
View File
@@ -16,14 +16,21 @@ limitations under the License.
/** @module ContentHelpers */
import { REFERENCE_RELATION } from "matrix-events-sdk";
import { MBeaconEventContent, MBeaconInfoContent, MBeaconInfoEventContent, M_BEACON_INFO } from "./@types/beacon";
import { MsgType } from "./@types/event";
import { TEXT_NODE_TYPE } from "./@types/extensible_events";
import {
ASSET_NODE_TYPE,
ILocationContent,
M_ASSET,
LocationAssetType,
LOCATION_EVENT_TYPE,
TIMESTAMP_NODE_TYPE,
M_LOCATION,
M_TIMESTAMP,
LocationEventWireContent,
MLocationEventContent,
MLocationContent,
MAssetContent,
LegacyLocationEventContent,
} from "./@types/location";
/**
@@ -107,35 +114,152 @@ export function makeEmoteMessage(body: string) {
};
}
/** Location content helpers */
export const getTextForLocationEvent = (
uri: string,
assetType: LocationAssetType,
timestamp: number,
description?: string,
): string => {
const date = `at ${new Date(timestamp).toISOString()}`;
const assetName = assetType === LocationAssetType.Self ? 'User' : undefined;
const quotedDescription = description ? `"${description}"` : undefined;
return [
assetName,
'Location',
quotedDescription,
uri,
date,
].filter(Boolean).join(' ');
};
/**
* Generates the content for a Location event
* @param text a text for of our location
* @param uri a geo:// uri for the location
* @param ts the timestamp when the location was correct (milliseconds since
* the UNIX epoch)
* @param description the (optional) label for this location on the map
* @param asset_type the (optional) asset type of this location e.g. "m.self"
* @param text optional. A text for the location
*/
export function makeLocationContent(
text: string,
export const makeLocationContent = (
// this is first but optional
// to avoid a breaking change
text: string | undefined,
uri: string,
ts: number,
timestamp?: number,
description?: string,
assetType?: LocationAssetType,
): ILocationContent {
): LegacyLocationEventContent & MLocationEventContent => {
const defaultedText = text ??
getTextForLocationEvent(uri, assetType || LocationAssetType.Self, timestamp, description);
const timestampEvent = timestamp ? { [M_TIMESTAMP.name]: timestamp } : {};
return {
"body": text,
"msgtype": MsgType.Location,
"geo_uri": uri,
[LOCATION_EVENT_TYPE.name]: {
uri,
msgtype: MsgType.Location,
body: defaultedText,
geo_uri: uri,
[M_LOCATION.name]: {
description,
uri,
},
[ASSET_NODE_TYPE.name]: {
type: assetType ?? LocationAssetType.Self,
[M_ASSET.name]: {
type: assetType || LocationAssetType.Self,
},
[TEXT_NODE_TYPE.name]: text,
[TIMESTAMP_NODE_TYPE.name]: ts,
// TODO: MSC1767 fallbacks m.image thumbnail
[TEXT_NODE_TYPE.name]: defaultedText,
...timestampEvent,
} as LegacyLocationEventContent & MLocationEventContent;
};
/**
* Parse location event content and transform to
* a backwards compatible modern m.location event format
*/
export const parseLocationEvent = (wireEventContent: LocationEventWireContent): MLocationEventContent => {
const location = M_LOCATION.findIn<MLocationContent>(wireEventContent);
const asset = M_ASSET.findIn<MAssetContent>(wireEventContent);
const timestamp = M_TIMESTAMP.findIn<number>(wireEventContent);
const text = TEXT_NODE_TYPE.findIn<string>(wireEventContent);
const geoUri = location?.uri ?? wireEventContent?.geo_uri;
const description = location?.description;
const assetType = asset?.type ?? LocationAssetType.Self;
const fallbackText = text ?? wireEventContent.body;
return makeLocationContent(fallbackText, geoUri, timestamp, description, assetType);
};
/**
* Beacon event helpers
*/
export type MakeBeaconInfoContent = (
timeout: number,
isLive?: boolean,
description?: string,
assetType?: LocationAssetType,
timestamp?: number
) => MBeaconInfoEventContent;
export const makeBeaconInfoContent: MakeBeaconInfoContent = (
timeout,
isLive,
description,
assetType,
timestamp,
) => ({
[M_BEACON_INFO.name]: {
description,
timeout,
live: isLive,
},
[M_TIMESTAMP.name]: timestamp || Date.now(),
[M_ASSET.name]: {
type: assetType ?? LocationAssetType.Self,
},
});
export type BeaconInfoState = MBeaconInfoContent & {
assetType: LocationAssetType;
timestamp: number;
};
/**
* Flatten beacon info event content
*/
export const parseBeaconInfoContent = (content: MBeaconInfoEventContent): BeaconInfoState => {
const { description, timeout, live } = M_BEACON_INFO.findIn<MBeaconInfoContent>(content);
const { type: assetType } = M_ASSET.findIn<MAssetContent>(content);
const timestamp = M_TIMESTAMP.findIn<number>(content);
return {
description,
timeout,
live,
assetType,
timestamp,
};
}
};
export type MakeBeaconContent = (
uri: string,
timestamp: number,
beaconInfoId: string,
description?: string,
) => MBeaconEventContent;
export const makeBeaconContent: MakeBeaconContent = (
uri,
timestamp,
beaconInfoId,
description,
) => ({
[M_LOCATION.name]: {
description,
uri,
},
[M_TIMESTAMP.name]: timestamp,
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: beaconInfoId,
},
});
+1
View File
@@ -709,6 +709,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
}
await this.baseApis.sendToDevice("org.matrix.room_key.withheld", contentMap);
await this.baseApis.sendToDevice("m.room_key.withheld", contentMap);
// record the fact that we notified these blocked devices
for (const userId of Object.keys(contentMap)) {
+3 -2
View File
@@ -920,7 +920,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
// secrets using it, in theory. We could move them to the new key but a)
// that would mean we'd need to prompt for the old passphrase, and b)
// it's not clear that would be the right thing to do anyway.
const { keyInfo, privateKey } = await createSecretStorageKey();
const { keyInfo = {} as IAddSecretStorageKeyOpts, privateKey } = await createSecretStorageKey();
newKeyId = await createSSSS(keyInfo, privateKey);
} else if (!storageExists && keyBackupInfo) {
// we have an existing backup, but no SSSS
@@ -3115,7 +3115,8 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
this.secretStorage.onRequestReceived(event);
} else if (event.getType() === "m.secret.send") {
this.secretStorage.onSecretReceived(event);
} else if (event.getType() === "org.matrix.room_key.withheld") {
} else if (event.getType() === "m.room_key.withheld"
|| event.getType() === "org.matrix.room_key.withheld") {
this.onRoomKeyWithheldEvent(event);
} else if (event.getContent().transaction_id) {
this.onKeyVerificationMessage(event);
@@ -325,7 +325,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
});
},
);
if (limit && session.length >= limit) {
if (limit && sessions.length >= limit) {
break;
}
}
@@ -112,7 +112,7 @@ export class VerificationRequest<
private commonMethods: VerificationMethod[] = [];
private _phase: Phase;
private _cancellingUserId: string;
public _cancellingUserId: string; // Used in tests only
private _verifier: VerificationBase<any, any>;
constructor(
+16 -2
View File
@@ -29,9 +29,22 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
const decrypt = options.decrypt !== false;
function mapper(plainOldJsObject: Partial<IEvent>) {
const event = new MatrixEvent(plainOldJsObject);
const room = client.getRoom(plainOldJsObject.room_id);
let event: MatrixEvent;
// If the event is already known to the room, let's re-use the model rather than duplicating.
// We avoid doing this to state events as they may be forward or backwards looking which tweaks behaviour.
if (room && plainOldJsObject.state_key === undefined) {
event = room.findEventById(plainOldJsObject.event_id);
}
if (!event || event.status) {
event = new MatrixEvent(plainOldJsObject);
} else {
// merge the latest unsigned data from the server
event.setUnsigned({ ...event.getUnsigned(), ...plainOldJsObject.unsigned });
}
const room = client.getRoom(event.getRoomId());
if (room?.threads.has(event.getId())) {
event.setThread(room.threads.get(event.getId()));
}
@@ -46,6 +59,7 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
client.decryptEventIfNeeded(event);
}
}
if (!preventReEmit) {
client.reEmitter.reEmit(event, [
MatrixEventEvent.Replaced,
+25 -20
View File
@@ -15,11 +15,12 @@ limitations under the License.
*/
import { RelationType } from "./@types/event";
import {
UNSTABLE_FILTER_RELATED_BY_REL_TYPES,
UNSTABLE_FILTER_RELATED_BY_SENDERS,
} from "./filter";
import { MatrixEvent } from "./models/event";
import {
FILTER_RELATED_BY_REL_TYPES,
FILTER_RELATED_BY_SENDERS,
THREAD_RELATION_TYPE,
} from "./models/thread";
/**
* @module filter-component
@@ -51,8 +52,12 @@ export interface IFilterComponent {
not_senders?: string[];
contains_url?: boolean;
limit?: number;
[UNSTABLE_FILTER_RELATED_BY_SENDERS.name]?: string[];
[UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name]?: Array<RelationType | string>;
related_by_senders?: Array<RelationType | string>;
related_by_rel_types?: string[];
// Unstable values
"io.element.relation_senders"?: Array<RelationType | string>;
"io.element.relation_types"?: string[];
}
/* eslint-enable camelcase */
@@ -84,7 +89,7 @@ export class FilterComponent {
// of performance
// This should be improved when bundled relationships solve that problem
const relationSenders = [];
if (this.userId && bundledRelationships?.[RelationType.Thread]?.current_user_participated) {
if (this.userId && bundledRelationships?.[THREAD_RELATION_TYPE.name]?.current_user_participated) {
relationSenders.push(this.userId);
}
@@ -103,15 +108,15 @@ export class FilterComponent {
*/
public toJSON(): object {
return {
types: this.filterJson.types || null,
not_types: this.filterJson.not_types || [],
rooms: this.filterJson.rooms || null,
not_rooms: this.filterJson.not_rooms || [],
senders: this.filterJson.senders || null,
not_senders: this.filterJson.not_senders || [],
contains_url: this.filterJson.contains_url || null,
[UNSTABLE_FILTER_RELATED_BY_SENDERS.name]: UNSTABLE_FILTER_RELATED_BY_SENDERS.findIn(this.filterJson),
[UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name]: UNSTABLE_FILTER_RELATED_BY_REL_TYPES.findIn(this.filterJson),
"types": this.filterJson.types || null,
"not_types": this.filterJson.not_types || [],
"rooms": this.filterJson.rooms || null,
"not_rooms": this.filterJson.not_rooms || [],
"senders": this.filterJson.senders || null,
"not_senders": this.filterJson.not_senders || [],
"contains_url": this.filterJson.contains_url || null,
[FILTER_RELATED_BY_SENDERS.name]: this.filterJson[FILTER_RELATED_BY_SENDERS.name] || [],
[FILTER_RELATED_BY_REL_TYPES.name]: this.filterJson[FILTER_RELATED_BY_REL_TYPES.name] || [],
};
}
@@ -165,14 +170,14 @@ export class FilterComponent {
return false;
}
const relationTypesFilter = this.filterJson[UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name];
const relationTypesFilter = this.filterJson[FILTER_RELATED_BY_REL_TYPES.name];
if (relationTypesFilter !== undefined) {
if (!this.arrayMatchesFilter(relationTypesFilter, relationTypes)) {
return false;
}
}
const relationSendersFilter = this.filterJson[UNSTABLE_FILTER_RELATED_BY_SENDERS.name];
const relationSendersFilter = this.filterJson[FILTER_RELATED_BY_SENDERS.name];
if (relationSendersFilter !== undefined) {
if (!this.arrayMatchesFilter(relationSendersFilter, relationSenders)) {
return false;
@@ -183,8 +188,8 @@ export class FilterComponent {
}
private arrayMatchesFilter(filter: any[], values: any[]): boolean {
return values.length > 0 && values.every(value => {
return filter.includes(value);
return values.length > 0 && filter.every(value => {
return values.includes(value);
});
}
+6 -13
View File
@@ -24,17 +24,6 @@ import {
} from "./@types/event";
import { FilterComponent, IFilterComponent } from "./filter-component";
import { MatrixEvent } from "./models/event";
import { UnstableValue } from "./NamespacedValue";
export const UNSTABLE_FILTER_RELATED_BY_SENDERS = new UnstableValue(
"related_by_senders",
"io.element.relation_senders",
);
export const UNSTABLE_FILTER_RELATED_BY_REL_TYPES = new UnstableValue(
"related_by_rel_types",
"io.element.relation_types",
);
/**
* @param {Object} obj
@@ -66,8 +55,12 @@ export interface IRoomEventFilter extends IFilterComponent {
lazy_load_members?: boolean;
include_redundant_members?: boolean;
types?: Array<EventType | string>;
[UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name]?: Array<RelationType | string>;
[UNSTABLE_FILTER_RELATED_BY_SENDERS.name]?: string[];
related_by_senders?: Array<RelationType | string>;
related_by_rel_types?: string[];
// Unstable values
"io.element.relation_senders"?: Array<RelationType | string>;
"io.element.relation_types"?: string[];
}
interface IStateFilter extends IRoomEventFilter {}
+1 -1
View File
@@ -1079,7 +1079,7 @@ export class MatrixError extends Error {
* @constructor
*/
export class ConnectionError extends Error {
constructor(message: string, private readonly cause: Error = undefined) {
constructor(message: string, cause: Error = undefined) {
super(message + (cause ? `: ${cause.message}` : ""));
}
+1 -1
View File
@@ -27,9 +27,9 @@ export * from "./http-api";
export * from "./autodiscovery";
export * from "./sync-accumulator";
export * from "./errors";
export * from "./models/beacon";
export * from "./models/event";
export * from "./models/room";
export * from "./models/group";
export * from "./models/event-timeline";
export * from "./models/event-timeline-set";
export * from "./models/room-member";
+134
View File
@@ -0,0 +1,134 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { M_BEACON_INFO } from "../@types/beacon";
import { BeaconInfoState, parseBeaconInfoContent } from "../content-helpers";
import { MatrixEvent } from "../matrix";
import { TypedEventEmitter } from "./typed-event-emitter";
export enum BeaconEvent {
New = "Beacon.new",
Update = "Beacon.update",
LivenessChange = "Beacon.LivenessChange",
Destroy = "Destroy",
}
export type BeaconEventHandlerMap = {
[BeaconEvent.Update]: (event: MatrixEvent, beacon: Beacon) => void;
[BeaconEvent.LivenessChange]: (isLive: boolean, beacon: Beacon) => void;
[BeaconEvent.Destroy]: (beaconIdentifier: string) => void;
};
export const isTimestampInDuration = (
startTimestamp: number,
durationMs: number,
timestamp: number,
): boolean => timestamp >= startTimestamp && startTimestamp + durationMs >= timestamp;
export const isBeaconInfoEventType = (type: string) =>
type.startsWith(M_BEACON_INFO.name) ||
type.startsWith(M_BEACON_INFO.altName);
// https://github.com/matrix-org/matrix-spec-proposals/pull/3489
export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.New>, BeaconEventHandlerMap> {
public readonly roomId: string;
private _beaconInfo: BeaconInfoState;
private _isLive: boolean;
private livenessWatchInterval: number;
constructor(
private rootEvent: MatrixEvent,
) {
super();
this.setBeaconInfo(this.rootEvent);
this.roomId = this.rootEvent.getRoomId();
}
public get isLive(): boolean {
return this._isLive;
}
public get identifier(): string {
return this.beaconInfoEventType;
}
public get beaconInfoId(): string {
return this.rootEvent.getId();
}
public get beaconInfoOwner(): string {
return this.rootEvent.getStateKey();
}
public get beaconInfoEventType(): string {
return this.rootEvent.getType();
}
public get beaconInfo(): BeaconInfoState {
return this._beaconInfo;
}
public update(beaconInfoEvent: MatrixEvent): void {
if (beaconInfoEvent.getType() !== this.beaconInfoEventType) {
throw new Error('Invalid updating event');
}
this.rootEvent = beaconInfoEvent;
this.setBeaconInfo(this.rootEvent);
this.emit(BeaconEvent.Update, beaconInfoEvent, this);
}
public destroy(): void {
if (this.livenessWatchInterval) {
clearInterval(this.livenessWatchInterval);
}
this._isLive = false;
this.emit(BeaconEvent.Destroy, this.identifier);
}
/**
* Monitor liveness of a beacon
* Emits BeaconEvent.LivenessChange when beacon expires
*/
public monitorLiveness(): void {
if (this.livenessWatchInterval) {
clearInterval(this.livenessWatchInterval);
}
if (this.isLive) {
const expiryInMs = (this._beaconInfo?.timestamp + this._beaconInfo?.timeout + 1) - Date.now();
if (expiryInMs > 1) {
this.livenessWatchInterval = setInterval(this.checkLiveness.bind(this), expiryInMs);
}
}
}
private setBeaconInfo(event: MatrixEvent): void {
this._beaconInfo = parseBeaconInfoContent(event.getContent());
this.checkLiveness();
}
private checkLiveness(): void {
const prevLiveness = this.isLive;
this._isLive = this._beaconInfo?.live &&
isTimestampInDuration(this._beaconInfo?.timestamp, this._beaconInfo?.timeout, Date.now());
if (prevLiveness !== this.isLive) {
this.emit(BeaconEvent.LivenessChange, this.isLive, this);
}
}
}
+1 -1
View File
@@ -42,7 +42,7 @@ export class EventContext {
*
* @constructor
*/
constructor(ourEvent: MatrixEvent) {
constructor(public readonly ourEvent: MatrixEvent) {
this.timeline = [ourEvent];
}
+18 -10
View File
@@ -28,7 +28,7 @@ import { EVENT_VISIBILITY_CHANGE_TYPE, EventType, MsgType, RelationType } from "
import { Crypto, IEventDecryptionResult } from "../crypto";
import { deepSortedObjectEntries } from "../utils";
import { RoomMember } from "./room-member";
import { Thread, ThreadEvent, EventHandlerMap as ThreadEventHandlerMap } from "./thread";
import { Thread, ThreadEvent, EventHandlerMap as ThreadEventHandlerMap, THREAD_RELATION_TYPE } from "./thread";
import { IActionsObject } from '../pushprocessor';
import { TypedReEmitter } from '../ReEmitter';
import { MatrixError } from "../http-api";
@@ -86,9 +86,17 @@ export interface IEvent {
unsigned: IUnsigned;
redacts?: string;
// v1 legacy fields
/**
* @deprecated
*/
user_id?: string;
/**
* @deprecated
*/
prev_content?: IContent;
/**
* @deprecated
*/
age?: number;
}
@@ -102,11 +110,11 @@ export interface IAggregatedRelation {
}
export interface IEventRelation {
rel_type: RelationType | string;
event_id: string;
rel_type?: RelationType | string;
event_id?: string;
is_falling_back?: boolean;
"m.in_reply_to"?: {
event_id: string;
"m.render_in"?: string[];
};
key?: string;
}
@@ -279,7 +287,7 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
public target: RoomMember = null;
public status: EventStatus = null;
public error: MatrixError = null;
public forwardLooking = true;
public forwardLooking = true; // only state events may be backwards looking
/* If the event is a `m.key.verification.request` (or to_device `m.key.verification.start`) event,
* `Crypto` will set this the `VerificationRequest` for the event
@@ -478,7 +486,7 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
*
* @return {Object} The event content JSON, or an empty object.
*/
public getContent<T = IContent>(): T {
public getContent<T extends IContent = IContent>(): T {
if (this._localRedactionEvent) {
return {} as T;
} else if (this._replacingEvent) {
@@ -504,7 +512,7 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
*/
public get threadRootId(): string | undefined {
const relatesTo = this.getWireContent()?.["m.relates_to"];
if (relatesTo?.rel_type === RelationType.Thread) {
if (relatesTo?.rel_type === THREAD_RELATION_TYPE.name) {
return relatesTo.event_id;
} else {
return this.getThread()?.id || this.threadId;
@@ -523,7 +531,7 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
*/
public get isThreadRoot(): boolean {
const threadDetails = this
.getServerAggregatedRelation<IThreadBundledRelationship>(RelationType.Thread);
.getServerAggregatedRelation<IThreadBundledRelationship>(THREAD_RELATION_TYPE.name);
// Bundled relationships only returned when the sync response is limited
// hence us having to check both bundled relation and inspect the thread
@@ -1356,7 +1364,7 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
return this.status;
}
public getServerAggregatedRelation<T>(relType: RelationType): T | undefined {
public getServerAggregatedRelation<T>(relType: RelationType | string): T | undefined {
return this.getUnsigned()["m.relations"]?.[relType];
}
-100
View File
@@ -1,100 +0,0 @@
/*
Copyright 2017 New Vector Ltd
Copyright 2019 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.
*/
/**
* @module models/group
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
// eslint-disable-next-line no-restricted-imports
import { EventEmitter } from "events";
import * as utils from "../utils";
/**
* Construct a new Group.
*
* @param {string} groupId The ID of this group.
*
* @prop {string} groupId The ID of this group.
* @prop {string} name The human-readable display name for this group.
* @prop {string} avatarUrl The mxc URL for this group's avatar.
* @prop {string} myMembership The logged in user's membership of this group
* @prop {Object} inviter Infomation about the user who invited the logged in user
* to the group, if myMembership is 'invite'.
* @prop {string} inviter.userId The user ID of the inviter
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
export function Group(groupId) {
this.groupId = groupId;
this.name = null;
this.avatarUrl = null;
this.myMembership = null;
this.inviter = null;
}
utils.inherits(Group, EventEmitter);
Group.prototype.setProfile = function(name, avatarUrl) {
if (this.name === name && this.avatarUrl === avatarUrl) return;
this.name = name || this.groupId;
this.avatarUrl = avatarUrl;
this.emit("Group.profile", this);
};
Group.prototype.setMyMembership = function(membership) {
if (this.myMembership === membership) return;
this.myMembership = membership;
this.emit("Group.myMembership", this);
};
/**
* Sets the 'inviter' property. This does not emit an event (the inviter
* will only change when the user is revited / reinvited to a room),
* so set this before setting myMembership.
* @param {Object} inviter Infomation about who invited us to the room
*/
Group.prototype.setInviter = function(inviter) {
this.inviter = inviter;
};
/**
* Fires whenever a group's profile information is updated.
* This means the 'name' and 'avatarUrl' properties.
* @event module:client~MatrixClient#"Group.profile"
* @param {Group} group The group whose profile was updated.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
* @example
* matrixClient.on("Group.profile", function(group){
* var name = group.name;
* });
*/
/**
* Fires whenever the logged in user's membership status of
* the group is updated.
* @event module:client~MatrixClient#"Group.myMembership"
* @param {Group} group The group in which the user's membership changed
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
* @example
* matrixClient.on("Group.myMembership", function(group){
* var myMembership = group.myMembership;
* });
*/
+75 -1
View File
@@ -26,6 +26,8 @@ import { MatrixEvent } from "./event";
import { MatrixClient } from "../client";
import { GuestAccess, HistoryVisibility, IJoinRuleEventContent, JoinRule } from "../@types/partials";
import { TypedEventEmitter } from "./typed-event-emitter";
import { Beacon, BeaconEvent, isBeaconInfoEventType, BeaconEventHandlerMap } from "./beacon";
import { TypedReEmitter } from "../ReEmitter";
// possible statuses for out-of-band member loading
enum OobStatus {
@@ -39,6 +41,7 @@ export enum RoomStateEvent {
Members = "RoomState.members",
NewMember = "RoomState.newMember",
Update = "RoomState.update", // signals batches of updates without specificity
BeaconLiveness = "RoomState.BeaconLiveness",
}
export type RoomStateEventHandlerMap = {
@@ -46,9 +49,15 @@ export type RoomStateEventHandlerMap = {
[RoomStateEvent.Members]: (event: MatrixEvent, state: RoomState, member: RoomMember) => void;
[RoomStateEvent.NewMember]: (event: MatrixEvent, state: RoomState, member: RoomMember) => void;
[RoomStateEvent.Update]: (state: RoomState) => void;
[RoomStateEvent.BeaconLiveness]: (state: RoomState, hasLiveBeacons: boolean) => void;
[BeaconEvent.New]: (event: MatrixEvent, beacon: Beacon) => void;
};
export class RoomState extends TypedEventEmitter<RoomStateEvent, RoomStateEventHandlerMap> {
type EmittedEvents = RoomStateEvent | BeaconEvent;
type EventHandlerMap = RoomStateEventHandlerMap & BeaconEventHandlerMap;
export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
public readonly reEmitter = new TypedReEmitter<EmittedEvents, EventHandlerMap>(this);
private sentinels: Record<string, RoomMember> = {}; // userId: RoomMember
// stores fuzzy matches to a list of userIDs (applies utils.removeHiddenChars to keys)
private displayNameToUserIds: Record<string, string[]> = {};
@@ -71,6 +80,9 @@ export class RoomState extends TypedEventEmitter<RoomStateEvent, RoomStateEventH
public events = new Map<string, Map<string, MatrixEvent>>(); // Map<eventType, Map<stateKey, MatrixEvent>>
public paginationToken: string = null;
public readonly beacons = new Map<string, Beacon>();
private liveBeaconIds: string[] = [];
/**
* Construct room state.
*
@@ -232,6 +244,10 @@ export class RoomState extends TypedEventEmitter<RoomStateEvent, RoomStateEventH
return event ? event : null;
}
public get hasLiveBeacons(): boolean {
return !!this.liveBeaconIds?.length;
}
/**
* Creates a copy of this room state so that mutations to either won't affect the other.
* @return {RoomState} the copy of the room state
@@ -314,6 +330,10 @@ export class RoomState extends TypedEventEmitter<RoomStateEvent, RoomStateEventH
return;
}
if (isBeaconInfoEventType(event.getType())) {
this.setBeacon(event);
}
const lastStateEvent = this.getStateEventMatching(event);
this.setStateEvent(event);
if (event.getType() === EventType.RoomMember) {
@@ -323,6 +343,8 @@ export class RoomState extends TypedEventEmitter<RoomStateEvent, RoomStateEventH
this.emit(RoomStateEvent.Events, event, this, lastStateEvent);
});
this.onBeaconLivenessChange();
// update higher level data structures. This needs to be done AFTER the
// 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
@@ -411,6 +433,58 @@ export class RoomState extends TypedEventEmitter<RoomStateEvent, RoomStateEventH
this.events.get(event.getType()).set(event.getStateKey(), event);
}
/**
* @experimental
*/
private setBeacon(event: MatrixEvent): void {
if (this.beacons.has(event.getType())) {
const beacon = this.beacons.get(event.getType());
if (event.isRedacted()) {
beacon.destroy();
this.beacons.delete(event.getType());
return;
}
return beacon.update(event);
}
if (event.isRedacted()) {
return;
}
const beacon = new Beacon(event);
this.reEmitter.reEmit<BeaconEvent, BeaconEvent>(beacon, [
BeaconEvent.New,
BeaconEvent.Update,
BeaconEvent.Destroy,
BeaconEvent.LivenessChange,
]);
this.emit(BeaconEvent.New, event, beacon);
beacon.on(BeaconEvent.LivenessChange, this.onBeaconLivenessChange.bind(this));
this.beacons.set(beacon.beaconInfoEventType, beacon);
}
/**
* @experimental
* Check liveness of room beacons
* emit RoomStateEvent.BeaconLiveness when
* roomstate.hasLiveBeacons has changed
*/
private onBeaconLivenessChange(): void {
const prevHasLiveBeacons = !!this.liveBeaconIds?.length;
this.liveBeaconIds = Array.from(this.beacons.values())
.filter(beacon => beacon.isLive)
.map(beacon => beacon.beaconInfoId);
const hasLiveBeacons = !!this.liveBeaconIds.length;
if (prevHasLiveBeacons !== hasLiveBeacons) {
this.emit(RoomStateEvent.BeaconLiveness, this, hasLiveBeacons);
}
}
private getStateEventMatching(event: MatrixEvent): MatrixEvent | null {
return this.events.get(event.getType())?.get(event.getStateKey()) ?? null;
}
+363 -99
View File
@@ -23,7 +23,7 @@ import { Direction, EventTimeline } from "./event-timeline";
import { getHttpUriForMxc } from "../content-repo";
import * as utils from "../utils";
import { normalize } from "../utils";
import { IEvent, MatrixEvent } from "./event";
import { IEvent, IThreadBundledRelationship, MatrixEvent } from "./event";
import { EventStatus } from "./event-status";
import { RoomMember } from "./room-member";
import { IRoomSummary, RoomSummary } from "./room-summary";
@@ -32,12 +32,20 @@ import { TypedReEmitter } from '../ReEmitter';
import {
EventType, RoomCreateTypeField, RoomType, UNSTABLE_ELEMENT_FUNCTIONAL_USERS,
EVENT_VISIBILITY_CHANGE_TYPE,
RelationType,
} from "../@types/event";
import { IRoomVersionsCapability, MatrixClient, PendingEventOrdering, RoomVersionStability } from "../client";
import { GuestAccess, HistoryVisibility, JoinRule, ResizeMethod } from "../@types/partials";
import { Filter } from "../filter";
import { Filter, IFilterDefinition } from "../filter";
import { RoomState } from "./room-state";
import { Thread, ThreadEvent, EventHandlerMap as ThreadHandlerMap } from "./thread";
import {
Thread,
ThreadEvent,
EventHandlerMap as ThreadHandlerMap,
FILTER_RELATED_BY_REL_TYPES, THREAD_RELATION_TYPE,
FILTER_RELATED_BY_SENDERS,
ThreadFilterType,
} from "./thread";
import { Method } from "../http-api";
import { TypedEventEmitter } from "./typed-event-emitter";
@@ -141,6 +149,8 @@ export interface ICreateFilterOpts {
// timeline. Useful to disable for some filters that can't be achieved by the
// client in an efficient manner
prepopulateTimeline?: boolean;
useSyncEvents?: boolean;
pendingEvents?: boolean;
}
export enum RoomEvent {
@@ -159,6 +169,7 @@ export enum RoomEvent {
type EmittedEvents = RoomEvent
| ThreadEvent.New
| ThreadEvent.Update
| ThreadEvent.NewReply
| RoomEvent.Timeline
| RoomEvent.TimelineReset;
@@ -176,7 +187,7 @@ export type RoomEventHandlerMap = {
oldEventId?: string,
oldStatus?: EventStatus,
) => void;
[ThreadEvent.New]: (thread: Thread) => void;
[ThreadEvent.New]: (thread: Thread, toStartOfTimeline: boolean) => void;
} & ThreadHandlerMap;
export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap> {
@@ -190,6 +201,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
private receiptCacheByEventId: ReceiptCache = {}; // { event_id: ICachedReceipt[] }
private notificationCounts: Partial<Record<NotificationCountType, number>> = {};
private readonly timelineSets: EventTimelineSet[];
public readonly threadsTimelineSets: EventTimelineSet[] = [];
// any filtered timeline sets we're maintaining for this room
private readonly filteredTimelineSets: Record<string, EventTimelineSet> = {}; // filter_id: timelineSet
private readonly pendingEventList?: MatrixEvent[];
@@ -363,6 +375,26 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
}
}
private threadTimelineSetsPromise: Promise<[EventTimelineSet, EventTimelineSet]> | null = null;
public async createThreadsTimelineSets(): Promise<[EventTimelineSet, EventTimelineSet]> {
if (this.threadTimelineSetsPromise) {
return this.threadTimelineSetsPromise;
}
if (this.client?.supportsExperimentalThreads) {
try {
this.threadTimelineSetsPromise = Promise.all([
this.createThreadTimelineSet(),
this.createThreadTimelineSet(ThreadFilterType.My),
]);
const timelineSets = await this.threadTimelineSetsPromise;
this.threadsTimelineSets.push(...timelineSets);
} catch (e) {
this.threadTimelineSetsPromise = null;
}
}
}
/**
* Bulk decrypt critical events in a room
*
@@ -973,17 +1005,15 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
}
/**
* Get an event which is stored in our unfiltered timeline set or in a thread
* Get an event which is stored in our unfiltered timeline set, or in a thread
*
* @param {string} eventId event ID to look for
* @param {string} eventId event ID to look for
* @return {?module:models/event.MatrixEvent} the given event, or undefined if unknown
*/
public findEventById(eventId: string): MatrixEvent | undefined {
let event = this.getUnfilteredTimelineSet().findEventById(eventId);
if (event) {
return event;
} else {
if (!event) {
const threads = this.getThreads();
for (let i = 0; i < threads.length; i++) {
const thread = threads[i];
@@ -993,6 +1023,8 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
}
}
}
return event;
}
/**
@@ -1172,10 +1204,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
timeline: EventTimeline,
paginationToken?: string,
): void {
timeline.getTimelineSet().addEventsToTimeline(
events, toStartOfTimeline,
timeline, paginationToken,
);
timeline.getTimelineSet().addEventsToTimeline(events, toStartOfTimeline, timeline, paginationToken);
}
/**
@@ -1315,19 +1344,25 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
*/
public getOrCreateFilteredTimelineSet(
filter: Filter,
{ prepopulateTimeline = true }: ICreateFilterOpts = {},
{
prepopulateTimeline = true,
useSyncEvents = true,
pendingEvents = true,
}: ICreateFilterOpts = {},
): EventTimelineSet {
if (this.filteredTimelineSets[filter.filterId]) {
return this.filteredTimelineSets[filter.filterId];
}
const opts = Object.assign({ filter: filter }, this.opts);
const opts = Object.assign({ filter, pendingEvents }, this.opts);
const timelineSet = new EventTimelineSet(this, opts);
this.reEmitter.reEmit(timelineSet, [
RoomEvent.Timeline,
RoomEvent.TimelineReset,
]);
this.filteredTimelineSets[filter.filterId] = timelineSet;
this.timelineSets.push(timelineSet);
if (useSyncEvents) {
this.filteredTimelineSets[filter.filterId] = timelineSet;
this.timelineSets.push(timelineSet);
}
const unfilteredLiveTimeline = this.getLiveTimeline();
// Not all filter are possible to replicate client-side only
@@ -1355,7 +1390,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
timeline.getPaginationToken(EventTimeline.BACKWARDS),
EventTimeline.BACKWARDS,
);
} else {
} else if (useSyncEvents) {
const livePaginationToken = unfilteredLiveTimeline.getPaginationToken(Direction.Forward);
timelineSet
.getLiveTimeline()
@@ -1373,6 +1408,146 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
return timelineSet;
}
private async getThreadListFilter(filterType = ThreadFilterType.All): Promise<Filter> {
const myUserId = this.client.getUserId();
const filter = new Filter(myUserId);
const definition: IFilterDefinition = {
"room": {
"timeline": {
[FILTER_RELATED_BY_REL_TYPES.name]: [THREAD_RELATION_TYPE.name],
},
},
};
if (filterType === ThreadFilterType.My) {
definition.room.timeline[FILTER_RELATED_BY_SENDERS.name] = [myUserId];
}
filter.setDefinition(definition);
const filterId = await this.client.getOrCreateFilter(
`THREAD_PANEL_${this.roomId}_${filterType}`,
filter,
);
filter.filterId = filterId;
return filter;
}
private async createThreadTimelineSet(filterType?: ThreadFilterType): Promise<EventTimelineSet> {
let timelineSet: EventTimelineSet;
if (Thread.hasServerSideSupport) {
const filter = await this.getThreadListFilter(filterType);
timelineSet = this.getOrCreateFilteredTimelineSet(
filter,
{
prepopulateTimeline: false,
useSyncEvents: false,
pendingEvents: false,
},
);
} else {
timelineSet = new EventTimelineSet(this, {
pendingEvents: false,
});
Array.from(this.threads)
.forEach(([, thread]) => {
if (thread.length === 0) return;
const currentUserParticipated = thread.events.some(event => {
return event.getSender() === this.client.getUserId();
});
if (filterType !== ThreadFilterType.My || currentUserParticipated) {
timelineSet.getLiveTimeline().addEvent(thread.rootEvent, false);
}
});
}
return timelineSet;
}
public threadsReady = false;
public async fetchRoomThreads(): Promise<void> {
if (this.threadsReady) {
return;
}
const allThreadsFilter = await this.getThreadListFilter();
const { chunk: events } = await this.client.createMessagesRequest(
this.roomId,
"",
Number.MAX_SAFE_INTEGER,
Direction.Backward,
allThreadsFilter,
);
if (!events.length) return;
// Sorted by last_reply origin_server_ts
const threadRoots = events
.map(this.client.getEventMapper())
.sort((eventA, eventB) => {
/**
* `origin_server_ts` in a decentralised world is far from ideal
* but for lack of any better, we will have to use this
* Long term the sorting should be handled by homeservers and this
* is only meant as a short term patch
*/
const threadAMetadata = eventA
.getServerAggregatedRelation<IThreadBundledRelationship>(RelationType.Thread);
const threadBMetadata = eventB
.getServerAggregatedRelation<IThreadBundledRelationship>(RelationType.Thread);
return threadAMetadata.latest_event.origin_server_ts - threadBMetadata.latest_event.origin_server_ts;
});
let latestMyThreadsRootEvent: MatrixEvent;
const roomState = this.getLiveTimeline().getState(EventTimeline.FORWARDS);
for (const rootEvent of threadRoots) {
this.threadsTimelineSets[0].addLiveEvent(
rootEvent,
DuplicateStrategy.Ignore,
false,
roomState,
);
const threadRelationship = rootEvent
.getServerAggregatedRelation<IThreadBundledRelationship>(RelationType.Thread);
if (threadRelationship.current_user_participated) {
this.threadsTimelineSets[1].addLiveEvent(
rootEvent,
DuplicateStrategy.Ignore,
false,
roomState,
);
latestMyThreadsRootEvent = rootEvent;
}
if (!this.getThread(rootEvent.getId())) {
this.createThread(rootEvent, [], true);
}
}
this.client.decryptEventIfNeeded(threadRoots[threadRoots.length -1]);
if (latestMyThreadsRootEvent) {
this.client.decryptEventIfNeeded(latestMyThreadsRootEvent);
}
this.threadsReady = true;
this.on(ThreadEvent.NewReply, this.onThreadNewReply);
}
private onThreadNewReply(thread: Thread): void {
for (const timelineSet of this.threadsTimelineSets) {
timelineSet.removeEvent(thread.id);
timelineSet.addLiveEvent(thread.rootEvent);
}
}
/**
* Forget the timelineSet for this room with the given filter
*
@@ -1387,39 +1562,82 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
}
}
public findThreadForEvent(event: MatrixEvent): Thread | null {
if (!event) {
return null;
public eventShouldLiveIn(event: MatrixEvent, events?: MatrixEvent[], roots?: Set<string>): {
shouldLiveInRoom: boolean;
shouldLiveInThread: boolean;
threadId?: string;
} {
// A thread root is always shown in both timelines
if (event.isThreadRoot || roots?.has(event.getId())) {
return {
shouldLiveInRoom: true,
shouldLiveInThread: true,
threadId: event.getId(),
};
}
// A thread relation is always only shown in a thread
if (event.isThreadRelation) {
return this.threads.get(event.threadRootId);
} else if (event.isThreadRoot) {
return this.threads.get(event.getId());
} else {
const parentEvent = this.findEventById(event.getAssociatedId());
return this.findThreadForEvent(parentEvent);
return {
shouldLiveInRoom: false,
shouldLiveInThread: true,
threadId: event.relationEventId,
};
}
const parentEventId = event.getAssociatedId();
const parentEvent = this.findEventById(parentEventId) ?? events?.find(e => e.getId() === parentEventId);
// Treat relations and redactions as extensions of their parents so evaluate parentEvent instead
if (parentEvent && (event.isRelation() || event.isRedaction())) {
return this.eventShouldLiveIn(parentEvent, events, roots);
}
// Edge case where we know the event is a relation but don't have the parentEvent
if (roots?.has(event.relationEventId)) {
return {
shouldLiveInRoom: true,
shouldLiveInThread: true,
threadId: event.relationEventId,
};
}
// A reply directly to a thread response is shown as part of the thread only, this is to provide a better
// experience when communicating with users using clients without full threads support
if (parentEvent?.isThreadRelation) {
return {
shouldLiveInRoom: false,
shouldLiveInThread: true,
threadId: parentEvent.threadRootId,
};
}
// We've exhausted all scenarios, can safely assume that this event should live in the room timeline only
return {
shouldLiveInRoom: true,
shouldLiveInThread: false,
};
}
/**
* Add an event to a thread's timeline. Will fire "Thread.update"
* @experimental
*/
public async addThreadedEvent(event: MatrixEvent, toStartOfTimeline: boolean): Promise<void> {
let thread = this.findThreadForEvent(event);
if (thread) {
thread.addEvent(event, toStartOfTimeline);
} else {
const events = [event];
let rootEvent = this.findEventById(event.threadRootId);
// If the rootEvent does not exist in the current sync, then look for
// it over the network
public findThreadForEvent(event?: MatrixEvent): Thread | null {
if (!event) return null;
const { threadId } = this.eventShouldLiveIn(event);
return threadId ? this.getThread(threadId) : null;
}
public async createThreadFetchRoot(
threadId: string,
events?: MatrixEvent[],
toStartOfTimeline?: boolean,
): Promise<Thread> {
let thread = this.getThread(threadId);
if (!thread) {
let rootEvent = this.findEventById(threadId);
// If the rootEvent does not exist in the local stores, then fetch it from the server.
try {
let eventData;
if (event.threadRootId) {
eventData = await this.client.fetchRoomEvent(this.roomId, event.threadRootId);
}
const eventData = await this.client.fetchRoomEvent(this.roomId, threadId);
if (!rootEvent) {
rootEvent = new MatrixEvent(eventData);
@@ -1427,18 +1645,40 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
rootEvent.setUnsigned(eventData.unsigned);
}
} finally {
// The root event might be not be visible to the person requesting
// it. If it wasn't fetched successfully the thread will work
// in "limited" mode and won't benefit from all the APIs a homeserver
// can provide to enhance the thread experience
thread = this.createThread(rootEvent, events);
// The root event might be not be visible to the person requesting it.
// If it wasn't fetched successfully the thread will work in "limited" mode and won't
// benefit from all the APIs a homeserver can provide to enhance the thread experience
thread = this.createThread(rootEvent, events, toStartOfTimeline);
if (thread) {
rootEvent.setThread(thread);
}
}
}
return thread;
}
/**
* Add an event to a thread's timeline. Will fire "Thread.update"
* @experimental
*/
public async addThreadedEvent(event: MatrixEvent, toStartOfTimeline: boolean): Promise<void> {
this.applyRedaction(event);
let thread = this.findThreadForEvent(event);
if (thread) {
await thread.addEvent(event, toStartOfTimeline);
} else {
thread = await this.createThreadFetchRoot(event.threadRootId, [event], toStartOfTimeline);
}
this.emit(ThreadEvent.Update, thread);
}
public createThread(rootEvent: MatrixEvent | undefined, events: MatrixEvent[] = []): Thread | undefined {
public createThread(
rootEvent: MatrixEvent | undefined,
events: MatrixEvent[] = [],
toStartOfTimeline: boolean,
): Thread | undefined {
if (rootEvent) {
const tl = this.getTimelineForEvent(rootEvent.getId());
const relatedEvents = tl?.getTimelineSet().getAllRelationsEventForEvent(rootEvent.getId());
@@ -1452,12 +1692,12 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
room: this,
client: this.client,
});
// If we managed to create a thread and figure out its `id`
// then we can use it
// If we managed to create a thread and figure out its `id` then we can use it
if (thread.id) {
this.threads.set(thread.id, thread);
this.reEmitter.reEmit(thread, [
ThreadEvent.Update,
ThreadEvent.NewReply,
RoomEvent.Timeline,
RoomEvent.TimelineReset,
]);
@@ -1466,22 +1706,29 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
this.lastThread = thread;
}
this.emit(ThreadEvent.New, thread);
this.emit(ThreadEvent.New, thread, toStartOfTimeline);
if (this.threadsReady) {
this.threadsTimelineSets.forEach(timelineSet => {
if (thread.rootEvent) {
if (Thread.hasServerSideSupport) {
timelineSet.addLiveEvent(thread.rootEvent);
} else {
timelineSet.addEventToTimeline(
thread.rootEvent,
timelineSet.getLiveTimeline(),
toStartOfTimeline,
);
}
}
});
}
return thread;
}
}
/**
* Add an event to the end of this room's live timelines. Will fire
* "Room.timeline".
*
* @param {MatrixEvent} event Event to be added
* @param {string?} duplicateStrategy 'ignore' or 'replace'
* @param {boolean} fromCache whether the sync response came from cache
* @fires module:client~MatrixClient#event:"Room.timeline"
* @private
*/
private addLiveEvent(event: MatrixEvent, duplicateStrategy?: DuplicateStrategy, fromCache = false): void {
private applyRedaction(event: MatrixEvent): void {
if (event.isRedaction()) {
const redactId = event.event.redacts;
@@ -1525,6 +1772,20 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
// clients can say "so and so redacted an event" if they wish to. Also
// this may be needed to trigger an update.
}
}
/**
* Add an event to the end of this room's live timelines. Will fire
* "Room.timeline".
*
* @param {MatrixEvent} event Event to be added
* @param {string?} duplicateStrategy 'ignore' or 'replace'
* @param {boolean} fromCache whether the sync response came from cache
* @fires module:client~MatrixClient#event:"Room.timeline"
* @private
*/
private addLiveEvent(event: MatrixEvent, duplicateStrategy?: DuplicateStrategy, fromCache = false): void {
this.applyRedaction(event);
// Implement MSC3531: hiding messages.
if (event.isVisibilityEvent()) {
@@ -1694,12 +1955,11 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
* @param {module:models/event.MatrixEvent} event the relation event that needs to be aggregated.
*/
private aggregateNonLiveRelation(event: MatrixEvent): void {
const thread = this.findThreadForEvent(event);
if (thread) {
thread.timelineSet.aggregateRelations(event);
}
const { shouldLiveInRoom, threadId } = this.eventShouldLiveIn(event);
const thread = this.getThread(threadId);
thread?.timelineSet.aggregateRelations(event);
if (thread?.id === event.getAssociatedId() || !thread) {
if (shouldLiveInRoom) {
// TODO: We should consider whether this means it would be a better
// design to lift the relations handling up to the room instead.
for (let i = 0; i < this.timelineSets.length; i++) {
@@ -1755,12 +2015,11 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
// any, which is good, because we don't want to try decoding it again).
localEvent.handleRemoteEcho(remoteEvent.event);
const thread = this.findThreadForEvent(remoteEvent);
if (thread) {
thread.timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId);
}
const { shouldLiveInRoom, threadId } = this.eventShouldLiveIn(remoteEvent);
const thread = this.getThread(threadId);
thread?.timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId);
if (thread?.id === remoteEvent.getAssociatedId() || !thread) {
if (shouldLiveInRoom) {
for (let i = 0; i < this.timelineSets.length; i++) {
const timelineSet = this.timelineSets[i];
@@ -1826,11 +2085,11 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
// update the event id
event.replaceLocalEventId(newEventId);
const thread = this.findThreadForEvent(event);
if (thread) {
thread.timelineSet.replaceEventId(oldEventId, newEventId);
}
if (thread?.id === event.getAssociatedId() || !thread) {
const { shouldLiveInRoom, threadId } = this.eventShouldLiveIn(event);
const thread = this.getThread(threadId);
thread?.timelineSet.replaceEventId(oldEventId, newEventId);
if (shouldLiveInRoom) {
// if the event was already in the timeline (which will be the case if
// opts.pendingEventOrdering==chronological), we need to update the
// timeline map.
@@ -1841,12 +2100,10 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
} else if (newStatus == EventStatus.CANCELLED) {
// remove it from the pending event list, or the timeline.
if (this.pendingEventList) {
const idx = this.pendingEventList.findIndex(ev => ev.getId() === oldEventId);
if (idx !== -1) {
const [removedEvent] = this.pendingEventList.splice(idx, 1);
if (removedEvent.isRedaction()) {
this.revertRedactionLocalEcho(removedEvent);
}
const removedEvent = this.getPendingEvent(oldEventId);
this.removePendingEvent(oldEventId);
if (removedEvent.isRedaction()) {
this.revertRedactionLocalEcho(removedEvent);
}
}
this.removeEvent(oldEventId);
@@ -1892,13 +2149,12 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
* @throws If <code>duplicateStrategy</code> is not falsey, 'replace' or 'ignore'.
*/
public addLiveEvents(events: MatrixEvent[], duplicateStrategy?: DuplicateStrategy, fromCache = false): void {
let i;
if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) {
throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'");
}
// sanity check that the live timeline is still live
for (i = 0; i < this.timelineSets.length; i++) {
for (let i = 0; i < this.timelineSets.length; i++) {
const liveTimeline = this.timelineSets[i].getLiveTimeline();
if (liveTimeline.getPaginationToken(EventTimeline.FORWARDS)) {
throw new Error(
@@ -1907,21 +2163,13 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
);
}
if (liveTimeline.getNeighbouringTimeline(EventTimeline.FORWARDS)) {
throw new Error(
"live timeline " + i + " is no longer live - " +
"it has a neighbouring timeline",
);
throw new Error(`live timeline ${i} is no longer live - it has a neighbouring timeline`);
}
}
for (i = 0; i < events.length; i++) {
// TODO: We should have a filter to say "only add state event
// types X Y Z to the timeline".
for (let i = 0; i < events.length; i++) {
// 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.findThreadForEvent(events[i]);
if (thread) {
thread.addEvent(events[i], true);
}
}
}
@@ -2314,7 +2562,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
/**
* Returns the type of the room from the `m.room.create` event content or undefined if none is set
* @returns {?string} the type of the room. Currently only RoomType.Space is known.
* @returns {?string} the type of the room.
*/
public getType(): RoomType | string | undefined {
const createEvent = this.currentState.getStateEvents(EventType.RoomCreate, "");
@@ -2336,6 +2584,22 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
return this.getType() === RoomType.Space;
}
/**
* Returns whether the room is a call-room as defined by MSC3417.
* @returns {boolean} true if the room's type is RoomType.UnstableCall
*/
public isCallRoom(): boolean {
return this.getType() === RoomType.UnstableCall;
}
/**
* Returns whether the room is a video room.
* @returns {boolean} true if the room's type is RoomType.ElementVideo
*/
public isElementVideoRoom(): boolean {
return this.getType() === RoomType.ElementVideo;
}
/**
* This is an internal method. Calculates the name of the room from the current
* room state.
+9 -4
View File
@@ -33,14 +33,19 @@ export class SearchResult {
public static fromJson(jsonObj: ISearchResult, eventMapper: EventMapper): SearchResult {
const jsonContext = jsonObj.context || {} as IResultContext;
const eventsBefore = jsonContext.events_before || [];
const eventsAfter = jsonContext.events_after || [];
let eventsBefore = (jsonContext.events_before || []).map(eventMapper);
let eventsAfter = (jsonContext.events_after || []).map(eventMapper);
const context = new EventContext(eventMapper(jsonObj.result));
// Filter out any contextual events which do not correspond to the same timeline (thread or room)
const threadRootId = context.ourEvent.threadRootId;
eventsBefore = eventsBefore.filter(e => e.threadRootId === threadRootId);
eventsAfter = eventsAfter.filter(e => e.threadRootId === threadRootId);
context.setPaginateToken(jsonContext.start, true);
context.addEvents(eventsBefore.map(eventMapper), true);
context.addEvents(eventsAfter.map(eventMapper), false);
context.addEvents(eventsBefore, true);
context.addEvents(eventsAfter, false);
context.setPaginateToken(jsonContext.end, false);
return new SearchResult(jsonObj.rank, context);
+71 -48
View File
@@ -14,9 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixClient, RoomEvent } from "../matrix";
import { MatrixClient, RelationType, RoomEvent } from "../matrix";
import { TypedReEmitter } from "../ReEmitter";
import { RelationType } from "../@types/event";
import { IRelationsRequestOpts } from "../@types/requests";
import { IThreadBundledRelationship, MatrixEvent } from "./event";
import { Direction, EventTimeline } from "./event-timeline";
@@ -24,6 +23,8 @@ import { EventTimelineSet, EventTimelineSetHandlerMap } from './event-timeline-s
import { Room } from './room';
import { TypedEventEmitter } from "./typed-event-emitter";
import { RoomState } from "./room-state";
import { ServerControlledNamespacedValue } from "../NamespacedValue";
import { logger } from "../logger";
export enum ThreadEvent {
New = "Thread.new",
@@ -53,7 +54,6 @@ interface IThreadOpts {
*/
export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
public static hasServerSideSupport: boolean;
private static serverSupportPromise: Promise<boolean> | null;
/**
* A reference to all the events ID at the bottom of the threads
@@ -94,23 +94,9 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
RoomEvent.TimelineReset,
]);
if (Thread.hasServerSideSupport === undefined) {
Thread.serverSupportPromise = this.client.doesServerSupportUnstableFeature("org.matrix.msc3440");
Thread.serverSupportPromise.then((serverSupportsThread) => {
Thread.hasServerSideSupport = serverSupportsThread;
}).catch(() => {
Thread.serverSupportPromise = null;
});
}
// If we weren't able to find the root event, it's probably missing
// If we weren't able to find the root event, it's probably missing,
// and we define the thread ID from one of the thread relation
if (!rootEvent) {
this.id = opts?.initialEvents
?.find(event => event.isThreadRelation)?.relationEventId;
} else {
this.id = rootEvent.getId();
}
this.id = rootEvent?.getId() ?? opts?.initialEvents?.find(event => event.isThreadRelation)?.relationEventId;
this.initialiseThread(this.rootEvent);
opts?.initialEvents?.forEach(event => this.addEvent(event, false));
@@ -119,6 +105,15 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
this.room.on(RoomEvent.Timeline, this.onEcho);
}
public static setServerSideSupport(hasServerSideSupport: boolean, useStable: boolean): void {
Thread.hasServerSideSupport = hasServerSideSupport;
if (!useStable) {
FILTER_RELATED_BY_SENDERS.setPreferUnstable(true);
FILTER_RELATED_BY_REL_TYPES.setPreferUnstable(true);
THREAD_RELATION_TYPE.setPreferUnstable(true);
}
}
private onEcho = (event: MatrixEvent) => {
if (this.timelineSet.eventIdToTimeline(event.getId())) {
this.emit(ThreadEvent.Update, this);
@@ -159,10 +154,6 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
* to the start (and not the end) of the timeline.
*/
public async addEvent(event: MatrixEvent, toStartOfTimeline: boolean): Promise<void> {
if (Thread.hasServerSideSupport === undefined) {
await Thread.serverSupportPromise;
}
// Add all incoming events to the thread's timeline set when there's no server support
if (!Thread.hasServerSideSupport) {
// all the relevant membership info to hydrate events with a sender
@@ -174,19 +165,19 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
this.addEventToTimeline(event, toStartOfTimeline);
await this.client.decryptEventIfNeeded(event, {});
}
if (Thread.hasServerSideSupport && this.initialEventsFetched) {
if (event.localTimestamp > this.lastReply().localTimestamp) {
this.addEventToTimeline(event, false);
}
} else if (!toStartOfTimeline &&
this.initialEventsFetched &&
event.localTimestamp > this.lastReply().localTimestamp
) {
await this.fetchEditsWhereNeeded(event);
this.addEventToTimeline(event, false);
}
if (!this._currentUserParticipated && event.getSender() === this.client.getUserId()) {
this._currentUserParticipated = true;
}
const isThreadReply = event.getRelation()?.rel_type === RelationType.Thread;
const isThreadReply = event.getRelation()?.rel_type === THREAD_RELATION_TYPE.name;
// If no thread support exists we want to count all thread relation
// added as a reply. We can't rely on the bundled relationships count
if (!Thread.hasServerSideSupport && isThreadReply) {
@@ -196,7 +187,10 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
// There is a risk that the `localTimestamp` approximation will not be accurate
// when threads are used over federation. That could results in the reply
// count value drifting away from the value returned by the server
if (!this.lastEvent || (isThreadReply && event.localTimestamp > this.replyToEvent.localTimestamp)) {
if (!this.lastEvent || (isThreadReply
&& (event.getId() !== this.lastEvent.getId())
&& (event.localTimestamp > this.lastEvent.localTimestamp))
) {
this.lastEvent = event;
if (this.lastEvent.getId() !== this.id) {
// This counting only works when server side support is enabled
@@ -214,15 +208,8 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
}
private initialiseThread(rootEvent: MatrixEvent | undefined): void {
if (Thread.hasServerSideSupport === undefined) {
Thread.serverSupportPromise.then(() => {
this.initialiseThread(rootEvent);
});
return;
}
const bundledRelationship = rootEvent
?.getServerAggregatedRelation<IThreadBundledRelationship>(RelationType.Thread);
?.getServerAggregatedRelation<IThreadBundledRelationship>(THREAD_RELATION_TYPE.name);
if (Thread.hasServerSideSupport && bundledRelationship) {
this.replyCount = bundledRelationship.count;
@@ -230,24 +217,39 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
const event = new MatrixEvent(bundledRelationship.latest_event);
this.setEventMetadata(event);
event.setThread(this);
this.lastEvent = event;
this.fetchEditsWhereNeeded(event);
}
}
// XXX: Workaround for https://github.com/matrix-org/matrix-spec-proposals/pull/2676/files#r827240084
private async fetchEditsWhereNeeded(...events: MatrixEvent[]): Promise<unknown> {
return Promise.all(events.filter(e => e.isEncrypted()).map((event: MatrixEvent) => {
return this.client.relations(this.roomId, event.getId(), RelationType.Replace, event.getType(), {
limit: 1,
}).then(relations => {
if (relations.events.length) {
event.makeReplaced(relations.events[0]);
}
}).catch(e => {
logger.error("Failed to load edits for encrypted thread event", e);
});
}));
}
public async fetchInitialEvents(): Promise<{
originalEvent: MatrixEvent;
events: MatrixEvent[];
nextBatch?: string;
prevBatch?: string;
} | null> {
if (Thread.hasServerSideSupport === undefined) {
await Thread.serverSupportPromise;
}
if (!Thread.hasServerSideSupport) {
this.initialEventsFetched = true;
return null;
}
try {
const response = await this.fetchEvents();
this.initialEventsFetched = true;
@@ -266,6 +268,11 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
* Finds an event by ID in the current thread
*/
public findEventById(eventId: string) {
// Check the lastEvent as it may have been created based on a bundled relationship and not in a timeline
if (this.lastEvent?.getId() === eventId) {
return this.lastEvent;
}
return this.timelineSet.findEventById(eventId);
}
@@ -323,10 +330,6 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
nextBatch?: string;
prevBatch?: string;
}> {
if (Thread.hasServerSideSupport === undefined) {
await Thread.serverSupportPromise;
}
let {
originalEvent,
events,
@@ -335,7 +338,7 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
} = await this.client.relations(
this.room.roomId,
this.id,
RelationType.Thread,
THREAD_RELATION_TYPE.name,
null,
opts,
);
@@ -346,6 +349,8 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
events = [...events, originalEvent];
}
await this.fetchEditsWhereNeeded(...events);
await Promise.all(events.map(event => {
this.setEventMetadata(event);
return this.client.decryptEventIfNeeded(event);
@@ -368,3 +373,21 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
};
}
}
export const FILTER_RELATED_BY_SENDERS = new ServerControlledNamespacedValue(
"related_by_senders",
"io.element.relation_senders",
);
export const FILTER_RELATED_BY_REL_TYPES = new ServerControlledNamespacedValue(
"related_by_rel_types",
"io.element.relation_types",
);
export const THREAD_RELATION_TYPE = new ServerControlledNamespacedValue(
"m.thread",
"io.element.thread",
);
export enum ThreadFilterType {
"My",
"All"
}
+1 -25
View File
@@ -15,19 +15,17 @@ limitations under the License.
*/
import { EventType } from "../@types/event";
import { Group } from "../models/group";
import { Room } from "../models/room";
import { User } from "../models/user";
import { IEvent, MatrixEvent } from "../models/event";
import { Filter } from "../filter";
import { RoomSummary } from "../models/room-summary";
import { IMinimalEvent, IGroups, IRooms, ISyncResponse } from "../sync-accumulator";
import { IMinimalEvent, IRooms, ISyncResponse } from "../sync-accumulator";
import { IStartClientOpts } from "../client";
export interface ISavedSync {
nextBatch: string;
roomsData: IRooms;
groupsData: IGroups;
accountData: IMinimalEvent[];
}
@@ -53,28 +51,6 @@ export interface IStore {
*/
setSyncToken(token: string): void;
/**
* No-op.
* @param {Group} group
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
storeGroup(group: Group): void;
/**
* No-op.
* @param {string} groupId
* @return {null}
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
getGroup(groupId: string): Group | null;
/**
* No-op.
* @return {Array} An empty array.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
getGroups(): Group[];
/**
* No-op.
* @param {Room} room
+1 -5
View File
@@ -215,7 +215,6 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
this.syncAccumulator.accumulate({
next_batch: syncData.nextBatch,
rooms: syncData.roomsData,
groups: syncData.groupsData,
account_data: {
events: accountData,
},
@@ -405,7 +404,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
await Promise.all([
this.persistUserPresenceEvents(userTuples),
this.persistAccountData(syncData.accountData),
this.persistSyncData(syncData.nextBatch, syncData.roomsData, syncData.groupsData),
this.persistSyncData(syncData.nextBatch, syncData.roomsData),
]);
}
@@ -413,13 +412,11 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
* Persist rooms /sync data along with the next batch token.
* @param {string} nextBatch The next_batch /sync value.
* @param {Object} roomsData The 'rooms' /sync data from a SyncAccumulator
* @param {Object} groupsData The 'groups' /sync data from a SyncAccumulator
* @return {Promise} Resolves if the data was persisted.
*/
private persistSyncData(
nextBatch: string,
roomsData: ISyncResponse["rooms"],
groupsData: ISyncResponse["groups"],
): Promise<void> {
logger.log("Persisting sync data up to", nextBatch);
return utils.promiseTry<void>(() => {
@@ -429,7 +426,6 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
clobber: "-", // constant key so will always clobber
nextBatch,
roomsData,
groupsData,
}); // put == UPSERT
return txnAsPromise(txn).then();
});
-30
View File
@@ -20,7 +20,6 @@ limitations under the License.
*/
import { EventType } from "../@types/event";
import { Group } from "../models/group";
import { Room } from "../models/room";
import { User } from "../models/user";
import { IEvent, MatrixEvent } from "../models/event";
@@ -53,7 +52,6 @@ export interface IOpts {
*/
export class MemoryStore implements IStore {
private rooms: Record<string, Room> = {}; // roomId: Room
private groups: Record<string, Group> = {}; // groupId: Group
private users: Record<string, User> = {}; // userId: User
private syncToken: string = null;
// userId: {
@@ -90,34 +88,6 @@ export class MemoryStore implements IStore {
this.syncToken = token;
}
/**
* Store the given room.
* @param {Group} group The group to be stored
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public storeGroup(group: Group) {
this.groups[group.groupId] = group;
}
/**
* Retrieve a group by its group ID.
* @param {string} groupId The group ID.
* @return {Group} The group or null.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public getGroup(groupId: string): Group | null {
return this.groups[groupId] || null;
}
/**
* Retrieve all known groups.
* @return {Group[]} A list of groups, which may be empty.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public getGroups(): Group[] {
return Object.values(this.groups);
}
/**
* Store the given room.
* @param {Room} room The room to be stored. All properties must be stored.
-27
View File
@@ -20,7 +20,6 @@ limitations under the License.
*/
import { EventType } from "../@types/event";
import { Group } from "../models/group";
import { Room } from "../models/room";
import { User } from "../models/user";
import { IEvent, MatrixEvent } from "../models/event";
@@ -58,32 +57,6 @@ export class StubStore implements IStore {
this.fromToken = token;
}
/**
* No-op.
* @param {Group} group
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public storeGroup(group: Group) {}
/**
* No-op.
* @param {string} groupId
* @return {null}
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public getGroup(groupId: string): Group | null {
return null;
}
/**
* No-op.
* @return {Array} An empty array.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/
public getGroups(): Group[] {
return [];
}
/**
* No-op.
* @param {Room} room
-50
View File
@@ -128,12 +128,6 @@ interface IDeviceLists {
left: string[];
}
export interface IGroups {
[Category.Join]: object;
[Category.Invite]: object;
[Category.Leave]: object;
}
export interface ISyncResponse {
next_batch: string;
rooms: IRooms;
@@ -142,8 +136,6 @@ export interface ISyncResponse {
to_device?: IToDevice;
device_lists?: IDeviceLists;
device_one_time_keys_count?: Record<string, number>;
groups: IGroups; // unspecced
}
/* eslint-enable camelcase */
@@ -174,7 +166,6 @@ export interface ISyncData {
nextBatch: string;
accountData: IMinimalEvent[];
roomsData: IRooms;
groupsData: IGroups;
}
/**
@@ -197,13 +188,6 @@ export class SyncAccumulator {
// streaming from without losing events.
private nextBatch: string = null;
// { ('invite'|'join'|'leave'): $groupId: { ... sync 'group' data } }
private groups: Record<Category, object> = {
invite: {},
join: {},
leave: {},
};
/**
* @param {Object} opts
* @param {Number=} opts.maxTimelineEntries The ideal maximum number of
@@ -219,7 +203,6 @@ export class SyncAccumulator {
public accumulate(syncResponse: ISyncResponse, fromDatabase = false): void {
this.accumulateRooms(syncResponse, fromDatabase);
this.accumulateGroups(syncResponse);
this.accumulateAccountData(syncResponse);
this.nextBatch = syncResponse.next_batch;
}
@@ -505,38 +488,6 @@ export class SyncAccumulator {
}
}
/**
* Accumulate incremental /sync group data.
* @param {Object} syncResponse the complete /sync JSON
*/
private accumulateGroups(syncResponse: ISyncResponse): void {
if (!syncResponse.groups) {
return;
}
if (syncResponse.groups.invite) {
Object.keys(syncResponse.groups.invite).forEach((groupId) => {
this.accumulateGroup(groupId, Category.Invite, syncResponse.groups.invite[groupId]);
});
}
if (syncResponse.groups.join) {
Object.keys(syncResponse.groups.join).forEach((groupId) => {
this.accumulateGroup(groupId, Category.Join, syncResponse.groups.join[groupId]);
});
}
if (syncResponse.groups.leave) {
Object.keys(syncResponse.groups.leave).forEach((groupId) => {
this.accumulateGroup(groupId, Category.Leave, syncResponse.groups.leave[groupId]);
});
}
}
private accumulateGroup(groupId: string, category: Category, data: object): void {
for (const cat of [Category.Invite, Category.Leave, Category.Join]) {
delete this.groups[cat][groupId];
}
this.groups[category][groupId] = data;
}
/**
* Return everything under the 'rooms' key from a /sync response which
* represents all room data that should be stored. This should be paired
@@ -694,7 +645,6 @@ export class SyncAccumulator {
return {
nextBatch: this.nextBatch,
roomsData: data,
groupsData: this.groups,
accountData: accData,
};
}
+31 -113
View File
@@ -25,7 +25,6 @@ limitations under the License.
import { User, UserEvent } from "./models/user";
import { NotificationCountType, Room, RoomEvent } from "./models/room";
import { Group } from "./models/group";
import * as utils from "./utils";
import { IDeferred } from "./utils";
import { Filter } from "./filter";
@@ -35,7 +34,6 @@ import { logger } from './logger';
import { InvalidStoreError } from './errors';
import { ClientEvent, IStoredClientOpts, MatrixClient, PendingEventOrdering } from "./client";
import {
Category,
IEphemeral,
IInvitedRoom,
IInviteState,
@@ -55,6 +53,7 @@ import { EventType } from "./@types/event";
import { IPushRules } from "./@types/PushRules";
import { RoomStateEvent } from "./models/room-state";
import { RoomMemberEvent } from "./models/room-member";
import { BeaconEvent } from "./models/beacon";
const DEBUG = true;
@@ -212,21 +211,6 @@ export class SyncApi {
return room;
}
/**
* @param {string} groupId
* @return {Group}
*/
public createGroup(groupId: string): Group {
const client = this.client;
const group = new Group(groupId);
client.reEmitter.reEmit(group, [
ClientEvent.GroupProfile,
ClientEvent.GroupMyMembership,
]);
client.store.storeGroup(group);
return group;
}
/**
* @param {Room} room
* @private
@@ -241,7 +225,12 @@ export class SyncApi {
RoomStateEvent.Members,
RoomStateEvent.NewMember,
RoomStateEvent.Update,
BeaconEvent.New,
BeaconEvent.Update,
BeaconEvent.Destroy,
BeaconEvent.LivenessChange,
]);
room.currentState.on(RoomStateEvent.NewMember, function(event, state, member) {
member.user = client.getUser(member.userId);
client.reEmitter.reEmit(member, [
@@ -288,15 +277,13 @@ export class SyncApi {
return client.http.authedRequest<any>( // TODO types
undefined, Method.Get, "/sync", qps as any, undefined, localTimeoutMs,
);
}).then((data) => {
}).then(async (data) => {
let leaveRooms = [];
if (data.rooms?.leave) {
leaveRooms = this.mapSyncResponseToRoomArray(data.rooms.leave);
}
const rooms = [];
leaveRooms.forEach(async (leaveObj) => {
return Promise.all(leaveRooms.map(async (leaveObj) => {
const room = leaveObj.room;
rooms.push(room);
if (!leaveObj.isBrandNewRoom) {
// the intention behind syncLeftRooms is to add in rooms which were
// *omitted* from the initial /sync. Rooms the user were joined to
@@ -310,25 +297,22 @@ export class SyncApi {
}
leaveObj.timeline = leaveObj.timeline || {};
const events = this.mapSyncEventsFormat(leaveObj.timeline, room);
const [timelineEvents, threadedEvents] = this.client.partitionThreadedEvents(events);
const stateEvents = this.mapSyncEventsFormat(leaveObj.state, room);
// set the back-pagination token. Do this *before* adding any
// events so that clients can start back-paginating.
room.getLiveTimeline().setPaginationToken(leaveObj.timeline.prev_batch,
EventTimeline.BACKWARDS);
room.getLiveTimeline().setPaginationToken(leaveObj.timeline.prev_batch, EventTimeline.BACKWARDS);
this.processRoomEvents(room, stateEvents, timelineEvents);
await this.processThreadEvents(room, threadedEvents, false);
await this.processRoomEvents(room, stateEvents, events);
room.recalculate();
client.store.storeRoom(room);
client.emit(ClientEvent.Room, room);
this.processEventsForNotifs(room, events);
});
return rooms;
return room;
}));
});
}
@@ -763,7 +747,6 @@ export class SyncApi {
const data: ISyncResponse = {
next_batch: nextSyncToken,
rooms: savedSync.roomsData,
groups: savedSync.groupsData,
account_data: {
events: savedSync.accountData,
},
@@ -772,7 +755,7 @@ export class SyncApi {
try {
await this.processSyncResponse(syncEventData, data);
} catch (e) {
logger.error("Error processing cached sync", e.stack || e);
logger.error("Error processing cached sync", e);
}
// Don't emit a prepared if we've bailed because the store is invalid:
@@ -847,7 +830,7 @@ export class SyncApi {
} catch (e) {
// log the exception with stack if we have it, else fall back
// to the plain description
logger.error("Caught /sync error", e.stack || e);
logger.error("Caught /sync error", e);
// Emit the exception for client handling
this.client.emit(ClientEvent.SyncUnexpectedError, e);
@@ -1050,20 +1033,7 @@ export class SyncApi {
// timeline: { events: [], prev_batch: $token }
// }
// }
// },
// groups: {
// invite: {
// $groupId: {
// inviter: $inviter,
// profile: {
// avatar_url: $avatarUrl,
// name: $groupName,
// },
// },
// },
// join: {},
// leave: {},
// },
// }
// }
// TODO-arch:
@@ -1113,9 +1083,7 @@ export class SyncApi {
}
// handle to-device events
if (data.to_device && Array.isArray(data.to_device.events) &&
data.to_device.events.length > 0
) {
if (Array.isArray(data.to_device?.events) && data.to_device.events.length > 0) {
const cancelledKeyVerificationTxns = [];
data.to_device.events
.map(client.getEventMapper())
@@ -1167,20 +1135,6 @@ export class SyncApi {
this.catchingUp = false;
}
if (data.groups) {
if (data.groups.invite) {
this.processGroupSyncEntry(data.groups.invite, Category.Invite);
}
if (data.groups.join) {
this.processGroupSyncEntry(data.groups.join, Category.Join);
}
if (data.groups.leave) {
this.processGroupSyncEntry(data.groups.leave, Category.Leave);
}
}
// the returned json structure is a bit crap, so make it into a
// nicer form (array) after applying sanity to make sure we don't fail
// on missing keys (on the off chance)
@@ -1203,11 +1157,11 @@ export class SyncApi {
this.notifEvents = [];
// Handle invites
inviteRooms.forEach((inviteObj) => {
await utils.promiseMapSeries(inviteRooms, async (inviteObj) => {
const room = inviteObj.room;
const stateEvents = this.mapSyncEventsFormat(inviteObj.invite_state, room);
this.processRoomEvents(room, stateEvents);
await this.processRoomEvents(room, stateEvents);
if (inviteObj.isBrandNewRoom) {
room.recalculate();
client.store.storeRoom(room);
@@ -1314,10 +1268,7 @@ export class SyncApi {
}
}
const [timelineEvents, threadedEvents] = this.client.partitionThreadedEvents(events);
this.processRoomEvents(room, stateEvents, timelineEvents, syncEventData.fromCache);
await this.processThreadEvents(room, threadedEvents, false);
await this.processRoomEvents(room, stateEvents, events, syncEventData.fromCache);
// set summary after processing events,
// because it will trigger a name calculation
@@ -1358,8 +1309,7 @@ export class SyncApi {
};
await utils.promiseMapSeries(stateEvents, processRoomEvent);
await utils.promiseMapSeries(timelineEvents, processRoomEvent);
await utils.promiseMapSeries(threadedEvents, processRoomEvent);
await utils.promiseMapSeries(events, processRoomEvent);
ephemeralEvents.forEach(function(e) {
client.emit(ClientEvent.Event, e);
});
@@ -1376,16 +1326,13 @@ export class SyncApi {
});
// Handle leaves (e.g. kicked rooms)
leaveRooms.forEach(async (leaveObj) => {
await utils.promiseMapSeries(leaveRooms, async (leaveObj) => {
const room = leaveObj.room;
const stateEvents = this.mapSyncEventsFormat(leaveObj.state, room);
const events = this.mapSyncEventsFormat(leaveObj.timeline, room);
const accountDataEvents = this.mapSyncEventsFormat(leaveObj.account_data);
const [timelineEvents, threadedEvents] = this.client.partitionThreadedEvents(events);
this.processRoomEvents(room, stateEvents, timelineEvents);
await this.processThreadEvents(room, threadedEvents, false);
await this.processRoomEvents(room, stateEvents, events);
room.addAccountData(accountDataEvents);
room.recalculate();
@@ -1399,10 +1346,7 @@ export class SyncApi {
stateEvents.forEach(function(e) {
client.emit(ClientEvent.Event, e);
});
timelineEvents.forEach(function(e) {
client.emit(ClientEvent.Event, e);
});
threadedEvents.forEach(function(e) {
events.forEach(function(e) {
client.emit(ClientEvent.Event, e);
});
accountDataEvents.forEach(function(e) {
@@ -1537,35 +1481,6 @@ export class SyncApi {
});
}
/**
* @param {Object} groupsSection Groups section object, eg. response.groups.invite
* @param {string} sectionName Which section this is ('invite', 'join' or 'leave')
*/
private processGroupSyncEntry(groupsSection: object, sectionName: Category) {
// Processes entries from 'groups' section of the sync stream
for (const groupId of Object.keys(groupsSection)) {
const groupInfo = groupsSection[groupId];
let group = this.client.store.getGroup(groupId);
const isBrandNew = group === null;
if (group === null) {
group = this.createGroup(groupId);
}
if (groupInfo.profile) {
group.setProfile(
groupInfo.profile.name, groupInfo.profile.avatar_url,
);
}
if (groupInfo.inviter) {
group.setInviter({ userId: groupInfo.inviter });
}
group.setMyMembership(sectionName);
if (isBrandNew) {
// Now we've filled in all the fields, emit the Group event
this.client.emit(ClientEvent.Group, group);
}
}
}
/**
* @param {Object} obj
* @return {Object[]}
@@ -1661,16 +1576,16 @@ export class SyncApi {
* @param {Room} room
* @param {MatrixEvent[]} stateEventList A list of state events. This is the state
* at the *START* of the timeline list if it is supplied.
* @param {MatrixEvent[]} [timelineEventList] A list of timeline events. Lower index
* @param {MatrixEvent[]} [timelineEventList] A list of timeline events, including threaded. Lower index
* @param {boolean} fromCache whether the sync response came from cache
* is earlier in time. Higher index is later.
*/
private processRoomEvents(
private async processRoomEvents(
room: Room,
stateEventList: MatrixEvent[],
timelineEventList?: MatrixEvent[],
fromCache = false,
): void {
): Promise<void> {
// If there are no events in the timeline yet, initialise it with
// the given state events
const liveTimeline = room.getLiveTimeline();
@@ -1720,11 +1635,14 @@ export class SyncApi {
room.oldState.setStateEvents(stateEventList || []);
room.currentState.setStateEvents(stateEventList || []);
}
// execute the timeline events. This will continue to diverge the current state
// Execute the timeline events. This will continue to diverge the current state
// if the timeline has any state events in it.
// This also needs to be done before running push rules on the events as they need
// to be decorated with sender etc.
room.addLiveEvents(timelineEventList || [], null, fromCache);
const [mainTimelineEvents, threadedEvents] = this.client.partitionThreadedEvents(room, timelineEventList || []);
room.addLiveEvents(mainTimelineEvents, null, fromCache);
await this.processThreadEvents(room, threadedEvents, false);
}
/**
+11 -20
View File
@@ -99,11 +99,11 @@ export class TimelineWindow {
*
* @return {Promise}
*/
public load(initialEventId?: string, initialWindowSize = 20): Promise<any> {
public load(initialEventId?: string, initialWindowSize = 20): Promise<void> {
// given an EventTimeline, find the event we were looking for, and initialise our
// fields so that the event in question is in the middle of the window.
const initFields = (timeline: EventTimeline) => {
let eventIndex;
let eventIndex: number;
const events = timeline.getEvents();
@@ -111,40 +111,31 @@ export class TimelineWindow {
// we were looking for the live timeline: initialise to the end
eventIndex = events.length;
} else {
for (let i = 0; i < events.length; i++) {
if (events[i].getId() == initialEventId) {
eventIndex = i;
break;
}
}
eventIndex = events.findIndex(e => e.getId() === initialEventId);
if (eventIndex === undefined) {
if (eventIndex < 0) {
throw new Error("getEventTimeline result didn't include requested event");
}
}
const endIndex = Math.min(events.length,
eventIndex + Math.ceil(initialWindowSize / 2));
const endIndex = Math.min(events.length, eventIndex + Math.ceil(initialWindowSize / 2));
const startIndex = Math.max(0, endIndex - initialWindowSize);
this.start = new TimelineIndex(timeline, startIndex - timeline.getBaseIndex());
this.end = new TimelineIndex(timeline, endIndex - timeline.getBaseIndex());
this.eventCount = endIndex - startIndex;
};
// We avoid delaying the resolution of the promise by a reactor tick if
// we already have the data we need, which is important to keep room-switching
// feeling snappy.
//
// We avoid delaying the resolution of the promise by a reactor tick if we already have the data we need,
// which is important to keep room-switching feeling snappy.
if (initialEventId) {
const timeline = this.timelineSet.getTimelineForEvent(initialEventId);
if (timeline) {
// hot-path optimization to save a reactor tick by replicating the sync check getTimelineForEvent does.
initFields(timeline);
return Promise.resolve(timeline);
return Promise.resolve();
}
const prom = this.client.getEventTimeline(this.timelineSet, initialEventId);
return prom.then(initFields);
return this.client.getEventTimeline(this.timelineSet, initialEventId).then(initFields);
} else {
const tl = this.timelineSet.getLiveTimeline();
initFields(tl);
@@ -240,7 +231,7 @@ export class TimelineWindow {
}
return Boolean(tl.timeline.getNeighbouringTimeline(direction) ||
tl.timeline.getPaginationToken(direction));
tl.timeline.getPaginationToken(direction) !== null);
}
/**
@@ -297,7 +288,7 @@ export class TimelineWindow {
// try making a pagination request
const token = tl.timeline.getPaginationToken(direction);
if (!token) {
if (token === null) {
debuglog("TimelineWindow: no token");
return Promise.resolve(false);
}
+1
View File
@@ -6,6 +6,7 @@
"module": "commonjs",
"moduleResolution": "node",
"noImplicitAny": false,
"noUnusedLocals": true,
"noEmit": true,
"declaration": true
},