Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e29ee105aa | |||
| 454eb7e627 | |||
| 70c6a4b567 | |||
| 5cd615c495 | |||
| 88f5ea675d | |||
| ac5fee0a69 | |||
| 274d6a9597 | |||
| d190cdc307 | |||
| b896111269 | |||
| 6137afeb28 | |||
| 2ebf33544f | |||
| 45f1991d4e | |||
| aa283a3c9e | |||
| 34ee566d88 | |||
| 3649cf46d3 | |||
| 8f00eb6771 | |||
| 01ec51891a | |||
| ba06e430c4 | |||
| ac08e52410 | |||
| 1bb82108b7 | |||
| e133005b44 | |||
| c0cb66233a | |||
| d82cdd3b19 | |||
| 6a51b02bed | |||
| 47d2c063fa | |||
| 540514c805 | |||
| db58a66e19 | |||
| 59763a84f8 | |||
| 1375a4849c | |||
| c0e3ad4b83 | |||
| 5305e373a0 | |||
| 877e3df71b | |||
| d705a0ed9e | |||
| 7023fb1c99 | |||
| fe36dafcc7 | |||
| 04a6dbbedf | |||
| 29b427aab7 | |||
| 643b783dec | |||
| 872033a552 | |||
| d457fd6db0 | |||
| 03f4700bd7 |
@@ -21,3 +21,6 @@ insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
codecov:
|
||||
allow_coverage_offsets: True
|
||||
coverage:
|
||||
status:
|
||||
project: off
|
||||
patch: off
|
||||
comment:
|
||||
layout: "diff, files"
|
||||
behavior: default
|
||||
require_changes: false
|
||||
require_base: no
|
||||
require_head: no
|
||||
@@ -0,0 +1,14 @@
|
||||
name: Notify Downstream Projects
|
||||
on:
|
||||
push:
|
||||
branches: [ develop ]
|
||||
jobs:
|
||||
notify-matrix-react-sdk:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Notify matrix-react-sdk repo that a new SDK build is on develop so it can CI against it
|
||||
uses: peter-evans/repository-dispatch@v1
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
repository: vector-im/element-web
|
||||
event-type: upstream-sdk-notify
|
||||
@@ -1,12 +0,0 @@
|
||||
name: Preview Changelog
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [ opened, edited, labeled ]
|
||||
jobs:
|
||||
changelog:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Preview Changelog
|
||||
uses: matrix-org/allchange@main
|
||||
with:
|
||||
ghToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,24 @@
|
||||
name: Pull Request
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [ opened, edited, labeled, unlabeled ]
|
||||
jobs:
|
||||
changelog:
|
||||
name: Preview Changelog
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: matrix-org/allchange@main
|
||||
with:
|
||||
ghToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
enforce-label:
|
||||
name: Enforce Labels
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: read
|
||||
steps:
|
||||
- uses: yogevbd/enforce-label-action@2.1.0
|
||||
with:
|
||||
REQUIRED_LABELS_ANY: "T-Defect,T-Deprecation,T-Enhancement,T-Task"
|
||||
BANNED_LABELS: "X-Blocked"
|
||||
BANNED_LABELS_DESCRIPTION: "Preventing merge whilst PR is marked blocked!"
|
||||
@@ -0,0 +1,47 @@
|
||||
name: SonarQube
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [ "Tests" ]
|
||||
types:
|
||||
- completed
|
||||
jobs:
|
||||
sonarqube:
|
||||
name: SonarQube
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
|
||||
# There's a 'download artifact' action, but it hasn't been updated for the workflow_run action
|
||||
# (https://github.com/actions/download-artifact/issues/60) so instead we get this mess:
|
||||
- name: Download Coverage Report
|
||||
uses: actions/github-script@v3.1.0
|
||||
with:
|
||||
script: |
|
||||
const artifacts = await github.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: ${{ github.event.workflow_run.id }},
|
||||
});
|
||||
const matchArtifact = artifacts.data.artifacts.filter((artifact) => {
|
||||
return artifact.name == "coverage"
|
||||
})[0];
|
||||
const download = await github.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: matchArtifact.id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
const fs = require('fs');
|
||||
fs.writeFileSync('${{github.workspace}}/coverage.zip', Buffer.from(download.data));
|
||||
|
||||
- name: Extract Coverage Report
|
||||
run: unzip -d coverage coverage.zip && rm coverage.zip
|
||||
|
||||
- name: SonarCloud Scan
|
||||
uses: SonarSource/sonarcloud-github-action@master
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
@@ -0,0 +1,53 @@
|
||||
name: Static Analysis
|
||||
on:
|
||||
pull_request: { }
|
||||
push:
|
||||
branches: [ develop, master ]
|
||||
jobs:
|
||||
ts_lint:
|
||||
name: "Typescript Syntax Check"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
|
||||
- name: Typecheck
|
||||
run: "yarn run lint:types"
|
||||
|
||||
js_lint:
|
||||
name: "ESLint"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
|
||||
- name: Run Linter
|
||||
run: "yarn run lint:js"
|
||||
|
||||
docs:
|
||||
name: "JSDoc Checker"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
|
||||
- name: Generate Docs
|
||||
run: "yarn run gendoc"
|
||||
@@ -1,32 +0,0 @@
|
||||
name: Test coverage
|
||||
on:
|
||||
pull_request: {}
|
||||
push:
|
||||
branches: [develop, main, master]
|
||||
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"
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v2
|
||||
with:
|
||||
fail_ci_if_error: false
|
||||
verbose: true
|
||||
override_commit: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || '' }}
|
||||
@@ -0,0 +1,34 @@
|
||||
name: Tests
|
||||
on:
|
||||
pull_request: { }
|
||||
push:
|
||||
branches: [ develop, main, master ]
|
||||
jobs:
|
||||
jest:
|
||||
name: Jest
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Yarn cache
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install dependencies
|
||||
run: "yarn install"
|
||||
|
||||
- name: Build
|
||||
run: "yarn build"
|
||||
|
||||
- name: Run tests with coverage
|
||||
run: "yarn coverage --ci"
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: coverage
|
||||
path: |
|
||||
coverage
|
||||
!coverage/lcov-report
|
||||
+16
-2
@@ -1,3 +1,17 @@
|
||||
Changes in [17.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v17.2.0) (2022-05-10)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Live location sharing: handle encrypted messages in processBeaconEvents ([\#2327](https://github.com/matrix-org/matrix-js-sdk/pull/2327)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix race conditions around threads ([\#2331](https://github.com/matrix-org/matrix-js-sdk/pull/2331)). Fixes vector-im/element-web#21627.
|
||||
* Ignore m.replace relations on state events, they're invalid ([\#2306](https://github.com/matrix-org/matrix-js-sdk/pull/2306)). Fixes vector-im/element-web#21851.
|
||||
* fix example in readme ([\#2315](https://github.com/matrix-org/matrix-js-sdk/pull/2315)).
|
||||
* Don't decrement the length count of a thread when root redacted ([\#2314](https://github.com/matrix-org/matrix-js-sdk/pull/2314)).
|
||||
* Prevent attempt to create thread with id "undefined" ([\#2308](https://github.com/matrix-org/matrix-js-sdk/pull/2308)).
|
||||
* Update threads handling for replies-to-thread-responses as per MSC update ([\#2305](https://github.com/matrix-org/matrix-js-sdk/pull/2305)). Fixes vector-im/element-web#19678.
|
||||
|
||||
Changes in [17.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v17.1.0) (2022-04-26)
|
||||
==================================================================================================
|
||||
|
||||
@@ -684,7 +698,7 @@ Changes in [11.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/ta
|
||||
BREAKING CHANGES
|
||||
---
|
||||
|
||||
* `MatrixCall` and related APIs have been redesigned to support multiple streams
|
||||
* `MatrixCall` and related APIs have been redesigned to support multiple streams
|
||||
(see [\#1660](https://github.com/matrix-org/matrix-js-sdk/pull/1660) for more details)
|
||||
|
||||
All changes
|
||||
@@ -1357,7 +1371,7 @@ BREAKING CHANGES
|
||||
---
|
||||
|
||||
* `RoomState` events changed to use a Map instead of an object, which changes the collection APIs available to access them.
|
||||
|
||||
|
||||
All Changes
|
||||
---
|
||||
|
||||
|
||||
@@ -243,3 +243,15 @@ on Git 2.17+ you can mass signoff using rebase:
|
||||
```
|
||||
git rebase --signoff origin/develop
|
||||
```
|
||||
|
||||
Merge Strategy
|
||||
==============
|
||||
|
||||
The preferred method for merging pull requests is squash merging to keep the
|
||||
commit history trim, but it is up to the discretion of the team member merging
|
||||
the change. When stacking pull requests, you may wish to do the following:
|
||||
|
||||
1. Branch from develop to your branch (branch1), push commits onto it and open a pull request
|
||||
2. Branch from your base branch (branch1) to your work branch (branch2), push commits and open a pull request configuring the base to be branch1, saying in the description that it is based on your other PR.
|
||||
3. Merge the first PR using a merge commit otherwise your stacked PR will need a rebase. Github will automatically adjust the base branch of your other PR to be develop.
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ attached to ``window`` through which you can access the SDK. See below for how t
|
||||
include libolm to enable end-to-end-encryption.
|
||||
|
||||
The browser bundle supports recent versions of browsers. Typically this is ES2015
|
||||
or `> 0.5%, last 2 versions, Firefox ESR, not dead` if using
|
||||
or `> 0.5%, last 2 versions, Firefox ESR, not dead` if using
|
||||
[browserlists](https://github.com/browserslist/browserslist).
|
||||
|
||||
Please check [the working browser example](examples/browser) for more information.
|
||||
@@ -26,11 +26,11 @@ In Node.js
|
||||
|
||||
Ensure you have the latest LTS version of Node.js installed.
|
||||
|
||||
This SDK targets Node 10 for compatibility, which translates to ES6. If you're using
|
||||
This SDK targets Node 12 for compatibility, which translates to ES6. If you're using
|
||||
a bundler like webpack you'll likely have to transpile dependencies, including this
|
||||
SDK, to match your target browsers.
|
||||
|
||||
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://classic.yarnpkg.com/en/docs/install)
|
||||
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://classic.yarnpkg.com/en/docs/install)
|
||||
if you do not have it already.
|
||||
|
||||
``yarn add matrix-js-sdk``
|
||||
@@ -307,7 +307,7 @@ The SDK supports end-to-end encryption via the Olm and Megolm protocols, using
|
||||
[libolm](https://gitlab.matrix.org/matrix-org/olm). It is left up to the
|
||||
application to make libolm available, via the ``Olm`` global.
|
||||
|
||||
It is also necessary to call ``matrixClient.initCrypto()`` after creating a new
|
||||
It is also necessary to call ``await matrixClient.initCrypto()`` after creating a new
|
||||
``MatrixClient`` (but **before** calling ``matrixClient.startClient()``) to
|
||||
initialise the crypto layer.
|
||||
|
||||
|
||||
+13
-4
@@ -1,7 +1,10 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "17.1.0",
|
||||
"version": "17.2.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=12.9.0"
|
||||
},
|
||||
"scripts": {
|
||||
"prepublishOnly": "yarn build",
|
||||
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
|
||||
@@ -97,6 +100,7 @@
|
||||
"fake-indexeddb": "^3.1.2",
|
||||
"jest": "^26.6.3",
|
||||
"jest-localstorage-mock": "^2.4.6",
|
||||
"jest-sonar-reporter": "^2.0.0",
|
||||
"jsdoc": "^3.6.6",
|
||||
"matrix-mock-request": "^1.2.3",
|
||||
"rimraf": "^3.0.2",
|
||||
@@ -113,9 +117,14 @@
|
||||
"<rootDir>/src/**/*.{js,ts}"
|
||||
],
|
||||
"coverageReporters": [
|
||||
"text",
|
||||
"json"
|
||||
]
|
||||
"text-summary",
|
||||
"lcov"
|
||||
],
|
||||
"testResultsProcessor": "jest-sonar-reporter"
|
||||
},
|
||||
"jestSonar": {
|
||||
"reportPath": "coverage",
|
||||
"sonar56x": true
|
||||
},
|
||||
"typings": "./lib/index.d.ts"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
sonar.projectKey=matrix-js-sdk
|
||||
sonar.organization=matrix-org
|
||||
|
||||
# This is the name and version displayed in the SonarCloud UI.
|
||||
#sonar.projectName=matrix-js-sdk
|
||||
#sonar.projectVersion=1.0
|
||||
|
||||
# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows.
|
||||
#sonar.sources=.
|
||||
|
||||
# Encoding of the source code. Default is default system encoding
|
||||
#sonar.sourceEncoding=UTF-8
|
||||
|
||||
sonar.sources=src
|
||||
sonar.tests=spec
|
||||
sonar.exclusions=docs,examples,git-hooks
|
||||
|
||||
sonar.typescript.tsconfigPath=./tsconfig.json
|
||||
sonar.javascript.lcov.reportPaths=coverage/lcov.info
|
||||
sonar.coverage.exclusions=spec/*.ts
|
||||
sonar.testExecutionReportPaths=coverage/test-report.xml
|
||||
@@ -797,7 +797,7 @@ describe("MatrixClient", function() {
|
||||
]);
|
||||
});
|
||||
|
||||
it("sends reply to thread responses to thread timeline only", () => {
|
||||
it("sends reply to thread responses to main timeline only", () => {
|
||||
client.clientOpts = { experimentalThreadSupport: true };
|
||||
|
||||
const threadRootEvent = buildEventPollStartThreadRoot();
|
||||
@@ -814,12 +814,12 @@ describe("MatrixClient", function() {
|
||||
|
||||
expect(timeline).toEqual([
|
||||
threadRootEvent,
|
||||
replyToThreadResponse,
|
||||
]);
|
||||
|
||||
expect(threaded).toEqual([
|
||||
threadRootEvent,
|
||||
eventMessageInThread,
|
||||
replyToThreadResponse,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -883,4 +883,138 @@ describe("Cross Signing", function() {
|
||||
expect(bobTrust3.isCrossSigningVerified()).toBeTruthy();
|
||||
expect(bobTrust3.isTofu()).toBeTruthy();
|
||||
});
|
||||
|
||||
it(
|
||||
"should observe that our own device is cross-signed, even if this device doesn't trust the key",
|
||||
async function() {
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
|
||||
// Generate Alice's SSK etc
|
||||
const aliceMasterSigning = new global.Olm.PkSigning();
|
||||
const aliceMasterPrivkey = aliceMasterSigning.generate_seed();
|
||||
const aliceMasterPubkey = aliceMasterSigning.init_with_seed(aliceMasterPrivkey);
|
||||
const aliceSigning = new global.Olm.PkSigning();
|
||||
const alicePrivkey = aliceSigning.generate_seed();
|
||||
const alicePubkey = aliceSigning.init_with_seed(alicePrivkey);
|
||||
const aliceSSK = {
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["self_signing"],
|
||||
keys: {
|
||||
["ed25519:" + alicePubkey]: alicePubkey,
|
||||
},
|
||||
};
|
||||
const sskSig = aliceMasterSigning.sign(anotherjson.stringify(aliceSSK));
|
||||
aliceSSK.signatures = {
|
||||
"@alice:example.com": {
|
||||
["ed25519:" + aliceMasterPubkey]: sskSig,
|
||||
},
|
||||
};
|
||||
|
||||
// Alice's device downloads the keys, but doesn't trust them yet
|
||||
alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["master"],
|
||||
keys: {
|
||||
["ed25519:" + aliceMasterPubkey]: aliceMasterPubkey,
|
||||
},
|
||||
},
|
||||
self_signing: aliceSSK,
|
||||
},
|
||||
firstUse: 1,
|
||||
unsigned: {},
|
||||
});
|
||||
|
||||
// Alice has a second device that's cross-signed
|
||||
const aliceCrossSignedDevice = {
|
||||
user_id: "@alice:example.com",
|
||||
device_id: "Dynabook",
|
||||
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
|
||||
keys: {
|
||||
"curve25519:Dynabook": "somePubkey",
|
||||
"ed25519:Dynabook": "someOtherPubkey",
|
||||
},
|
||||
};
|
||||
const sig = aliceSigning.sign(anotherjson.stringify(aliceCrossSignedDevice));
|
||||
aliceCrossSignedDevice.signatures = {
|
||||
"@alice:example.com": {
|
||||
["ed25519:" + alicePubkey]: sig,
|
||||
},
|
||||
};
|
||||
alice.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
|
||||
Dynabook: aliceCrossSignedDevice,
|
||||
});
|
||||
|
||||
// We don't trust the cross-signing keys yet...
|
||||
expect(alice.checkDeviceTrust(aliceCrossSignedDevice.device_id).isCrossSigningVerified()).toBeFalsy();
|
||||
// ... but we do acknowledge that the device is signed by them
|
||||
expect(alice.checkIfOwnDeviceCrossSigned(aliceCrossSignedDevice.device_id)).toBeTruthy();
|
||||
},
|
||||
);
|
||||
|
||||
it("should observe that our own device isn't cross-signed", async function() {
|
||||
const { client: alice } = await makeTestClient(
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
|
||||
// Generate Alice's SSK etc
|
||||
const aliceMasterSigning = new global.Olm.PkSigning();
|
||||
const aliceMasterPrivkey = aliceMasterSigning.generate_seed();
|
||||
const aliceMasterPubkey = aliceMasterSigning.init_with_seed(aliceMasterPrivkey);
|
||||
const aliceSigning = new global.Olm.PkSigning();
|
||||
const alicePrivkey = aliceSigning.generate_seed();
|
||||
const alicePubkey = aliceSigning.init_with_seed(alicePrivkey);
|
||||
const aliceSSK = {
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["self_signing"],
|
||||
keys: {
|
||||
["ed25519:" + alicePubkey]: alicePubkey,
|
||||
},
|
||||
};
|
||||
const sskSig = aliceMasterSigning.sign(anotherjson.stringify(aliceSSK));
|
||||
aliceSSK.signatures = {
|
||||
"@alice:example.com": {
|
||||
["ed25519:" + aliceMasterPubkey]: sskSig,
|
||||
},
|
||||
};
|
||||
|
||||
// Alice's device downloads the keys
|
||||
alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["master"],
|
||||
keys: {
|
||||
["ed25519:" + aliceMasterPubkey]: aliceMasterPubkey,
|
||||
},
|
||||
},
|
||||
self_signing: aliceSSK,
|
||||
},
|
||||
firstUse: 1,
|
||||
unsigned: {},
|
||||
});
|
||||
|
||||
// Alice has a second device that's also not cross-signed
|
||||
const aliceNotCrossSignedDevice = {
|
||||
user_id: "@alice:example.com",
|
||||
device_id: "Dynabook",
|
||||
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
|
||||
keys: {
|
||||
"curve25519:Dynabook": "somePubkey",
|
||||
"ed25519:Dynabook": "someOtherPubkey",
|
||||
},
|
||||
};
|
||||
alice.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
|
||||
Dynabook: aliceNotCrossSignedDevice,
|
||||
});
|
||||
|
||||
expect(alice.checkIfOwnDeviceCrossSigned(aliceNotCrossSignedDevice.device_id)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -811,9 +811,7 @@ describe("MatrixClient", function() {
|
||||
}
|
||||
},
|
||||
},
|
||||
threads: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
getThread: jest.fn(),
|
||||
addPendingEvent: jest.fn(),
|
||||
updatePendingEvent: jest.fn(),
|
||||
reEmitter: {
|
||||
@@ -1047,7 +1045,7 @@ describe("MatrixClient", function() {
|
||||
expect(roomStateProcessSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls room states processBeaconEvents with m.beacon events', () => {
|
||||
it('calls room states processBeaconEvents with events', () => {
|
||||
const room = new Room(roomId, client, userId);
|
||||
const roomStateProcessSpy = jest.spyOn(room.currentState, 'processBeaconEvents');
|
||||
|
||||
@@ -1055,7 +1053,7 @@ describe("MatrixClient", function() {
|
||||
const beaconEvent = makeBeaconEvent(userId);
|
||||
|
||||
client.processBeaconEvents(room, [messageEvent, beaconEvent]);
|
||||
expect(roomStateProcessSpy).toHaveBeenCalledWith([beaconEvent]);
|
||||
expect(roomStateProcessSpy).toHaveBeenCalledWith([messageEvent, beaconEvent], client);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -130,4 +130,49 @@ describe("Relations", function() {
|
||||
await relationsCreated;
|
||||
}
|
||||
});
|
||||
|
||||
it("should ignore m.replace for state events", async () => {
|
||||
const userId = "@bob:example.com";
|
||||
const room = new Room("room123", null, userId);
|
||||
const relations = new Relations("m.replace", "m.room.topic", room);
|
||||
|
||||
// Create an instance of a state event with rel_type m.replace
|
||||
const originalTopic = new MatrixEvent({
|
||||
"sender": userId,
|
||||
"type": "m.room.topic",
|
||||
"event_id": "$orig",
|
||||
"room_id": room.roomId,
|
||||
"content": {
|
||||
"topic": "orig",
|
||||
},
|
||||
"state_key": "",
|
||||
});
|
||||
const badlyEditedTopic = new MatrixEvent({
|
||||
"sender": userId,
|
||||
"type": "m.room.topic",
|
||||
"event_id": "$orig",
|
||||
"room_id": room.roomId,
|
||||
"content": {
|
||||
"topic": "topic",
|
||||
"m.new_content": {
|
||||
"topic": "edit",
|
||||
},
|
||||
"m.relates_to": {
|
||||
"event_id": "$orig",
|
||||
"rel_type": "m.replace",
|
||||
},
|
||||
},
|
||||
"state_key": "",
|
||||
});
|
||||
|
||||
await relations.setTargetEvent(originalTopic);
|
||||
expect(originalTopic.replacingEvent()).toBe(null);
|
||||
expect(originalTopic.getContent().topic).toBe("orig");
|
||||
|
||||
await relations.addEvent(badlyEditedTopic);
|
||||
expect(originalTopic.replacingEvent()).toBe(null);
|
||||
expect(originalTopic.getContent().topic).toBe("orig");
|
||||
expect(badlyEditedTopic.replacingEvent()).toBe(null);
|
||||
expect(badlyEditedTopic.getContent().topic).toBe("topic");
|
||||
});
|
||||
});
|
||||
|
||||
+216
-24
@@ -3,6 +3,12 @@ import { makeBeaconEvent, makeBeaconInfoEvent } from "../test-utils/beacon";
|
||||
import { filterEmitCallsByEventType } from "../test-utils/emitter";
|
||||
import { RoomState, RoomStateEvent } from "../../src/models/room-state";
|
||||
import { BeaconEvent, getBeaconInfoIdentifier } from "../../src/models/beacon";
|
||||
import { EventType, RelationType } from "../../src/@types/event";
|
||||
import {
|
||||
MatrixEvent,
|
||||
MatrixEventEvent,
|
||||
} from "../../src/models/event";
|
||||
import { M_BEACON } from "../../src/@types/beacon";
|
||||
|
||||
describe("RoomState", function() {
|
||||
const roomId = "!foo:bar";
|
||||
@@ -717,52 +723,238 @@ describe("RoomState", function() {
|
||||
const beacon1 = makeBeaconInfoEvent(userA, roomId, {}, '$beacon1', '$beacon1');
|
||||
const beacon2 = makeBeaconInfoEvent(userB, roomId, {}, '$beacon2', '$beacon2');
|
||||
|
||||
const mockClient = { decryptEventIfNeeded: jest.fn() };
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient.decryptEventIfNeeded.mockClear();
|
||||
});
|
||||
|
||||
it('does nothing when state has no beacons', () => {
|
||||
const emitSpy = jest.spyOn(state, 'emit');
|
||||
state.processBeaconEvents([makeBeaconEvent(userA, { beaconInfoId: '$beacon1' })]);
|
||||
state.processBeaconEvents([makeBeaconEvent(userA, { beaconInfoId: '$beacon1' })], mockClient);
|
||||
expect(emitSpy).not.toHaveBeenCalled();
|
||||
expect(mockClient.decryptEventIfNeeded).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does nothing when there are no events', () => {
|
||||
state.setStateEvents([beacon1, beacon2]);
|
||||
const emitSpy = jest.spyOn(state, 'emit').mockClear();
|
||||
state.processBeaconEvents([]);
|
||||
state.processBeaconEvents([], mockClient);
|
||||
expect(emitSpy).not.toHaveBeenCalled();
|
||||
expect(mockClient.decryptEventIfNeeded).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('discards events for beacons that are not in state', () => {
|
||||
const location = makeBeaconEvent(userA, {
|
||||
beaconInfoId: 'some-other-beacon',
|
||||
describe('without encryption', () => {
|
||||
it('discards events for beacons that are not in state', () => {
|
||||
const location = makeBeaconEvent(userA, {
|
||||
beaconInfoId: 'some-other-beacon',
|
||||
});
|
||||
const otherRelatedEvent = new MatrixEvent({
|
||||
sender: userA,
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
['m.relates_to']: {
|
||||
event_id: 'whatever',
|
||||
},
|
||||
},
|
||||
});
|
||||
state.setStateEvents([beacon1, beacon2]);
|
||||
const emitSpy = jest.spyOn(state, 'emit').mockClear();
|
||||
state.processBeaconEvents([location, otherRelatedEvent], mockClient);
|
||||
expect(emitSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('discards events that are not beacon type', () => {
|
||||
// related to beacon1
|
||||
const otherRelatedEvent = new MatrixEvent({
|
||||
sender: userA,
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
['m.relates_to']: {
|
||||
rel_type: RelationType.Reference,
|
||||
event_id: beacon1.getId(),
|
||||
},
|
||||
},
|
||||
});
|
||||
state.setStateEvents([beacon1, beacon2]);
|
||||
const emitSpy = jest.spyOn(state, 'emit').mockClear();
|
||||
state.processBeaconEvents([otherRelatedEvent], mockClient);
|
||||
expect(emitSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('adds locations to beacons', () => {
|
||||
const location1 = makeBeaconEvent(userA, {
|
||||
beaconInfoId: '$beacon1', timestamp: Date.now() + 1,
|
||||
});
|
||||
const location2 = makeBeaconEvent(userA, {
|
||||
beaconInfoId: '$beacon1', timestamp: Date.now() + 2,
|
||||
});
|
||||
const location3 = makeBeaconEvent(userB, {
|
||||
beaconInfoId: 'some-other-beacon',
|
||||
});
|
||||
|
||||
state.setStateEvents([beacon1, beacon2], mockClient);
|
||||
|
||||
expect(state.beacons.size).toEqual(2);
|
||||
|
||||
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beacon1));
|
||||
const addLocationsSpy = jest.spyOn(beaconInstance, 'addLocations');
|
||||
|
||||
state.processBeaconEvents([location1, location2, location3], mockClient);
|
||||
|
||||
expect(addLocationsSpy).toHaveBeenCalledTimes(2);
|
||||
// only called with locations for beacon1
|
||||
expect(addLocationsSpy).toHaveBeenCalledWith([location1]);
|
||||
expect(addLocationsSpy).toHaveBeenCalledWith([location2]);
|
||||
});
|
||||
state.setStateEvents([beacon1, beacon2]);
|
||||
const emitSpy = jest.spyOn(state, 'emit').mockClear();
|
||||
state.processBeaconEvents([location]);
|
||||
expect(emitSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('adds locations to beacons', () => {
|
||||
const location1 = makeBeaconEvent(userA, {
|
||||
beaconInfoId: '$beacon1', timestamp: Date.now() + 1,
|
||||
describe('with encryption', () => {
|
||||
const beacon1RelationContent = { ['m.relates_to']: {
|
||||
rel_type: RelationType.Reference,
|
||||
event_id: beacon1.getId(),
|
||||
} };
|
||||
const relatedEncryptedEvent = new MatrixEvent({
|
||||
sender: userA,
|
||||
type: EventType.RoomMessageEncrypted,
|
||||
content: beacon1RelationContent,
|
||||
});
|
||||
const location2 = makeBeaconEvent(userA, {
|
||||
beaconInfoId: '$beacon1', timestamp: Date.now() + 2,
|
||||
const decryptingRelatedEvent = new MatrixEvent({
|
||||
sender: userA,
|
||||
type: EventType.RoomMessageEncrypted,
|
||||
content: beacon1RelationContent,
|
||||
});
|
||||
const location3 = makeBeaconEvent(userB, {
|
||||
beaconInfoId: 'some-other-beacon',
|
||||
jest.spyOn(decryptingRelatedEvent, 'isBeingDecrypted').mockReturnValue(true);
|
||||
|
||||
const failedDecryptionRelatedEvent = new MatrixEvent({
|
||||
sender: userA,
|
||||
type: EventType.RoomMessageEncrypted,
|
||||
content: beacon1RelationContent,
|
||||
});
|
||||
jest.spyOn(failedDecryptionRelatedEvent, 'isDecryptionFailure').mockReturnValue(true);
|
||||
|
||||
it('discards events without relations', () => {
|
||||
const unrelatedEvent = new MatrixEvent({
|
||||
sender: userA,
|
||||
type: EventType.RoomMessageEncrypted,
|
||||
});
|
||||
state.setStateEvents([beacon1, beacon2]);
|
||||
const emitSpy = jest.spyOn(state, 'emit').mockClear();
|
||||
state.processBeaconEvents([unrelatedEvent], mockClient);
|
||||
expect(emitSpy).not.toHaveBeenCalled();
|
||||
// discard unrelated events early
|
||||
expect(mockClient.decryptEventIfNeeded).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
state.setStateEvents([beacon1, beacon2]);
|
||||
it('discards events for beacons that are not in state', () => {
|
||||
const location = makeBeaconEvent(userA, {
|
||||
beaconInfoId: 'some-other-beacon',
|
||||
});
|
||||
const otherRelatedEvent = new MatrixEvent({
|
||||
sender: userA,
|
||||
type: EventType.RoomMessageEncrypted,
|
||||
content: {
|
||||
['m.relates_to']: {
|
||||
rel_type: RelationType.Reference,
|
||||
event_id: 'whatever',
|
||||
},
|
||||
},
|
||||
});
|
||||
state.setStateEvents([beacon1, beacon2]);
|
||||
|
||||
expect(state.beacons.size).toEqual(2);
|
||||
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1));
|
||||
const addLocationsSpy = jest.spyOn(beacon, 'addLocations').mockClear();
|
||||
state.processBeaconEvents([location, otherRelatedEvent], mockClient);
|
||||
expect(addLocationsSpy).not.toHaveBeenCalled();
|
||||
// discard unrelated events early
|
||||
expect(mockClient.decryptEventIfNeeded).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beacon1));
|
||||
const addLocationsSpy = jest.spyOn(beaconInstance, 'addLocations');
|
||||
it('decrypts related events if needed', () => {
|
||||
const location = makeBeaconEvent(userA, {
|
||||
beaconInfoId: beacon1.getId(),
|
||||
});
|
||||
state.setStateEvents([beacon1, beacon2]);
|
||||
state.processBeaconEvents([location, relatedEncryptedEvent], mockClient);
|
||||
// discard unrelated events early
|
||||
expect(mockClient.decryptEventIfNeeded).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
state.processBeaconEvents([location1, location2, location3]);
|
||||
it('listens for decryption on events that are being decrypted', () => {
|
||||
const decryptingRelatedEvent = new MatrixEvent({
|
||||
sender: userA,
|
||||
type: EventType.RoomMessageEncrypted,
|
||||
content: beacon1RelationContent,
|
||||
});
|
||||
jest.spyOn(decryptingRelatedEvent, 'isBeingDecrypted').mockReturnValue(true);
|
||||
// spy on event.once
|
||||
const eventOnceSpy = jest.spyOn(decryptingRelatedEvent, 'once');
|
||||
|
||||
expect(addLocationsSpy).toHaveBeenCalledTimes(1);
|
||||
// only called with locations for beacon1
|
||||
expect(addLocationsSpy).toHaveBeenCalledWith([location1, location2]);
|
||||
state.setStateEvents([beacon1, beacon2]);
|
||||
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
|
||||
|
||||
// listener was added
|
||||
expect(eventOnceSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('listens for decryption on events that have decryption failure', () => {
|
||||
const failedDecryptionRelatedEvent = new MatrixEvent({
|
||||
sender: userA,
|
||||
type: EventType.RoomMessageEncrypted,
|
||||
content: beacon1RelationContent,
|
||||
});
|
||||
jest.spyOn(failedDecryptionRelatedEvent, 'isDecryptionFailure').mockReturnValue(true);
|
||||
// spy on event.once
|
||||
const eventOnceSpy = jest.spyOn(decryptingRelatedEvent, 'once');
|
||||
|
||||
state.setStateEvents([beacon1, beacon2]);
|
||||
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
|
||||
|
||||
// listener was added
|
||||
expect(eventOnceSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('discard events that are not m.beacon type after decryption', () => {
|
||||
const decryptingRelatedEvent = new MatrixEvent({
|
||||
sender: userA,
|
||||
type: EventType.RoomMessageEncrypted,
|
||||
content: beacon1RelationContent,
|
||||
});
|
||||
jest.spyOn(decryptingRelatedEvent, 'isBeingDecrypted').mockReturnValue(true);
|
||||
state.setStateEvents([beacon1, beacon2]);
|
||||
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1));
|
||||
const addLocationsSpy = jest.spyOn(beacon, 'addLocations').mockClear();
|
||||
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
|
||||
|
||||
// this event is a message after decryption
|
||||
decryptingRelatedEvent.type = EventType.RoomMessage;
|
||||
decryptingRelatedEvent.emit(MatrixEventEvent.Decrypted);
|
||||
|
||||
expect(addLocationsSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('adds locations to beacons after decryption', () => {
|
||||
const decryptingRelatedEvent = new MatrixEvent({
|
||||
sender: userA,
|
||||
type: EventType.RoomMessageEncrypted,
|
||||
content: beacon1RelationContent,
|
||||
});
|
||||
const locationEvent = makeBeaconEvent(userA, {
|
||||
beaconInfoId: '$beacon1', timestamp: Date.now() + 1,
|
||||
});
|
||||
jest.spyOn(decryptingRelatedEvent, 'isBeingDecrypted').mockReturnValue(true);
|
||||
state.setStateEvents([beacon1, beacon2]);
|
||||
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1));
|
||||
const addLocationsSpy = jest.spyOn(beacon, 'addLocations').mockClear();
|
||||
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
|
||||
|
||||
// update type after '''decryption'''
|
||||
decryptingRelatedEvent.event.type = M_BEACON.name;
|
||||
decryptingRelatedEvent.event.content = locationEvent.content;
|
||||
decryptingRelatedEvent.emit(MatrixEventEvent.Decrypted);
|
||||
|
||||
expect(addLocationsSpy).toHaveBeenCalledWith([decryptingRelatedEvent]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+122
-42
@@ -36,7 +36,7 @@ import { RoomState } from "../../src/models/room-state";
|
||||
import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { emitPromise } from "../test-utils/test-utils";
|
||||
import { ThreadEvent } from "../../src/models/thread";
|
||||
import { Thread, ThreadEvent } from "../../src/models/thread";
|
||||
|
||||
describe("Room", function() {
|
||||
const roomId = "!foo:bar";
|
||||
@@ -1471,16 +1471,13 @@ describe("Room", function() {
|
||||
isRoomEncrypted: function() {
|
||||
return false;
|
||||
},
|
||||
http: {
|
||||
serverResponse,
|
||||
authedRequest: function() {
|
||||
if (this.serverResponse instanceof Error) {
|
||||
return Promise.reject(this.serverResponse);
|
||||
} else {
|
||||
return Promise.resolve({ chunk: this.serverResponse });
|
||||
}
|
||||
},
|
||||
},
|
||||
members: jest.fn().mockImplementation(() => {
|
||||
if (serverResponse instanceof Error) {
|
||||
return Promise.reject(serverResponse);
|
||||
} else {
|
||||
return Promise.resolve({ chunk: serverResponse });
|
||||
}
|
||||
}),
|
||||
store: {
|
||||
storageResponse,
|
||||
storedMembers: null,
|
||||
@@ -1547,7 +1544,7 @@ describe("Room", function() {
|
||||
}
|
||||
expect(hasThrown).toEqual(true);
|
||||
|
||||
client.http.serverResponse = [memberEvent];
|
||||
client.members.mockReturnValue({ chunk: [memberEvent] });
|
||||
await room.loadMembersIfNeeded();
|
||||
const memberA = room.getMember("@user_a:bar");
|
||||
expect(memberA.name).toEqual("User A");
|
||||
@@ -1917,7 +1914,7 @@ describe("Room", function() {
|
||||
},
|
||||
});
|
||||
|
||||
room.createThread(undefined, [eventWithoutARootEvent]);
|
||||
room.createThread("$000", undefined, [eventWithoutARootEvent]);
|
||||
|
||||
const rootEvent = new MatrixEvent({
|
||||
event_id: "$666",
|
||||
@@ -1935,7 +1932,7 @@ describe("Room", function() {
|
||||
},
|
||||
});
|
||||
|
||||
expect(() => room.createThread(rootEvent, [])).not.toThrow();
|
||||
expect(() => room.createThread(rootEvent.getId(), rootEvent, [])).not.toThrow();
|
||||
});
|
||||
|
||||
it("Edits update the lastReply event", async () => {
|
||||
@@ -1962,14 +1959,16 @@ describe("Room", function() {
|
||||
},
|
||||
});
|
||||
|
||||
let prom = emitPromise(room, ThreadEvent.New);
|
||||
room.addLiveEvents([randomMessage, threadRoot, threadResponse]);
|
||||
const thread = await emitPromise(room, ThreadEvent.New);
|
||||
const thread = await prom;
|
||||
|
||||
expect(thread.replyToEvent).toBe(threadResponse);
|
||||
expect(thread.replyToEvent.getContent().body).toBe(threadResponse.getContent().body);
|
||||
|
||||
prom = emitPromise(thread, ThreadEvent.Update);
|
||||
room.addLiveEvents([threadResponseEdit]);
|
||||
await emitPromise(thread, ThreadEvent.Update);
|
||||
await prom;
|
||||
expect(thread.replyToEvent.getContent().body).toBe(threadResponseEdit.getContent()["m.new_content"].body);
|
||||
});
|
||||
|
||||
@@ -1996,15 +1995,17 @@ describe("Room", function() {
|
||||
},
|
||||
});
|
||||
|
||||
let prom = emitPromise(room, ThreadEvent.New);
|
||||
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2]);
|
||||
const thread = await emitPromise(room, ThreadEvent.New);
|
||||
const thread = await prom;
|
||||
|
||||
expect(thread).toHaveLength(2);
|
||||
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
|
||||
|
||||
prom = emitPromise(thread, ThreadEvent.Update);
|
||||
const threadResponse1Redaction = mkRedaction(threadResponse1);
|
||||
room.addLiveEvents([threadResponse1Redaction]);
|
||||
await emitPromise(thread, ThreadEvent.Update);
|
||||
await prom;
|
||||
expect(thread).toHaveLength(1);
|
||||
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
|
||||
});
|
||||
@@ -2033,19 +2034,59 @@ describe("Room", function() {
|
||||
},
|
||||
});
|
||||
|
||||
let prom = emitPromise(room, ThreadEvent.New);
|
||||
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2, threadResponse2Reaction]);
|
||||
const thread = await emitPromise(room, ThreadEvent.New);
|
||||
const thread = await prom;
|
||||
|
||||
expect(thread).toHaveLength(2);
|
||||
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
|
||||
|
||||
prom = emitPromise(thread, ThreadEvent.Update);
|
||||
const threadResponse2ReactionRedaction = mkRedaction(threadResponse2Reaction);
|
||||
room.addLiveEvents([threadResponse2ReactionRedaction]);
|
||||
await emitPromise(thread, ThreadEvent.Update);
|
||||
await prom;
|
||||
expect(thread).toHaveLength(2);
|
||||
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
|
||||
});
|
||||
|
||||
it("should not decrement the length when the thread root is redacted", async () => {
|
||||
room.client.supportsExperimentalThreads = () => true;
|
||||
|
||||
const threadRoot = mkMessage();
|
||||
const threadResponse1 = mkThreadResponse(threadRoot);
|
||||
threadResponse1.localTimestamp += 1000;
|
||||
const threadResponse2 = mkThreadResponse(threadRoot);
|
||||
threadResponse2.localTimestamp += 2000;
|
||||
const threadResponse2Reaction = mkReaction(threadResponse2);
|
||||
|
||||
room.client.fetchRoomEvent = (eventId: string) => Promise.resolve({
|
||||
...threadRoot.event,
|
||||
unsigned: {
|
||||
"age": 123,
|
||||
"m.relations": {
|
||||
"m.thread": {
|
||||
latest_event: threadResponse2.event,
|
||||
count: 2,
|
||||
current_user_participated: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let prom = emitPromise(room, ThreadEvent.New);
|
||||
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2, threadResponse2Reaction]);
|
||||
const thread = await prom;
|
||||
|
||||
expect(thread).toHaveLength(2);
|
||||
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
|
||||
|
||||
prom = emitPromise(room, ThreadEvent.Update);
|
||||
const threadRootRedaction = mkRedaction(threadRoot);
|
||||
room.addLiveEvents([threadRootRedaction]);
|
||||
await prom;
|
||||
expect(thread).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("Redacting the lastEvent finds a new lastEvent", async () => {
|
||||
room.client.supportsExperimentalThreads = () => true;
|
||||
|
||||
@@ -2069,21 +2110,24 @@ describe("Room", function() {
|
||||
},
|
||||
});
|
||||
|
||||
let prom = emitPromise(room, ThreadEvent.New);
|
||||
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2]);
|
||||
const thread = await emitPromise(room, ThreadEvent.New);
|
||||
const thread = await prom;
|
||||
|
||||
expect(thread).toHaveLength(2);
|
||||
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
|
||||
|
||||
prom = emitPromise(room, ThreadEvent.Update);
|
||||
const threadResponse2Redaction = mkRedaction(threadResponse2);
|
||||
room.addLiveEvents([threadResponse2Redaction]);
|
||||
await emitPromise(thread, ThreadEvent.Update);
|
||||
await prom;
|
||||
expect(thread).toHaveLength(1);
|
||||
expect(thread.replyToEvent.getId()).toBe(threadResponse1.getId());
|
||||
|
||||
prom = emitPromise(room, ThreadEvent.Update);
|
||||
const threadResponse1Redaction = mkRedaction(threadResponse1);
|
||||
room.addLiveEvents([threadResponse1Redaction]);
|
||||
await emitPromise(thread, ThreadEvent.Update);
|
||||
await prom;
|
||||
expect(thread).toHaveLength(0);
|
||||
expect(thread.replyToEvent.getId()).toBe(threadRoot.getId());
|
||||
});
|
||||
@@ -2154,36 +2198,32 @@ describe("Room", function() {
|
||||
expect(room.eventShouldLiveIn(threadReaction2Redaction, events, roots).threadId).toBe(threadRoot.getId());
|
||||
});
|
||||
|
||||
it("reply to thread response and its relations&redactions should be only in thread timeline", () => {
|
||||
it("reply to thread response and its relations&redactions should be only in main timeline", () => {
|
||||
const threadRoot = mkMessage();
|
||||
const threadResponse1 = mkThreadResponse(threadRoot);
|
||||
const reply1 = mkReply(threadResponse1);
|
||||
const threadReaction1 = mkReaction(reply1);
|
||||
const threadReaction2 = mkReaction(reply1);
|
||||
const threadReaction2Redaction = mkRedaction(reply1);
|
||||
const reaction1 = mkReaction(reply1);
|
||||
const reaction2 = mkReaction(reply1);
|
||||
const reaction2Redaction = mkRedaction(reply1);
|
||||
|
||||
const roots = new Set([threadRoot.getId()]);
|
||||
const events = [
|
||||
threadRoot,
|
||||
threadResponse1,
|
||||
reply1,
|
||||
threadReaction1,
|
||||
threadReaction2,
|
||||
threadReaction2Redaction,
|
||||
reaction1,
|
||||
reaction2,
|
||||
reaction2Redaction,
|
||||
];
|
||||
|
||||
expect(room.eventShouldLiveIn(reply1, events, roots).shouldLiveInRoom).toBeFalsy();
|
||||
expect(room.eventShouldLiveIn(reply1, events, roots).shouldLiveInThread).toBeTruthy();
|
||||
expect(room.eventShouldLiveIn(reply1, events, roots).threadId).toBe(threadRoot.getId());
|
||||
expect(room.eventShouldLiveIn(threadReaction1, events, roots).shouldLiveInRoom).toBeFalsy();
|
||||
expect(room.eventShouldLiveIn(threadReaction1, events, roots).shouldLiveInThread).toBeTruthy();
|
||||
expect(room.eventShouldLiveIn(threadReaction1, events, roots).threadId).toBe(threadRoot.getId());
|
||||
expect(room.eventShouldLiveIn(threadReaction2, events, roots).shouldLiveInRoom).toBeFalsy();
|
||||
expect(room.eventShouldLiveIn(threadReaction2, events, roots).shouldLiveInThread).toBeTruthy();
|
||||
expect(room.eventShouldLiveIn(threadReaction2, events, roots).threadId).toBe(threadRoot.getId());
|
||||
expect(room.eventShouldLiveIn(threadReaction2Redaction, events, roots).shouldLiveInRoom).toBeFalsy();
|
||||
expect(room.eventShouldLiveIn(threadReaction2Redaction, events, roots).shouldLiveInThread).toBeTruthy();
|
||||
expect(room.eventShouldLiveIn(threadReaction2Redaction, events, roots).threadId).toBe(threadRoot.getId());
|
||||
expect(room.eventShouldLiveIn(reply1, events, roots).shouldLiveInRoom).toBeTruthy();
|
||||
expect(room.eventShouldLiveIn(reply1, events, roots).shouldLiveInThread).toBeFalsy();
|
||||
expect(room.eventShouldLiveIn(reaction1, events, roots).shouldLiveInRoom).toBeTruthy();
|
||||
expect(room.eventShouldLiveIn(reaction1, events, roots).shouldLiveInThread).toBeFalsy();
|
||||
expect(room.eventShouldLiveIn(reaction2, events, roots).shouldLiveInRoom).toBeTruthy();
|
||||
expect(room.eventShouldLiveIn(reaction2, events, roots).shouldLiveInThread).toBeFalsy();
|
||||
expect(room.eventShouldLiveIn(reaction2Redaction, events, roots).shouldLiveInRoom).toBeTruthy();
|
||||
expect(room.eventShouldLiveIn(reaction2Redaction, events, roots).shouldLiveInThread).toBeFalsy();
|
||||
});
|
||||
|
||||
it("reply to reply to thread root should only be in the main timeline", () => {
|
||||
@@ -2205,5 +2245,45 @@ describe("Room", function() {
|
||||
expect(room.eventShouldLiveIn(reply2, events, roots).shouldLiveInRoom).toBeTruthy();
|
||||
expect(room.eventShouldLiveIn(reply2, events, roots).shouldLiveInThread).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should aggregate relations in thread event timeline set", () => {
|
||||
Thread.setServerSideSupport(true, true);
|
||||
const threadRoot = mkMessage();
|
||||
const rootReaction = mkReaction(threadRoot);
|
||||
const threadResponse = mkThreadResponse(threadRoot);
|
||||
const threadReaction = mkReaction(threadResponse);
|
||||
|
||||
const events = [
|
||||
threadRoot,
|
||||
rootReaction,
|
||||
threadResponse,
|
||||
threadReaction,
|
||||
];
|
||||
|
||||
room.addLiveEvents(events);
|
||||
|
||||
const thread = threadRoot.getThread();
|
||||
expect(thread.rootEvent).toBe(threadRoot);
|
||||
|
||||
const rootRelations = thread.timelineSet.getRelationsForEvent(
|
||||
threadRoot.getId(),
|
||||
RelationType.Annotation,
|
||||
EventType.Reaction,
|
||||
).getSortedAnnotationsByKey();
|
||||
expect(rootRelations).toHaveLength(1);
|
||||
expect(rootRelations[0][0]).toEqual(rootReaction.getRelation().key);
|
||||
expect(rootRelations[0][1].size).toEqual(1);
|
||||
expect(rootRelations[0][1].has(rootReaction)).toBeTruthy();
|
||||
|
||||
const responseRelations = thread.timelineSet.getRelationsForEvent(
|
||||
threadResponse.getId(),
|
||||
RelationType.Annotation,
|
||||
EventType.Reaction,
|
||||
).getSortedAnnotationsByKey();
|
||||
expect(responseRelations).toHaveLength(1);
|
||||
expect(responseRelations[0][0]).toEqual(threadReaction.getRelation().key);
|
||||
expect(responseRelations[0][1].size).toEqual(1);
|
||||
expect(responseRelations[0][1].has(threadReaction)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+13
-14
@@ -249,21 +249,20 @@ export class AutoDiscovery {
|
||||
|
||||
// Step 7: Copy any other keys directly into the clientConfig. This is for
|
||||
// things like custom configuration of services.
|
||||
Object.keys(wellknown)
|
||||
.map((k) => {
|
||||
if (k === "m.homeserver" || k === "m.identity_server") {
|
||||
// Only copy selected parts of the config to avoid overwriting
|
||||
// properties computed by the validation logic above.
|
||||
const notProps = ["error", "state", "base_url"];
|
||||
for (const prop of Object.keys(wellknown[k])) {
|
||||
if (notProps.includes(prop)) continue;
|
||||
clientConfig[k][prop] = wellknown[k][prop];
|
||||
}
|
||||
} else {
|
||||
// Just copy the whole thing over otherwise
|
||||
clientConfig[k] = wellknown[k];
|
||||
Object.keys(wellknown).forEach((k) => {
|
||||
if (k === "m.homeserver" || k === "m.identity_server") {
|
||||
// Only copy selected parts of the config to avoid overwriting
|
||||
// properties computed by the validation logic above.
|
||||
const notProps = ["error", "state", "base_url"];
|
||||
for (const prop of Object.keys(wellknown[k])) {
|
||||
if (notProps.includes(prop)) continue;
|
||||
clientConfig[k][prop] = wellknown[k][prop];
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Just copy the whole thing over otherwise
|
||||
clientConfig[k] = wellknown[k];
|
||||
}
|
||||
});
|
||||
|
||||
// Step 8: Give the config to the caller (finally)
|
||||
return Promise.resolve(clientConfig);
|
||||
|
||||
+50
-34
@@ -48,7 +48,9 @@ import { IRoomEncryption, RoomList } from './crypto/RoomList';
|
||||
import { logger } from './logger';
|
||||
import { SERVICE_TYPES } from './service-types';
|
||||
import {
|
||||
FileType, HttpApiEvent, HttpApiEventHandlerMap,
|
||||
FileType,
|
||||
HttpApiEvent,
|
||||
HttpApiEventHandlerMap,
|
||||
IHttpOpts,
|
||||
IUpload,
|
||||
MatrixError,
|
||||
@@ -180,7 +182,7 @@ 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, M_BEACON_INFO } from "./@types/beacon";
|
||||
import { MBeaconInfoEventContent, M_BEACON_INFO } from "./@types/beacon";
|
||||
|
||||
export type Store = IStore;
|
||||
export type SessionStore = WebStorageSessionStore;
|
||||
@@ -2072,6 +2074,21 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
return this.crypto.checkDeviceTrust(userId, deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether one of our own devices is cross-signed by our
|
||||
* user's stored keys, regardless of whether we trust those keys yet.
|
||||
*
|
||||
* @param {string} deviceId The ID of the device to check
|
||||
*
|
||||
* @returns {boolean} true if the device is cross-signed
|
||||
*/
|
||||
public checkIfOwnDeviceCrossSigned(deviceId: string): boolean {
|
||||
if (!this.crypto) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
return this.crypto.checkIfOwnDeviceCrossSigned(deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the copy of our cross-signing key that we have in the device list and
|
||||
* see if we can get the private key. If so, mark it as trusted.
|
||||
@@ -3400,7 +3417,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
*/
|
||||
public setIgnoredUsers(userIds: string[], callback?: Callback): Promise<{}> {
|
||||
const content = { ignored_users: {} };
|
||||
userIds.map((u) => content.ignored_users[u] = {});
|
||||
userIds.forEach((u) => {
|
||||
content.ignored_users[u] = {};
|
||||
});
|
||||
return this.setAccountData("m.ignored_user_list", content, callback);
|
||||
}
|
||||
|
||||
@@ -3724,7 +3743,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
"rel_type": THREAD_RELATION_TYPE.name,
|
||||
"event_id": threadId,
|
||||
};
|
||||
const thread = this.getRoom(roomId)?.threads.get(threadId);
|
||||
const thread = this.getRoom(roomId)?.getThread(threadId);
|
||||
if (thread) {
|
||||
content["m.relates_to"]["m.in_reply_to"] = {
|
||||
"event_id": thread.lastReply((ev: MatrixEvent) => {
|
||||
@@ -3773,7 +3792,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}));
|
||||
|
||||
const room = this.getRoom(roomId);
|
||||
const thread = room?.threads.get(threadId);
|
||||
const thread = room?.getThread(threadId);
|
||||
if (thread) {
|
||||
localEvent.setThread(thread);
|
||||
}
|
||||
@@ -5168,7 +5187,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
limit,
|
||||
Direction.Backward,
|
||||
);
|
||||
}).then(async (res: IMessagesResponse) => {
|
||||
}).then((res: IMessagesResponse) => {
|
||||
const matrixEvents = res.chunk.map(this.getEventMapper());
|
||||
if (res.state) {
|
||||
const stateEvents = res.state.map(this.getEventMapper());
|
||||
@@ -5177,9 +5196,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
const [timelineEvents, threadedEvents] = room.partitionThreadedEvents(matrixEvents);
|
||||
|
||||
this.processBeaconEvents(room, matrixEvents);
|
||||
this.processBeaconEvents(room, timelineEvents);
|
||||
room.addEventsToTimeline(timelineEvents, true, room.getLiveTimeline());
|
||||
await this.processThreadEvents(room, threadedEvents, true);
|
||||
this.processThreadEvents(room, threadedEvents, true);
|
||||
|
||||
room.oldState.paginationToken = res.end;
|
||||
if (res.chunk.length === 0) {
|
||||
@@ -5282,25 +5301,27 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
event.isRelation(THREAD_RELATION_TYPE.name)
|
||||
) {
|
||||
const [, threadedEvents] = timelineSet.room.partitionThreadedEvents(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;
|
||||
let thread = timelineSet.room.getThread(event.threadRootId);
|
||||
if (!thread) {
|
||||
thread = timelineSet.room.createThread(event.threadRootId, undefined, threadedEvents, true);
|
||||
}
|
||||
|
||||
const opts: IRelationsRequestOpts = {
|
||||
direction: Direction.Backward,
|
||||
limit: 50,
|
||||
};
|
||||
|
||||
// Fetch events until we find the one we were asked for
|
||||
await thread.fetchInitialEvents();
|
||||
let nextBatch = thread.liveTimeline.getPaginationToken(Direction.Backward);
|
||||
|
||||
// Fetch events until we find the one we were asked for, or we run out of pages
|
||||
while (!thread.findEventById(eventId)) {
|
||||
if (nextBatch) {
|
||||
opts.from = nextBatch;
|
||||
}
|
||||
|
||||
({ nextBatch } = await thread.fetchEvents(opts));
|
||||
if (!nextBatch) break;
|
||||
}
|
||||
|
||||
return thread.liveTimeline;
|
||||
@@ -5319,8 +5340,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
const [timelineEvents, threadedEvents] = timelineSet.room.partitionThreadedEvents(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);
|
||||
this.processBeaconEvents(timelineSet.room, events);
|
||||
this.processThreadEvents(timelineSet.room, threadedEvents, true);
|
||||
this.processBeaconEvents(timelineSet.room, timelineEvents);
|
||||
|
||||
// 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
|
||||
@@ -5476,7 +5497,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
opts.limit,
|
||||
dir,
|
||||
eventTimeline.getFilter(),
|
||||
).then(async (res) => {
|
||||
).then((res) => {
|
||||
if (res.state) {
|
||||
const roomState = eventTimeline.getState(dir);
|
||||
const stateEvents = res.state.map(this.getEventMapper());
|
||||
@@ -5488,8 +5509,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
const timelineSet = eventTimeline.getTimelineSet();
|
||||
const [timelineEvents, threadedEvents] = timelineSet.room.partitionThreadedEvents(matrixEvents);
|
||||
timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
|
||||
this.processBeaconEvents(timelineSet.room, matrixEvents);
|
||||
await this.processThreadEvents(room, threadedEvents, backwards);
|
||||
this.processBeaconEvents(timelineSet.room, timelineEvents);
|
||||
this.processThreadEvents(room, threadedEvents, backwards);
|
||||
|
||||
// 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
|
||||
@@ -6646,7 +6667,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
eventId: string,
|
||||
relationType?: RelationType | string | null,
|
||||
eventType?: EventType | string | null,
|
||||
opts: IRelationsRequestOpts = {},
|
||||
opts: IRelationsRequestOpts = { direction: Direction.Backward },
|
||||
): Promise<{
|
||||
originalEvent: MatrixEvent;
|
||||
events: MatrixEvent[];
|
||||
@@ -7187,7 +7208,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
eventId: string,
|
||||
relationType?: RelationType | string | null,
|
||||
eventType?: EventType | string | null,
|
||||
opts: IRelationsRequestOpts = {},
|
||||
opts: IRelationsRequestOpts = { direction: Direction.Backward },
|
||||
): Promise<IRelationsResponse> {
|
||||
const queryString = utils.encodeParams(opts as Record<string, string | number>);
|
||||
|
||||
@@ -7262,12 +7283,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
*/
|
||||
public members(
|
||||
roomId: string,
|
||||
includeMembership?: string[],
|
||||
excludeMembership?: string[],
|
||||
includeMembership?: string,
|
||||
excludeMembership?: string,
|
||||
atEventId?: string,
|
||||
callback?: Callback,
|
||||
): Promise<{ [userId: string]: IStateEventWithRoomId }> {
|
||||
const queryParams: any = {};
|
||||
): Promise<{ [userId: string]: IStateEventWithRoomId[] }> {
|
||||
const queryParams: Record<string, string> = {};
|
||||
if (includeMembership) {
|
||||
queryParams.membership = includeMembership;
|
||||
}
|
||||
@@ -8899,12 +8920,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
public async processThreadEvents(
|
||||
room: Room,
|
||||
threadedEvents: MatrixEvent[],
|
||||
toStartOfTimeline: boolean,
|
||||
): Promise<void> {
|
||||
await room.processThreadedEvents(threadedEvents, toStartOfTimeline);
|
||||
public processThreadEvents(room: Room, threadedEvents: MatrixEvent[], toStartOfTimeline: boolean): void {
|
||||
room.processThreadedEvents(threadedEvents, toStartOfTimeline);
|
||||
}
|
||||
|
||||
public processBeaconEvents(
|
||||
@@ -8914,8 +8931,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
if (!events?.length) {
|
||||
return;
|
||||
}
|
||||
const beaconEvents = events.filter(event => M_BEACON.matches(event.getType()));
|
||||
room.currentState.processBeaconEvents(beaconEvents);
|
||||
room.currentState.processBeaconEvents(events, this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -309,10 +309,10 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
|
||||
*/
|
||||
private getDevicesFromStore(userIds: string[]): DeviceInfoMap {
|
||||
const stored: DeviceInfoMap = {};
|
||||
userIds.map((u) => {
|
||||
userIds.forEach((u) => {
|
||||
stored[u] = {};
|
||||
const devices = this.getStoredDevicesForUser(u) || [];
|
||||
devices.map(function(dev) {
|
||||
devices.forEach(function(dev) {
|
||||
stored[u][dev.deviceId] = dev;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -375,9 +375,7 @@ export class BackupManager {
|
||||
);
|
||||
if (device) {
|
||||
sigInfo.device = device;
|
||||
sigInfo.deviceTrust = await this.baseApis.checkDeviceTrust(
|
||||
this.baseApis.getUserId(), sigInfo.deviceId,
|
||||
);
|
||||
sigInfo.deviceTrust = this.baseApis.checkDeviceTrust(this.baseApis.getUserId(), sigInfo.deviceId);
|
||||
try {
|
||||
await verifySignature(
|
||||
this.baseApis.crypto.olmDevice,
|
||||
@@ -495,7 +493,7 @@ export class BackupManager {
|
||||
rooms[roomId] = { sessions: {} };
|
||||
}
|
||||
|
||||
const sessionData = await this.baseApis.crypto.olmDevice.exportInboundGroupSession(
|
||||
const sessionData = this.baseApis.crypto.olmDevice.exportInboundGroupSession(
|
||||
session.senderKey, session.sessionId, session.sessionData,
|
||||
);
|
||||
sessionData.algorithm = MEGOLM_ALGORITHM;
|
||||
|
||||
+20
-1
@@ -1026,7 +1026,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
const decodedBackupKey = new Uint8Array(olmlib.decodeBase64(
|
||||
fixedBackupKey || sessionBackupKey,
|
||||
));
|
||||
await builder.addSessionBackupPrivateKeyToCache(decodedBackupKey);
|
||||
builder.addSessionBackupPrivateKeyToCache(decodedBackupKey);
|
||||
} else if (this.backupManager.getKeyBackupEnabled()) {
|
||||
// key backup is enabled but we don't have a session backup key in SSSS: see if we have one in
|
||||
// the cache or the user can provide one, and if so, write it to SSSS
|
||||
@@ -1423,6 +1423,25 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether one of our own devices is cross-signed by our
|
||||
* user's stored keys, regardless of whether we trust those keys yet.
|
||||
*
|
||||
* @param {string} deviceId The ID of the device to check
|
||||
*
|
||||
* @returns {boolean} true if the device is cross-signed
|
||||
*/
|
||||
public checkIfOwnDeviceCrossSigned(deviceId: string): boolean {
|
||||
const device = this.deviceList.getStoredDevice(this.userId, deviceId);
|
||||
const userCrossSigning = this.deviceList.getStoredCrossSigningForUser(this.userId);
|
||||
return userCrossSigning.checkDeviceTrust(
|
||||
userCrossSigning,
|
||||
device,
|
||||
false,
|
||||
true,
|
||||
).isCrossSigningVerified();
|
||||
}
|
||||
|
||||
/*
|
||||
* Event handler for DeviceList's userNewDevices event
|
||||
*/
|
||||
|
||||
@@ -193,6 +193,7 @@ function calculateMAC(olmSAS: OlmSAS, method: string) {
|
||||
}
|
||||
|
||||
const calculateKeyAgreement = {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
"curve25519-hkdf-sha256": function(sas: SAS, olmSAS: OlmSAS, bytes: number): Uint8Array {
|
||||
const ourInfo = `${sas.baseApis.getUserId()}|${sas.baseApis.deviceId}|`
|
||||
+ `${sas.ourSASPubKey}|`;
|
||||
|
||||
@@ -796,8 +796,7 @@ export class VerificationRequest<
|
||||
}
|
||||
|
||||
private setupTimeout(phase: Phase): void {
|
||||
const shouldTimeout = !this.timeoutTimer && !this.observeOnly &&
|
||||
phase === PHASE_REQUESTED;
|
||||
const shouldTimeout = !this.timeoutTimer && !this.observeOnly && phase === PHASE_REQUESTED;
|
||||
|
||||
if (shouldTimeout) {
|
||||
this.timeoutTimer = setTimeout(this.cancelOnTimeout, this.timeout);
|
||||
@@ -814,15 +813,15 @@ export class VerificationRequest<
|
||||
}
|
||||
}
|
||||
|
||||
private cancelOnTimeout = () => {
|
||||
private cancelOnTimeout = async () => {
|
||||
try {
|
||||
if (this.initiatedByMe) {
|
||||
this.cancel({
|
||||
await this.cancel({
|
||||
reason: "Other party didn't accept in time",
|
||||
code: "m.timeout",
|
||||
});
|
||||
} else {
|
||||
this.cancel({
|
||||
await this.cancel({
|
||||
reason: "User didn't accept in time",
|
||||
code: "m.timeout",
|
||||
});
|
||||
|
||||
+1
-1
@@ -1105,7 +1105,7 @@ export class AbortError extends Error {
|
||||
* @return {any} the result of the network operation
|
||||
* @throws {ConnectionError} If after maxAttempts the callback still throws ConnectionError
|
||||
*/
|
||||
export async function retryNetworkOperation<T>(maxAttempts: number, callback: () => T): Promise<T> {
|
||||
export async function retryNetworkOperation<T>(maxAttempts: number, callback: () => Promise<T>): Promise<T> {
|
||||
let attempts = 0;
|
||||
let lastConnectionError = null;
|
||||
while (attempts < maxAttempts) {
|
||||
|
||||
@@ -852,14 +852,13 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
}
|
||||
let relationsWithEventType = relationsWithRelType[eventType];
|
||||
|
||||
let relatesToEvent: MatrixEvent;
|
||||
if (!relationsWithEventType) {
|
||||
relationsWithEventType = relationsWithRelType[eventType] = new Relations(
|
||||
relationType,
|
||||
eventType,
|
||||
this.room,
|
||||
);
|
||||
relatesToEvent = this.findEventById(relatesToEventId) || this.room.getPendingEvent(relatesToEventId);
|
||||
const relatesToEvent = this.findEventById(relatesToEventId) || this.room.getPendingEvent(relatesToEventId);
|
||||
if (relatesToEvent) {
|
||||
relationsWithEventType.setTargetEvent(relatesToEvent);
|
||||
}
|
||||
|
||||
+5
-2
@@ -1303,8 +1303,7 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
|
||||
public isRelation(relType: string = undefined): boolean {
|
||||
// Relation info is lifted out of the encrypted content when sent to
|
||||
// encrypted rooms, so we have to check `getWireContent` for this.
|
||||
const content = this.getWireContent();
|
||||
const relation = content && content["m.relates_to"];
|
||||
const relation = this.getWireContent()?.["m.relates_to"];
|
||||
return relation && relation.rel_type && relation.event_id &&
|
||||
((relType && relation.rel_type === relType) || !relType);
|
||||
}
|
||||
@@ -1336,6 +1335,10 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
|
||||
if (this.isRedacted() && newEvent) {
|
||||
return;
|
||||
}
|
||||
// don't allow state events to be replaced using this mechanism as per MSC2676
|
||||
if (this.isState()) {
|
||||
return;
|
||||
}
|
||||
if (this._replacingEvent !== newEvent) {
|
||||
this._replacingEvent = newEvent;
|
||||
this.emit(MatrixEventEvent.Replaced, this);
|
||||
|
||||
+33
-20
@@ -22,15 +22,14 @@ import { RoomMember } from "./room-member";
|
||||
import { logger } from '../logger';
|
||||
import * as utils from "../utils";
|
||||
import { EventType } from "../@types/event";
|
||||
import { MatrixEvent } from "./event";
|
||||
import { MatrixEvent, MatrixEventEvent } from "./event";
|
||||
import { MatrixClient } from "../client";
|
||||
import { GuestAccess, HistoryVisibility, IJoinRuleEventContent, JoinRule } from "../@types/partials";
|
||||
import { TypedEventEmitter } from "./typed-event-emitter";
|
||||
import { Beacon, BeaconEvent, BeaconEventHandlerMap } from "./beacon";
|
||||
import { TypedReEmitter } from "../ReEmitter";
|
||||
import { M_BEACON_INFO } from "../@types/beacon";
|
||||
import { getBeaconInfoIdentifier } from "./beacon";
|
||||
import { BeaconIdentifier } from "..";
|
||||
import { M_BEACON, M_BEACON_INFO } from "../@types/beacon";
|
||||
import { getBeaconInfoIdentifier, BeaconIdentifier } from "./beacon";
|
||||
|
||||
// possible statuses for out-of-band member loading
|
||||
enum OobStatus {
|
||||
@@ -411,7 +410,7 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
this.emit(RoomStateEvent.Update, this);
|
||||
}
|
||||
|
||||
public processBeaconEvents(events: MatrixEvent[]): void {
|
||||
public processBeaconEvents(events: MatrixEvent[], matrixClient: MatrixClient): void {
|
||||
if (
|
||||
!events.length ||
|
||||
// discard locations if we have no beacons
|
||||
@@ -420,24 +419,38 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
return;
|
||||
}
|
||||
|
||||
// names are confusing here
|
||||
// a Beacon is the parent event, but event type is 'm.beacon_info'
|
||||
// a location is the 'child' related to the Beacon, but the event type is 'm.beacon'
|
||||
// group locations by beaconInfo event id
|
||||
const locationEventsByBeaconEventId = events.reduce<Record<string, MatrixEvent[]>>((acc, event) => {
|
||||
const beaconInfoEventId = event.getRelation()?.event_id;
|
||||
if (!acc[beaconInfoEventId]) {
|
||||
acc[beaconInfoEventId] = [];
|
||||
}
|
||||
acc[beaconInfoEventId].push(event);
|
||||
return acc;
|
||||
}, {});
|
||||
const beaconByEventIdDict: Record<string, Beacon> =
|
||||
[...this.beacons.values()].reduce((dict, beacon) => ({ ...dict, [beacon.beaconInfoId]: beacon }), {});
|
||||
|
||||
Object.entries(locationEventsByBeaconEventId).forEach(([beaconInfoEventId, events]) => {
|
||||
const beacon = [...this.beacons.values()].find(beacon => beacon.beaconInfoId === beaconInfoEventId);
|
||||
const processBeaconRelation = (beaconInfoEventId: string, event: MatrixEvent): void => {
|
||||
if (!M_BEACON.matches(event.getType())) {
|
||||
return;
|
||||
}
|
||||
|
||||
const beacon = beaconByEventIdDict[beaconInfoEventId];
|
||||
|
||||
if (beacon) {
|
||||
beacon.addLocations(events);
|
||||
beacon.addLocations([event]);
|
||||
}
|
||||
};
|
||||
|
||||
events.forEach((event: MatrixEvent) => {
|
||||
const relatedToEventId = event.getRelation()?.event_id;
|
||||
// not related to a beacon we know about
|
||||
// discard
|
||||
if (!beaconByEventIdDict[relatedToEventId]) {
|
||||
return;
|
||||
}
|
||||
|
||||
matrixClient.decryptEventIfNeeded(event);
|
||||
|
||||
if (event.isBeingDecrypted() || event.isDecryptionFailure()) {
|
||||
// add an event listener for once the event is decrypted.
|
||||
event.once(MatrixEventEvent.Decrypted, async () => {
|
||||
processBeaconRelation(relatedToEventId, event);
|
||||
});
|
||||
} else {
|
||||
processBeaconRelation(relatedToEventId, event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
+62
-129
@@ -22,7 +22,7 @@ import { EventTimelineSet, DuplicateStrategy } from "./event-timeline-set";
|
||||
import { Direction, EventTimeline } from "./event-timeline";
|
||||
import { getHttpUriForMxc } from "../content-repo";
|
||||
import * as utils from "../utils";
|
||||
import { defer, normalize } from "../utils";
|
||||
import { normalize } from "../utils";
|
||||
import { IEvent, IThreadBundledRelationship, MatrixEvent, MatrixEventEvent, MatrixEventHandlerMap } from "./event";
|
||||
import { EventStatus } from "./event-status";
|
||||
import { RoomMember } from "./room-member";
|
||||
@@ -46,8 +46,8 @@ import {
|
||||
FILTER_RELATED_BY_SENDERS,
|
||||
ThreadFilterType,
|
||||
} from "./thread";
|
||||
import { Method } from "../http-api";
|
||||
import { TypedEventEmitter } from "./typed-event-emitter";
|
||||
import { IStateEventWithRoomId } from "../@types/search";
|
||||
|
||||
// These constants are used as sane defaults when the homeserver doesn't support
|
||||
// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be
|
||||
@@ -55,8 +55,8 @@ import { TypedEventEmitter } from "./typed-event-emitter";
|
||||
// room versions which are considered okay for people to run without being asked
|
||||
// to upgrade (ie: "stable"). Eventually, we should remove these when all homeservers
|
||||
// return an m.room_versions capability.
|
||||
const KNOWN_SAFE_ROOM_VERSION = '6';
|
||||
const SAFE_ROOM_VERSIONS = ['1', '2', '3', '4', '5', '6'];
|
||||
const KNOWN_SAFE_ROOM_VERSION = '9';
|
||||
const SAFE_ROOM_VERSIONS = ['1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
||||
|
||||
function synthesizeReceipt(userId: string, event: MatrixEvent, receiptType: string): MatrixEvent {
|
||||
// console.log("synthesizing receipt for "+event.getId());
|
||||
@@ -214,8 +214,6 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
||||
private getTypeWarning = false;
|
||||
private getVersionWarning = false;
|
||||
private membersPromise?: Promise<boolean>;
|
||||
// Map from threadId to pending Thread instance created by createThreadFetchRoot
|
||||
private threadPromises = new Map<string, Promise<Thread>>();
|
||||
|
||||
// XXX: These should be read-only
|
||||
/**
|
||||
@@ -266,7 +264,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
public threads = new Map<string, Thread>();
|
||||
private threads = new Map<string, Thread>();
|
||||
public lastThread: Thread;
|
||||
|
||||
/**
|
||||
@@ -788,16 +786,9 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
||||
}
|
||||
}
|
||||
|
||||
private async loadMembersFromServer(): Promise<IEvent[]> {
|
||||
private async loadMembersFromServer(): Promise<IStateEventWithRoomId[]> {
|
||||
const lastSyncToken = this.client.store.getSyncToken();
|
||||
const queryString = utils.encodeParams({
|
||||
not_membership: "leave",
|
||||
at: lastSyncToken,
|
||||
});
|
||||
const path = utils.encodeUri("/rooms/$roomId/members?" + queryString,
|
||||
{ $roomId: this.roomId });
|
||||
const http = this.client.http;
|
||||
const response = await http.authedRequest<{ chunk: IEvent[] }>(undefined, Method.Get, path);
|
||||
const response = await this.client.members(this.roomId, undefined, "leave", lastSyncToken);
|
||||
return response.chunk;
|
||||
}
|
||||
|
||||
@@ -806,12 +797,13 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
||||
let fromServer = false;
|
||||
let rawMembersEvents = await this.client.store.getOutOfBandMembers(this.roomId);
|
||||
// If the room is encrypted, we always fetch members from the server at
|
||||
// least once, in case the latest state wasn't persisted properly. Note
|
||||
// least once, in case the latest state wasn't persisted properly. Note
|
||||
// that this function is only called once (unless loading the members
|
||||
// fails), since loadMembersIfNeeded always returns this.membersPromise
|
||||
// if set, which will be the result of the first (successful) call.
|
||||
if (rawMembersEvents === null ||
|
||||
(this.client.isCryptoEnabled() && this.client.isRoomEncrypted(this.roomId))) {
|
||||
(this.client.isCryptoEnabled() && this.client.isRoomEncrypted(this.roomId))
|
||||
) {
|
||||
fromServer = true;
|
||||
rawMembersEvents = await this.loadMembersFromServer();
|
||||
logger.log(`LL: got ${rawMembersEvents.length} ` +
|
||||
@@ -857,7 +849,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
||||
if (fromServer) {
|
||||
const oobMembers = this.currentState.getMembers()
|
||||
.filter((m) => m.isOutOfBand())
|
||||
.map((m) => m.events.member.event as IEvent);
|
||||
.map((m) => m.events.member.event as IStateEventWithRoomId);
|
||||
logger.log(`LL: telling store to write ${oobMembers.length}`
|
||||
+ ` members for room ${this.roomId}`);
|
||||
const store = this.client.store;
|
||||
@@ -1214,9 +1206,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
||||
* @experimental
|
||||
*/
|
||||
public getThread(eventId: string): Thread {
|
||||
return this.getThreads().find(thread => {
|
||||
return thread.id === eventId;
|
||||
});
|
||||
return this.threads.get(eventId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1530,7 +1520,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
||||
}
|
||||
|
||||
if (!this.getThread(rootEvent.getId())) {
|
||||
this.createThread(rootEvent, [], true);
|
||||
this.createThread(rootEvent.getId(), rootEvent, [], true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1612,16 +1602,6 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
||||
};
|
||||
}
|
||||
|
||||
// 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,
|
||||
@@ -1636,58 +1616,14 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
||||
return threadId ? this.getThread(threadId) : null;
|
||||
}
|
||||
|
||||
public async createThreadFetchRoot(
|
||||
threadId: string,
|
||||
events?: MatrixEvent[],
|
||||
toStartOfTimeline?: boolean,
|
||||
): Promise<Thread | null> {
|
||||
private addThreadedEvents(threadId: string, events: MatrixEvent[], toStartOfTimeline = false): void {
|
||||
let thread = this.getThread(threadId);
|
||||
|
||||
if (!thread) {
|
||||
const deferred = defer<Thread | null>();
|
||||
this.threadPromises.set(threadId, deferred.promise);
|
||||
|
||||
let rootEvent = this.findEventById(threadId);
|
||||
// If the rootEvent does not exist in the local stores, then fetch it from the server.
|
||||
try {
|
||||
const eventData = await this.client.fetchRoomEvent(this.roomId, threadId);
|
||||
const mapper = this.client.getEventMapper();
|
||||
rootEvent = mapper(eventData); // will merge with existing event object if such is known
|
||||
} catch (e) {
|
||||
logger.error("Failed to fetch thread root to construct thread with", e);
|
||||
} finally {
|
||||
this.threadPromises.delete(threadId);
|
||||
// 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);
|
||||
}
|
||||
deferred.resolve(thread);
|
||||
}
|
||||
}
|
||||
|
||||
return thread;
|
||||
}
|
||||
|
||||
private async addThreadedEvents(events: MatrixEvent[], threadId: string, toStartOfTimeline = false): Promise<void> {
|
||||
let thread = this.getThread(threadId);
|
||||
if (this.threadPromises.has(threadId)) {
|
||||
thread = await this.threadPromises.get(threadId);
|
||||
}
|
||||
|
||||
events = events.filter(e => e.getId() !== threadId); // filter out any root events
|
||||
|
||||
if (thread) {
|
||||
for (const event of events) {
|
||||
await thread.addEvent(event, toStartOfTimeline);
|
||||
}
|
||||
thread.addEvents(events, toStartOfTimeline);
|
||||
} else {
|
||||
thread = await this.createThreadFetchRoot(threadId, events, toStartOfTimeline);
|
||||
}
|
||||
|
||||
if (thread) {
|
||||
const rootEvent = this.findEventById(threadId) ?? events.find(e => e.getId() === threadId);
|
||||
thread = this.createThread(threadId, rootEvent, events, toStartOfTimeline);
|
||||
this.emit(ThreadEvent.Update, thread);
|
||||
}
|
||||
}
|
||||
@@ -1696,28 +1632,29 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
||||
* Adds events to a thread's timeline. Will fire "Thread.update"
|
||||
* @experimental
|
||||
*/
|
||||
public async processThreadedEvents(events: MatrixEvent[], toStartOfTimeline: boolean): Promise<unknown> {
|
||||
public processThreadedEvents(events: MatrixEvent[], toStartOfTimeline: boolean): void {
|
||||
events.forEach(this.applyRedaction);
|
||||
|
||||
const eventsByThread: { [threadId: string]: MatrixEvent[] } = {};
|
||||
for (const event of events) {
|
||||
const { threadId } = this.eventShouldLiveIn(event);
|
||||
if (!eventsByThread[threadId]) {
|
||||
const { threadId, shouldLiveInThread } = this.eventShouldLiveIn(event);
|
||||
if (shouldLiveInThread && !eventsByThread[threadId]) {
|
||||
eventsByThread[threadId] = [];
|
||||
}
|
||||
eventsByThread[threadId].push(event);
|
||||
eventsByThread[threadId]?.push(event);
|
||||
}
|
||||
|
||||
return Promise.all(Object.entries(eventsByThread).map(([threadId, events]) => (
|
||||
this.addThreadedEvents(events, threadId, toStartOfTimeline)
|
||||
)));
|
||||
Object.entries(eventsByThread).map(([threadId, events]) => (
|
||||
this.addThreadedEvents(threadId, events, toStartOfTimeline)
|
||||
));
|
||||
}
|
||||
|
||||
public createThread(
|
||||
threadId: string,
|
||||
rootEvent: MatrixEvent | undefined,
|
||||
events: MatrixEvent[] = [],
|
||||
toStartOfTimeline: boolean,
|
||||
): Thread | undefined {
|
||||
): Thread {
|
||||
if (rootEvent) {
|
||||
const tl = this.getTimelineForEvent(rootEvent.getId());
|
||||
const relatedEvents = tl?.getTimelineSet().getAllRelationsEventForEvent(rootEvent.getId());
|
||||
@@ -1726,45 +1663,44 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
||||
}
|
||||
}
|
||||
|
||||
const thread = new Thread(rootEvent, {
|
||||
const thread = new Thread(threadId, rootEvent, {
|
||||
initialEvents: events,
|
||||
room: this,
|
||||
client: this.client,
|
||||
});
|
||||
|
||||
// If we managed to create a thread and figure out its `id` then we can use it
|
||||
if (thread.id) {
|
||||
this.threads.set(thread.id, thread);
|
||||
this.reEmitter.reEmit(thread, [
|
||||
ThreadEvent.Update,
|
||||
ThreadEvent.NewReply,
|
||||
RoomEvent.Timeline,
|
||||
RoomEvent.TimelineReset,
|
||||
]);
|
||||
this.threads.set(thread.id, thread);
|
||||
this.reEmitter.reEmit(thread, [
|
||||
ThreadEvent.Update,
|
||||
ThreadEvent.NewReply,
|
||||
RoomEvent.Timeline,
|
||||
RoomEvent.TimelineReset,
|
||||
]);
|
||||
|
||||
if (!this.lastThread || this.lastThread.rootEvent?.localTimestamp < rootEvent?.localTimestamp) {
|
||||
this.lastThread = 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;
|
||||
if (!this.lastThread || this.lastThread.rootEvent?.localTimestamp < rootEvent?.localTimestamp) {
|
||||
this.lastThread = 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;
|
||||
}
|
||||
|
||||
private applyRedaction = (event: MatrixEvent): void => {
|
||||
@@ -2205,7 +2141,6 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
||||
}
|
||||
|
||||
const threadRoots = this.findThreadRoots(events);
|
||||
const threadInfos = events.map(e => this.eventShouldLiveIn(e, events, threadRoots));
|
||||
const eventsByThread: { [threadId: string]: MatrixEvent[] } = {};
|
||||
|
||||
for (let i = 0; i < events.length; i++) {
|
||||
@@ -2216,14 +2151,12 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
||||
shouldLiveInRoom,
|
||||
shouldLiveInThread,
|
||||
threadId,
|
||||
} = threadInfos[i];
|
||||
} = this.eventShouldLiveIn(events[i], events, threadRoots);
|
||||
|
||||
if (shouldLiveInThread) {
|
||||
if (!eventsByThread[threadId]) {
|
||||
eventsByThread[threadId] = [];
|
||||
}
|
||||
eventsByThread[threadId].push(events[i]);
|
||||
if (shouldLiveInThread && !eventsByThread[threadId]) {
|
||||
eventsByThread[threadId] = [];
|
||||
}
|
||||
eventsByThread[threadId]?.push(events[i]);
|
||||
|
||||
if (shouldLiveInRoom) {
|
||||
this.addLiveEvent(events[i], duplicateStrategy, fromCache);
|
||||
@@ -2231,7 +2164,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
||||
}
|
||||
|
||||
Object.entries(eventsByThread).forEach(([threadId, threadEvents]) => {
|
||||
this.addThreadedEvents(threadEvents, threadId, false);
|
||||
this.addThreadedEvents(threadId, threadEvents, false);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+75
-45
@@ -70,12 +70,11 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
||||
public readonly room: Room;
|
||||
public readonly client: MatrixClient;
|
||||
|
||||
public initialEventsFetched = false;
|
||||
|
||||
public readonly id: string;
|
||||
public initialEventsFetched = !Thread.hasServerSideSupport;
|
||||
|
||||
constructor(
|
||||
public readonly rootEvent: MatrixEvent | undefined,
|
||||
public readonly id: string,
|
||||
public rootEvent: MatrixEvent | undefined,
|
||||
opts: IThreadOpts,
|
||||
) {
|
||||
super();
|
||||
@@ -99,12 +98,33 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
||||
this.room.on(RoomEvent.LocalEchoUpdated, this.onEcho);
|
||||
this.timelineSet.on(RoomEvent.Timeline, this.onEcho);
|
||||
|
||||
// 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
|
||||
this.id = rootEvent?.getId() ?? opts?.initialEvents?.find(event => event.isThreadRelation)?.relationEventId;
|
||||
this.initialiseThread(this.rootEvent);
|
||||
if (opts.initialEvents) {
|
||||
this.addEvents(opts.initialEvents, false);
|
||||
}
|
||||
// even if this thread is thought to be originating from this client, we initialise it as we may be in a
|
||||
// gappy sync and a thread around this event may already exist.
|
||||
this.initialiseThread();
|
||||
|
||||
opts?.initialEvents?.forEach(event => this.addEvent(event, false));
|
||||
this.rootEvent?.setThread(this);
|
||||
}
|
||||
|
||||
private async fetchRootEvent(): Promise<void> {
|
||||
this.rootEvent = this.room.findEventById(this.id);
|
||||
// If the rootEvent does not exist in the local stores, then fetch it from the server.
|
||||
try {
|
||||
const eventData = await this.client.fetchRoomEvent(this.roomId, this.id);
|
||||
const mapper = this.client.getEventMapper();
|
||||
this.rootEvent = mapper(eventData); // will merge with existing event object if such is known
|
||||
} catch (e) {
|
||||
logger.error("Failed to fetch thread root to construct thread with", e);
|
||||
}
|
||||
|
||||
// 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
|
||||
this.rootEvent?.setThread(this);
|
||||
|
||||
this.emit(ThreadEvent.Update, this);
|
||||
}
|
||||
|
||||
public static setServerSideSupport(hasServerSideSupport: boolean, useStable: boolean): void {
|
||||
@@ -119,6 +139,7 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
||||
private onBeforeRedaction = (event: MatrixEvent, redaction: MatrixEvent) => {
|
||||
if (event?.isRelation(THREAD_RELATION_TYPE.name) &&
|
||||
this.room.eventShouldLiveIn(event).threadId === this.id &&
|
||||
event.getId() !== this.id && // the root event isn't counted in the length so ignore this redaction
|
||||
!redaction.status // only respect it when it succeeds
|
||||
) {
|
||||
this.replyCount--;
|
||||
@@ -179,6 +200,11 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
||||
}
|
||||
}
|
||||
|
||||
public addEvents(events: MatrixEvent[], toStartOfTimeline: boolean): void {
|
||||
events.forEach(ev => this.addEvent(ev, toStartOfTimeline, false));
|
||||
this.emit(ThreadEvent.Update, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an event to the thread and updates
|
||||
* the tail/root references if needed
|
||||
@@ -186,43 +212,59 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
||||
* @param event The event to add
|
||||
* @param {boolean} toStartOfTimeline whether the event is being added
|
||||
* to the start (and not the end) of the timeline.
|
||||
* @param {boolean} emit whether to emit the Update event if the thread was updated or not.
|
||||
*/
|
||||
public async addEvent(event: MatrixEvent, toStartOfTimeline: boolean): Promise<void> {
|
||||
public addEvent(event: MatrixEvent, toStartOfTimeline: boolean, emit = true): void {
|
||||
event.setThread(this);
|
||||
|
||||
if (!this._currentUserParticipated && event.getSender() === this.client.getUserId()) {
|
||||
this._currentUserParticipated = true;
|
||||
}
|
||||
|
||||
// Add all annotations and replace relations to the timeline so that the relations are processed accordingly
|
||||
if ([RelationType.Annotation, RelationType.Replace].includes(event.getRelation()?.rel_type as RelationType)) {
|
||||
this.addEventToTimeline(event, toStartOfTimeline);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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
|
||||
// is held in the main room timeline
|
||||
// We want to fetch the room state from there and pass it down to this thread
|
||||
// timeline set to let it reconcile an event with its relevant RoomMember
|
||||
|
||||
event.setThread(this);
|
||||
this.addEventToTimeline(event, toStartOfTimeline);
|
||||
|
||||
await this.client.decryptEventIfNeeded(event, {});
|
||||
this.client.decryptEventIfNeeded(event, {});
|
||||
} else if (!toStartOfTimeline &&
|
||||
this.initialEventsFetched &&
|
||||
event.localTimestamp > this.lastReply().localTimestamp
|
||||
event.localTimestamp > this.lastReply()?.localTimestamp
|
||||
) {
|
||||
await this.fetchEditsWhereNeeded(event);
|
||||
this.fetchEditsWhereNeeded(event);
|
||||
this.addEventToTimeline(event, false);
|
||||
}
|
||||
|
||||
if (!this._currentUserParticipated && event.getSender() === this.client.getUserId()) {
|
||||
this._currentUserParticipated = true;
|
||||
}
|
||||
|
||||
// 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 && event.isRelation(THREAD_RELATION_TYPE.name)) {
|
||||
if ((!Thread.hasServerSideSupport || !this.rootEvent) && event.isRelation(THREAD_RELATION_TYPE.name)) {
|
||||
this.replyCount++;
|
||||
}
|
||||
|
||||
this.emit(ThreadEvent.Update, this);
|
||||
if (emit) {
|
||||
this.emit(ThreadEvent.Update, this);
|
||||
}
|
||||
}
|
||||
|
||||
private initialiseThread(rootEvent: MatrixEvent | undefined): void {
|
||||
const bundledRelationship = rootEvent
|
||||
?.getServerAggregatedRelation<IThreadBundledRelationship>(THREAD_RELATION_TYPE.name);
|
||||
private getRootEventBundledRelationship(rootEvent = this.rootEvent): IThreadBundledRelationship {
|
||||
return rootEvent?.getServerAggregatedRelation<IThreadBundledRelationship>(THREAD_RELATION_TYPE.name);
|
||||
}
|
||||
|
||||
private async initialiseThread(): Promise<void> {
|
||||
let bundledRelationship = this.getRootEventBundledRelationship();
|
||||
if (Thread.hasServerSideSupport && !bundledRelationship) {
|
||||
await this.fetchRootEvent();
|
||||
bundledRelationship = this.getRootEventBundledRelationship();
|
||||
}
|
||||
|
||||
if (Thread.hasServerSideSupport && bundledRelationship) {
|
||||
this.replyCount = bundledRelationship.count;
|
||||
@@ -235,6 +277,8 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
||||
|
||||
this.fetchEditsWhereNeeded(event);
|
||||
}
|
||||
|
||||
this.emit(ThreadEvent.Update, this);
|
||||
}
|
||||
|
||||
// XXX: Workaround for https://github.com/matrix-org/matrix-spec-proposals/pull/2676/files#r827240084
|
||||
@@ -252,24 +296,10 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
||||
}));
|
||||
}
|
||||
|
||||
public async fetchInitialEvents(): Promise<{
|
||||
originalEvent: MatrixEvent;
|
||||
events: MatrixEvent[];
|
||||
nextBatch?: string;
|
||||
prevBatch?: string;
|
||||
} | null> {
|
||||
if (!Thread.hasServerSideSupport) {
|
||||
this.initialEventsFetched = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.fetchEvents();
|
||||
this.initialEventsFetched = true;
|
||||
return response;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
public async fetchInitialEvents(): Promise<void> {
|
||||
if (this.initialEventsFetched) return;
|
||||
await this.fetchEvents();
|
||||
this.initialEventsFetched = true;
|
||||
}
|
||||
|
||||
private setEventMetadata(event: MatrixEvent): void {
|
||||
@@ -318,7 +348,7 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
||||
* A getter for the last event added to the thread
|
||||
*/
|
||||
public get replyToEvent(): MatrixEvent {
|
||||
return this.lastEvent;
|
||||
return this.lastEvent ?? this.lastReply();
|
||||
}
|
||||
|
||||
public get events(): MatrixEvent[] {
|
||||
@@ -337,7 +367,7 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
||||
return this.timelineSet.getLiveTimeline();
|
||||
}
|
||||
|
||||
public async fetchEvents(opts: IRelationsRequestOpts = { limit: 20 }): Promise<{
|
||||
public async fetchEvents(opts: IRelationsRequestOpts = { limit: 20, direction: Direction.Backward }): Promise<{
|
||||
originalEvent: MatrixEvent;
|
||||
events: MatrixEvent[];
|
||||
nextBatch?: string;
|
||||
@@ -369,7 +399,7 @@ export class Thread extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
|
||||
return this.client.decryptEventIfNeeded(event);
|
||||
}));
|
||||
|
||||
const prependEvents = !opts.direction || opts.direction === Direction.Backward;
|
||||
const prependEvents = (opts.direction ?? Direction.Backward) === Direction.Backward;
|
||||
|
||||
this.timelineSet.addEventsToTimeline(
|
||||
events,
|
||||
|
||||
+4
-3
@@ -17,11 +17,12 @@ limitations under the License.
|
||||
import { EventType } from "../@types/event";
|
||||
import { Room } from "../models/room";
|
||||
import { User } from "../models/user";
|
||||
import { IEvent, MatrixEvent } from "../models/event";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { Filter } from "../filter";
|
||||
import { RoomSummary } from "../models/room-summary";
|
||||
import { IMinimalEvent, IRooms, ISyncResponse } from "../sync-accumulator";
|
||||
import { IStartClientOpts } from "../client";
|
||||
import { IStateEventWithRoomId } from "../@types/search";
|
||||
|
||||
export interface ISavedSync {
|
||||
nextBatch: string;
|
||||
@@ -204,9 +205,9 @@ export interface IStore {
|
||||
*/
|
||||
deleteAllData(): Promise<void>;
|
||||
|
||||
getOutOfBandMembers(roomId: string): Promise<IEvent[] | null>;
|
||||
getOutOfBandMembers(roomId: string): Promise<IStateEventWithRoomId[] | null>;
|
||||
|
||||
setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void>;
|
||||
setOutOfBandMembers(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise<void>;
|
||||
|
||||
clearOutOfBandMembers(roomId: string): Promise<void>;
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { ISavedSync } from "./index";
|
||||
import { IEvent, IStartClientOpts, ISyncResponse } from "..";
|
||||
import { IEvent, IStartClientOpts, IStateEventWithRoomId, ISyncResponse } from "..";
|
||||
|
||||
export interface IIndexedDBBackend {
|
||||
connect(): Promise<void>;
|
||||
@@ -25,8 +25,8 @@ export interface IIndexedDBBackend {
|
||||
getSavedSync(): Promise<ISavedSync>;
|
||||
getNextBatchToken(): Promise<string>;
|
||||
clearDatabase(): Promise<void>;
|
||||
getOutOfBandMembers(roomId: string): Promise<IEvent[] | null>;
|
||||
setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void>;
|
||||
getOutOfBandMembers(roomId: string): Promise<IStateEventWithRoomId[] | null>;
|
||||
setOutOfBandMembers(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise<void>;
|
||||
clearOutOfBandMembers(roomId: string): Promise<void>;
|
||||
getUserPresenceEvents(): Promise<UserTuple[]>;
|
||||
getClientOptions(): Promise<IStartClientOpts>;
|
||||
|
||||
@@ -18,7 +18,7 @@ import { IMinimalEvent, ISyncData, ISyncResponse, SyncAccumulator } from "../syn
|
||||
import * as utils from "../utils";
|
||||
import * as IndexedDBHelpers from "../indexeddb-helpers";
|
||||
import { logger } from '../logger';
|
||||
import { IEvent, IStartClientOpts } from "..";
|
||||
import { IStartClientOpts, IStateEventWithRoomId } from "..";
|
||||
import { ISavedSync } from "./index";
|
||||
import { IIndexedDBBackend, UserTuple } from "./indexeddb-backend";
|
||||
|
||||
@@ -229,15 +229,15 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
|
||||
* @returns {Promise<event[]>} the events, potentially an empty array if OOB loading didn't yield any new members
|
||||
* @returns {null} in case the members for this room haven't been stored yet
|
||||
*/
|
||||
public getOutOfBandMembers(roomId: string): Promise<IEvent[] | null> {
|
||||
return new Promise<IEvent[] | null>((resolve, reject) => {
|
||||
public getOutOfBandMembers(roomId: string): Promise<IStateEventWithRoomId[] | null> {
|
||||
return new Promise<IStateEventWithRoomId[] | null>((resolve, reject) => {
|
||||
const tx = this.db.transaction(["oob_membership_events"], "readonly");
|
||||
const store = tx.objectStore("oob_membership_events");
|
||||
const roomIndex = store.index("room");
|
||||
const range = IDBKeyRange.only(roomId);
|
||||
const request = roomIndex.openCursor(range);
|
||||
|
||||
const membershipEvents: IEvent[] = [];
|
||||
const membershipEvents: IStateEventWithRoomId[] = [];
|
||||
// did we encounter the oob_written marker object
|
||||
// amongst the results? That means OOB member
|
||||
// loading already happened for this room
|
||||
@@ -278,7 +278,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
|
||||
* @param {string} roomId
|
||||
* @param {event[]} membershipEvents the membership events to store
|
||||
*/
|
||||
public async setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void> {
|
||||
public async setOutOfBandMembers(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise<void> {
|
||||
logger.log(`LL: backend about to store ${membershipEvents.length}` +
|
||||
` members for ${roomId}`);
|
||||
const tx = this.db.transaction(["oob_membership_events"], "readwrite");
|
||||
@@ -530,9 +530,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
|
||||
const txn = this.db.transaction(["client_options"], "readonly");
|
||||
const store = txn.objectStore("client_options");
|
||||
return selectQuery(store, undefined, (cursor) => {
|
||||
if (cursor.value && cursor.value && cursor.value.options) {
|
||||
return cursor.value.options;
|
||||
}
|
||||
return cursor.value?.options;
|
||||
}).then((results) => results[0]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import { logger } from "../logger";
|
||||
import { defer, IDeferred } from "../utils";
|
||||
import { ISavedSync } from "./index";
|
||||
import { IStartClientOpts } from "../client";
|
||||
import { IEvent, ISyncResponse } from "..";
|
||||
import { IStateEventWithRoomId, ISyncResponse } from "..";
|
||||
import { IIndexedDBBackend, UserTuple } from "./indexeddb-backend";
|
||||
|
||||
export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend {
|
||||
@@ -97,7 +97,7 @@ export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend {
|
||||
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
|
||||
* @returns {null} in case the members for this room haven't been stored yet
|
||||
*/
|
||||
public getOutOfBandMembers(roomId: string): Promise<IEvent[] | null> {
|
||||
public getOutOfBandMembers(roomId: string): Promise<IStateEventWithRoomId[] | null> {
|
||||
return this.doCmd('getOutOfBandMembers', [roomId]);
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend {
|
||||
* @param {event[]} membershipEvents the membership events to store
|
||||
* @returns {Promise} when all members have been stored
|
||||
*/
|
||||
public setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void> {
|
||||
public setOutOfBandMembers(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise<void> {
|
||||
return this.doCmd('setOutOfBandMembers', [roomId, membershipEvents]);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import { ISavedSync } from "./index";
|
||||
import { IIndexedDBBackend } from "./indexeddb-backend";
|
||||
import { ISyncResponse } from "../sync-accumulator";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||
import { IStateEventWithRoomId } from "../@types/search";
|
||||
|
||||
/**
|
||||
* This is an internal module. See {@link IndexedDBStore} for the public class.
|
||||
@@ -242,7 +243,7 @@ export class IndexedDBStore extends MemoryStore {
|
||||
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
|
||||
* @returns {null} in case the members for this room haven't been stored yet
|
||||
*/
|
||||
public getOutOfBandMembers = this.degradable((roomId: string): Promise<IEvent[]> => {
|
||||
public getOutOfBandMembers = this.degradable((roomId: string): Promise<IStateEventWithRoomId[]> => {
|
||||
return this.backend.getOutOfBandMembers(roomId);
|
||||
}, "getOutOfBandMembers");
|
||||
|
||||
@@ -254,10 +255,13 @@ export class IndexedDBStore extends MemoryStore {
|
||||
* @param {event[]} membershipEvents the membership events to store
|
||||
* @returns {Promise} when all members have been stored
|
||||
*/
|
||||
public setOutOfBandMembers = this.degradable((roomId: string, membershipEvents: IEvent[]): Promise<void> => {
|
||||
super.setOutOfBandMembers(roomId, membershipEvents);
|
||||
return this.backend.setOutOfBandMembers(roomId, membershipEvents);
|
||||
}, "setOutOfBandMembers");
|
||||
public setOutOfBandMembers = this.degradable(
|
||||
(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise<void> => {
|
||||
super.setOutOfBandMembers(roomId, membershipEvents);
|
||||
return this.backend.setOutOfBandMembers(roomId, membershipEvents);
|
||||
},
|
||||
"setOutOfBandMembers",
|
||||
);
|
||||
|
||||
public clearOutOfBandMembers = this.degradable((roomId: string) => {
|
||||
super.clearOutOfBandMembers(roomId);
|
||||
|
||||
+5
-4
@@ -22,13 +22,14 @@ limitations under the License.
|
||||
import { EventType } from "../@types/event";
|
||||
import { Room } from "../models/room";
|
||||
import { User } from "../models/user";
|
||||
import { IEvent, MatrixEvent } from "../models/event";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { RoomState, RoomStateEvent } from "../models/room-state";
|
||||
import { RoomMember } from "../models/room-member";
|
||||
import { Filter } from "../filter";
|
||||
import { ISavedSync, IStore } from "./index";
|
||||
import { RoomSummary } from "../models/room-summary";
|
||||
import { ISyncResponse } from "../sync-accumulator";
|
||||
import { IStateEventWithRoomId } from "../@types/search";
|
||||
|
||||
function isValidFilterId(filterId: string): boolean {
|
||||
const isValidStr = typeof filterId === "string" &&
|
||||
@@ -60,7 +61,7 @@ export class MemoryStore implements IStore {
|
||||
private filters: Record<string, Record<string, Filter>> = {};
|
||||
public accountData: Record<string, MatrixEvent> = {}; // type : content
|
||||
private readonly localStorage: Storage;
|
||||
private oobMembers: Record<string, IEvent[]> = {}; // roomId: [member events]
|
||||
private oobMembers: Record<string, IStateEventWithRoomId[]> = {}; // roomId: [member events]
|
||||
private clientOptions = {};
|
||||
|
||||
constructor(opts: IOpts = {}) {
|
||||
@@ -389,7 +390,7 @@ export class MemoryStore implements IStore {
|
||||
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
|
||||
* @returns {null} in case the members for this room haven't been stored yet
|
||||
*/
|
||||
public getOutOfBandMembers(roomId: string): Promise<IEvent[] | null> {
|
||||
public getOutOfBandMembers(roomId: string): Promise<IStateEventWithRoomId[] | null> {
|
||||
return Promise.resolve(this.oobMembers[roomId] || null);
|
||||
}
|
||||
|
||||
@@ -401,7 +402,7 @@ export class MemoryStore implements IStore {
|
||||
* @param {event[]} membershipEvents the membership events to store
|
||||
* @returns {Promise} when all members have been stored
|
||||
*/
|
||||
public setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void> {
|
||||
public setOutOfBandMembers(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise<void> {
|
||||
this.oobMembers[roomId] = membershipEvents;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
+4
-3
@@ -22,11 +22,12 @@ limitations under the License.
|
||||
import { EventType } from "../@types/event";
|
||||
import { Room } from "../models/room";
|
||||
import { User } from "../models/user";
|
||||
import { IEvent, MatrixEvent } from "../models/event";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { Filter } from "../filter";
|
||||
import { ISavedSync, IStore } from "./index";
|
||||
import { RoomSummary } from "../models/room-summary";
|
||||
import { ISyncResponse } from "../sync-accumulator";
|
||||
import { IStateEventWithRoomId } from "../@types/search";
|
||||
|
||||
/**
|
||||
* Construct a stub store. This does no-ops on most store methods.
|
||||
@@ -242,11 +243,11 @@ export class StubStore implements IStore {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
public getOutOfBandMembers(): Promise<IEvent[]> {
|
||||
public getOutOfBandMembers(): Promise<IStateEventWithRoomId[]> {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
public setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void> {
|
||||
public setOutOfBandMembers(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -464,7 +464,7 @@ export function defer<T = void>(): IDeferred<T> {
|
||||
}
|
||||
|
||||
export async function promiseMapSeries<T>(
|
||||
promises: T[],
|
||||
promises: Array<T | Promise<T>>,
|
||||
fn: (t: T) => void,
|
||||
): Promise<void> {
|
||||
for (const o of promises) {
|
||||
|
||||
Reference in New Issue
Block a user