Compare commits

..

37 Commits

Author SHA1 Message Date
RiotRobot ef97df8ed0 v38.3.0 2025-09-23 12:12:22 +00:00
RiotRobot 0ce751c462 v38.3.0-rc.0 2025-09-16 14:09:05 +00:00
RiotRobot 2706873948 Merge branch 'master' into develop 2025-09-16 11:40:18 +00:00
RiotRobot 4a9006aea6 v38.2.0 2025-09-16 11:39:28 +00:00
Michael Telatynski 43c72d5bf5 Merge commit from fork
* Validate room upgrade relationships in MatrixClient::getJoinedRooms

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-09-16 12:36:14 +01:00
Andy Balaam e551b92a07 Update matrix-sdk-crypto-wasm to 15.2.0 (#4991)
* Update matrix-sdk-crypto-wasm to 15.2.0

Most relevant changes:

-   History sharing: improve efficiency of building key bundle
    ([matrix-rust-sdk#5513](https://github.com/matrix-org/matrix-rust-sdk/issues/5513))

* Work around matrix-rust-sdk#5643

Modify the message content coming from Rust API to include the missing
property `msgtype: m.key.verification.request`
2025-09-11 14:53:13 +00:00
Richard van der Hoff 32f51e852b History sharing: do /keys/query before checking for key bundle (#4992)
* History sharing: do `/keys/query` before checking for key bundle

The next release of matrix-sdk-crypto-wasm will check that the device that sent
us the key bundle data was correctly cross-signed by its owner, which means we
need to have the owner's cross-signing keys before we check if we have the
bundle.

This replicates a change made in the Rust SDK, at https://github.com/matrix-org/matrix-rust-sdk/pull/5510/files#diff-9f89fa75c4eb3743ae674be1bb90f75169bd815a259917799c71b8a546449d51R133-R140.

* fix unit test

* Comment
2025-09-11 10:38:14 +00:00
RiotRobot 82aa04d894 Merge branch 'master' into develop 2025-09-09 16:30:39 +00:00
RiotRobot ff89c9ec42 v38.1.0 2025-09-09 16:29:55 +00:00
Svajūnas Budrys ccd825fb39 Remove knock state on join (#4977)
Signed-off-by: Svajunas Budrys <svajunas.budrys.sb@gmail.com>
2025-09-04 09:31:26 +00:00
renovate[bot] b313eb5912 Update dependency @types/sdp-transform to v2.15.0 (#4987)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-03 09:06:51 +00:00
renovate[bot] 2b12675675 Update typescript-eslint monorepo to v8.41.0 (#4989)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-02 17:14:06 +00:00
renovate[bot] 246788b874 Update shogo82148/actions-upload-release-asset digest to e6cd457 (#4985)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-02 17:13:57 +00:00
renovate[bot] b0b80401aa Update actions/upload-pages-artifact action to v4 (#4990)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-02 13:39:36 +00:00
renovate[bot] 6a0164f37f Update dependency knip to v5.63.0 (#4988)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-02 13:30:21 +00:00
renovate[bot] f963d61bcb Update typedoc (#4986)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-02 13:29:33 +00:00
RiotRobot b32619ad24 v38.1.0-rc.0 2025-09-02 12:53:40 +00:00
Tulir Asokan 3d3c3ba55f Fix m.topic format (#4984)
* Fix m.topic format

Fixes #4902

Signed-off-by: Tulir Asokan <tulir@maunium.net>

* Update tests

Signed-off-by: Tulir Asokan <tulir@maunium.net>

* Fix formatting

* Re-add temporary support for invalid form

Signed-off-by: Tulir Asokan <tulir@maunium.net>

---------

Signed-off-by: Tulir Asokan <tulir@maunium.net>
2025-09-02 12:14:08 +00:00
Timo d62c658a72 Remove custom org.matrix.msc4075.rtc.notification.parent relation type (#4979)
* Remove custom `org.matrix.msc4075.rtc.notification.parent` relation type

Signed-off-by: Timo K <toger5@hotmail.de>

* fix test

Signed-off-by: Timo K <toger5@hotmail.de>

* Fix test post rebase

---------

Signed-off-by: Timo K <toger5@hotmail.de>
Co-authored-by: Valere <bill.carson@valrsoft.com>
2025-09-02 08:36:26 +00:00
David Baker bdc4a69023 Fix stable-suffixed MSC4133 support (#4983)
* Fix stable-suffixed MSC4133 support

It looked for the ".stable" suffixed feature to work out what URL to use but not to see whether the server supported it.

This will only be relevant until the next spec release but may as well fix it.

See also https://github.com/element-hq/element-web/pull/30649

* Fix awaiting
2025-09-01 15:34:01 +01:00
Bas Nijholt ab892420b5 Fix thread edit aggregation race condition (#4980)
* test(thread): add regression tests for edit-race; ensure reaction aggregation idempotence

- Edit race: add failing test when `Replace` aggregated pre-init
- Reaction: ensure pre-init aggregation and dedup on replay
- Strengthen assertions for ordering and content

* fix(thread): apply edits after init; keep reactions pre-init; remove redundant aggregation
- Defer Replace aggregation until thread initialised and event is in timeline
- Aggregate Annotation pre-init to preserve reaction summaries
- Rely on EventTimelineSet to aggregate post-insert
- Fixes: element-hq/element-web#30617

* style: run prettier; docs: clarify reaction pre-init comment about counts
2025-08-28 09:03:33 +00:00
RiotRobot ed607c48b0 Merge branch 'master' into develop 2025-08-27 13:32:25 +00:00
Timo 2f1d654f14 MatrixRTC: Add RTC decline event (#4978)
* Add decline event

Signed-off-by: Timo K <toger5@hotmail.de>

* add `client.sendRtcDecline`.

Signed-off-by: Timo K <toger5@hotmail.de>

* remove `decline.reason` field.

Signed-off-by: Timo K <toger5@hotmail.de>

---------

Signed-off-by: Timo K <toger5@hotmail.de>
2025-08-26 15:21:40 +00:00
Timo c4c7f94514 Make a MatrixRTCSession emit once the RTCNotification is sent (#4976)
* MatrixRTCSession emits once the rtc notification is sent.

Signed-off-by: Timo K <toger5@hotmail.de>

* update correct type description

Signed-off-by: Timo K <toger5@hotmail.de>

* Add test

Signed-off-by: Timo K <toger5@hotmail.de>

* fix imports

Signed-off-by: Timo K <toger5@hotmail.de>

---------

Signed-off-by: Timo K <toger5@hotmail.de>
2025-08-26 15:16:02 +00:00
David Baker 1fac06e223 Use hydra semantics for unknown room versions (#4957)
This inverts the check for whether to use hydra semantics to only NOT use
it for known, old room versions and use hydra for everything else, so
rooms with versions we don't know about will use hydra semantics.

This will cause any rooms using old/experiental versions unknown to
the js-sdk to break, but will mean that wehn the next room version
comes out, we'll use hydra for it which is, of course, not a given,
but is way more likely than going back to the old semantics.

The mobile Element clients currently hardcode hydra versions (ie.
as it is without this change, but we expect them to make this same
change soon after the hydra release.

We do NOT expect this to land with the hydra release, but target it for
the release after.

Reverts 1e5054a8ff87f83b0875916aa16f435853bf165a from https://github.com/matrix-org/matrix-js-sdk/pull/4937

See https://github.com/element-hq/element-meta/issues/2921 for public
discussion.
2025-08-26 10:49:44 +00:00
Michael Telatynski b80d0091d2 Release tranche of breaking changes (#4963)
* Remove deprecated `IJoinRoomOpts.syncRoom` option (#4914)

This option is non-functional, and was deprecated in
https://github.com/matrix-org/matrix-js-sdk/pull/4913. Remove it altogether.

* Remove support for `onlyData != true` (#4939)

* Remove deprecated fields, methods, utilities (#4959)

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
Co-authored-by: Richard van der Hoff <richard@matrix.org>
2025-08-21 13:24:36 +00:00
Timo 3a33c658bb Expose the StatusChanged event through the RTCSession (#4974)
* Expose the StatusChanged event through the RTCSession

Signed-off-by: Timo K <toger5@hotmail.de>

* add membershipManagerStatus public get field

Signed-off-by: Timo K <toger5@hotmail.de>

* add probably left as a getter

Signed-off-by: Timo K <toger5@hotmail.de>

* add tests for coverage

Signed-off-by: Timo K <toger5@hotmail.de>

---------

Signed-off-by: Timo K <toger5@hotmail.de>
2025-08-20 16:48:56 +00:00
Timo 81e42b9531 Add probablyLeft event to the MatrixRTCSession (#4962)
* Add probablyLeft emission to the MatrixRTCSession

Signed-off-by: Timo K <toger5@hotmail.de>

* add docstring

Signed-off-by: Timo K <toger5@hotmail.de>

* Review: add additional test + fix pending promises issue.

Signed-off-by: Timo K <toger5@hotmail.de>

* review: `Pick` only a subset of membership manager events

Signed-off-by: Timo K <toger5@hotmail.de>

* reveiw: update probablyLeft logic to be more straight forward

Signed-off-by: Timo K <toger5@hotmail.de>

* fix test

Signed-off-by: Timo K <toger5@hotmail.de>

* make test not wait for 5s

Signed-off-by: Timo K <toger5@hotmail.de>

* review

Signed-off-by: Timo K <toger5@hotmail.de>

* fix linter (rebase)

Signed-off-by: Timo K <toger5@hotmail.de>

* fix import

Signed-off-by: Timo K <toger5@hotmail.de>

---------

Signed-off-by: Timo K <toger5@hotmail.de>
2025-08-20 12:05:07 +00:00
renovate[bot] 7f7ecd060d Update dependency lint-staged to v16.1.5 (#4969)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-19 15:06:35 +00:00
renovate[bot] 6126ee125a Update typedoc (#4970)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-08-19 14:27:08 +00:00
renovate[bot] 78f718ff82 Update typescript-eslint monorepo to v8.39.1 (#4971)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-19 13:27:09 +00:00
renovate[bot] c1c1be0c5d Update actions/download-artifact action to v5 (#4973)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-19 13:26:16 +00:00
renovate[bot] 1952eaa1ff Update actions/checkout action to v5 (#4972)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-19 13:26:08 +00:00
renovate[bot] 8851f8b07c Update dependency @types/node to v18.19.123 (#4968)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-19 13:24:51 +00:00
renovate[bot] 15eafe34b3 Update babel monorepo to v7.28.3 (#4966)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-19 13:24:25 +00:00
renovate[bot] f0d48236fa Update dependency @stylistic/eslint-plugin to v5.2.3 (#4967)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-19 13:24:23 +00:00
renovate[bot] dde9d48726 Update shogo82148/actions-upload-release-asset digest to 62365f2 (#4965)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-19 13:24:06 +00:00
34 changed files with 1165 additions and 494 deletions
@@ -22,7 +22,7 @@ runs:
- name: Upload tarball signature
if: ${{ inputs.upload-url }}
uses: shogo82148/actions-upload-release-asset@610b1987249a69a79de9565777e112fb38f22436 # v1
uses: shogo82148/actions-upload-release-asset@e6cd4579c50a5d0eb50ca05d0d597daafca8cebb # v1
with:
upload_url: ${{ inputs.upload-url }}
asset_path: ${{ env.VERSION }}.tar.gz.asc
@@ -29,13 +29,13 @@ runs:
- name: Upload asset signatures
if: inputs.gpg-fingerprint
uses: shogo82148/actions-upload-release-asset@610b1987249a69a79de9565777e112fb38f22436 # v1
uses: shogo82148/actions-upload-release-asset@e6cd4579c50a5d0eb50ca05d0d597daafca8cebb # v1
with:
upload_url: ${{ inputs.upload-url }}
asset_path: ${{ inputs.asset-path }}.asc
- name: Upload assets
uses: shogo82148/actions-upload-release-asset@610b1987249a69a79de9565777e112fb38f22436 # v1
uses: shogo82148/actions-upload-release-asset@e6cd4579c50a5d0eb50ca05d0d597daafca8cebb # v1
with:
upload_url: ${{ inputs.upload-url }}
asset_path: ${{ inputs.asset-path }}
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
deployments: write
steps:
- name: 📥 Download artifact
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
@@ -16,7 +16,7 @@ jobs:
contents: write
steps:
- name: 🧮 Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
ref: staging
fetch-depth: 0
@@ -37,7 +37,7 @@ jobs:
disable-autolabeler: true
- name: Get actions scripts
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
repository: matrix-org/matrix-js-sdk
persist-credentials: false
+2 -2
View File
@@ -18,14 +18,14 @@ jobs:
merge:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
# We will be pushing to this branch and want the CI to run after we do so we cannot use the GITHUB_TOKEN
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
fetch-depth: 0
- name: Get actions scripts
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
repository: matrix-org/matrix-js-sdk
persist-credentials: false
+2 -2
View File
@@ -73,7 +73,7 @@ jobs:
draft: true
latest: true
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
ref: staging
# We will be pushing to this branch and want the CI to run after we do so we cannot use the GITHUB_TOKEN
@@ -81,7 +81,7 @@ jobs:
fetch-depth: 0
- name: Get actions scripts
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
repository: matrix-org/matrix-js-sdk
persist-credentials: false
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
id: ${{ steps.npm-publish.outputs.id }}
steps:
- name: 🧮 Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
ref: staging
+3 -3
View File
@@ -44,7 +44,7 @@ jobs:
repo:
- element-hq/element-web
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
repository: ${{ matrix.repo }}
ref: staging
@@ -73,7 +73,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: 🧮 Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- name: 🔧 Yarn cache
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
@@ -88,7 +88,7 @@ jobs:
run: yarn gendoc
- name: Upload artifact
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4
with:
path: _docs
+3 -3
View File
@@ -36,14 +36,14 @@ jobs:
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
- name: "🧮 Checkout code"
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
repository: ${{ github.event.workflow_run.head_repository.full_name }}
ref: ${{ github.event.workflow_run.head_branch }} # checkout commit that triggered this workflow
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: 📥 Download artifact
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
if: ${{ !inputs.sharded }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -51,7 +51,7 @@ jobs:
name: coverage
path: coverage
- name: 📥 Download sharded artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
if: inputs.sharded
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
+7 -7
View File
@@ -14,7 +14,7 @@ jobs:
name: "Typescript Syntax Check"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
@@ -31,7 +31,7 @@ jobs:
name: "ESLint"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
@@ -48,7 +48,7 @@ jobs:
name: "Node.js example"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
@@ -83,7 +83,7 @@ jobs:
name: "Workflow Lint"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
@@ -100,7 +100,7 @@ jobs:
name: "JSDoc Checker"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
@@ -125,7 +125,7 @@ jobs:
name: "Analyse Dead Code"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
@@ -143,7 +143,7 @@ jobs:
if: github.event_name == 'merge_group'
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
repository: element-hq/element-web
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
node: ["lts/*", 22]
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- name: Setup Node
id: setupNode
+31
View File
@@ -1,3 +1,34 @@
Changes in [38.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v38.3.0) (2025-09-23)
==================================================================================================
## 🐛 Bug Fixes
* Remove knocked room when membership changes to join ([#4977](https://github.com/matrix-org/matrix-js-sdk/pull/4977)). Contributed by @svajunas-budrys.
Changes in [38.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v38.2.0) (2025-09-16)
==================================================================================================
Fix [CVE-2025-59160](https://www.cve.org/CVERecord?id=CVE-2025-59160) / [GHSA-mp7c-m3rh-r56v](https://github.com/matrix-org/matrix-js-sdk/security/advisories/GHSA-mp7c-m3rh-r56v)
Changes in [38.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v38.1.0) (2025-09-09)
==================================================================================================
## ✨ Features
* Remove custom `org.matrix.msc4075.rtc.notification.parent` relation type ([#4979](https://github.com/matrix-org/matrix-js-sdk/pull/4979)). Contributed by @toger5.
* MatrixRTC: Add RTC decline event ([#4978](https://github.com/matrix-org/matrix-js-sdk/pull/4978)). Contributed by @toger5.
* Make a MatrixRTCSession emit once the RTCNotification is sent ([#4976](https://github.com/matrix-org/matrix-js-sdk/pull/4976)). Contributed by @toger5.
* Use hydra semantics for unknown room versions ([#4957](https://github.com/matrix-org/matrix-js-sdk/pull/4957)). Contributed by @dbkr.
* Expose the StatusChanged event through the RTCSession ([#4974](https://github.com/matrix-org/matrix-js-sdk/pull/4974)). Contributed by @toger5.
* Add `probablyLeft` event to the MatrixRTCSession ([#4962](https://github.com/matrix-org/matrix-js-sdk/pull/4962)). Contributed by @toger5.
## 🐛 Bug Fixes
* Fix `m.topic` format ([#4984](https://github.com/matrix-org/matrix-js-sdk/pull/4984)). Contributed by @tulir.
* Fix stable-suffixed MSC4133 support ([#4983](https://github.com/matrix-org/matrix-js-sdk/pull/4983)). Contributed by @dbkr.
* Fix thread edit aggregation race condition ([#4980](https://github.com/matrix-org/matrix-js-sdk/pull/4980)). Contributed by @basnijholt.
Changes in [38.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v38.0.0) (2025-08-27)
==================================================================================================
## 🚨 BREAKING CHANGES
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "38.0.0",
"version": "38.3.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=22.0.0"
@@ -49,7 +49,7 @@
],
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/matrix-sdk-crypto-wasm": "^15.1.0",
"@matrix-org/matrix-sdk-crypto-wasm": "^15.2.0",
"another-json": "^0.2.0",
"bs58": "^6.0.0",
"content-type": "^1.0.4",
+70 -37
View File
@@ -183,39 +183,43 @@ describe("Topic content helpers", () => {
it("creates fully defined event content without html", () => {
expect(makeTopicContent("pizza")).toEqual({
topic: "pizza",
[M_TOPIC.name]: [
{
body: "pizza",
mimetype: "text/plain",
},
],
[M_TOPIC.name]: {
"m.text": [
{
body: "pizza",
mimetype: "text/plain",
},
],
},
});
});
it("creates fully defined event content with html", () => {
expect(makeTopicContent("pizza", "<b>pizza</b>")).toEqual({
topic: "pizza",
[M_TOPIC.name]: [
{
body: "<b>pizza</b>",
mimetype: "text/html",
},
{
body: "pizza",
mimetype: "text/plain",
},
],
[M_TOPIC.name]: {
"m.text": [
{
body: "<b>pizza</b>",
mimetype: "text/html",
},
{
body: "pizza",
mimetype: "text/plain",
},
],
},
});
});
it("creates an empty event when the topic is falsey", () => {
expect(makeTopicContent(undefined)).toEqual({
topic: undefined,
[M_TOPIC.name]: [],
[M_TOPIC.name]: { "m.text": [] },
});
expect(makeTopicContent(null)).toEqual({
topic: null,
[M_TOPIC.name]: [],
[M_TOPIC.name]: { "m.text": [] },
});
});
});
@@ -225,11 +229,13 @@ describe("Topic content helpers", () => {
expect(
parseTopicContent({
topic: "pizza",
[M_TOPIC.name]: [
{
body: "pizza",
},
],
[M_TOPIC.name]: {
"m.text": [
{
body: "pizza",
},
],
},
}),
).toEqual({
text: "pizza",
@@ -240,12 +246,14 @@ describe("Topic content helpers", () => {
expect(
parseTopicContent({
topic: "pizza",
[M_TOPIC.name]: [
{
body: "pizza",
mimetype: "text/plain",
},
],
[M_TOPIC.name]: {
"m.text": [
{
body: "pizza",
mimetype: "text/plain",
},
],
},
}),
).toEqual({
text: "pizza",
@@ -256,12 +264,14 @@ describe("Topic content helpers", () => {
expect(
parseTopicContent({
topic: "pizza",
[M_TOPIC.name]: [
{
body: "<b>pizza</b>",
mimetype: "text/html",
},
],
[M_TOPIC.name]: {
"m.text": [
{
body: "<b>pizza</b>",
mimetype: "text/html",
},
],
},
}),
).toEqual({
text: "pizza",
@@ -279,11 +289,34 @@ describe("Topic content helpers", () => {
});
});
it("uses legacy event content when new topic key is invalid", () => {
// TODO delete this test and re-enable the next one after support for the invalid form is removed
// https://github.com/matrix-org/matrix-js-sdk/pull/4984#pullrequestreview-3174251065
it("parses malformed event content with html topic", () => {
expect(
parseTopicContent({
"topic": "pizza",
"m.topic": {} as any,
"m.topic": [
{
body: "<b>pizza</b>",
mimetype: "text/html",
},
] as any,
}),
).toEqual({
text: "pizza",
html: "<b>pizza</b>",
});
});
it.skip("uses legacy event content when new topic key is invalid", () => {
expect(
parseTopicContent({
"topic": "pizza",
"m.topic": [
{
body: "<b>pizza</b>",
mimetype: "text/html",
},
] as any,
}),
).toEqual({
text: "pizza",
+23
View File
@@ -2998,6 +2998,8 @@ describe("MatrixClient", function () {
replacedByDynamicPredecessor2,
room2,
];
client.store.getRoom = (roomId: string) =>
client.store.getRooms().find((r) => r.roomId === roomId) || null;
room1.addLiveEvents(
[
roomCreateEvent(room1.roomId, replacedByCreate1.roomId),
@@ -3036,6 +3038,7 @@ describe("MatrixClient", function () {
replacedByDynamicPredecessor2,
};
}
it("Returns an empty list if there are no rooms", () => {
client.store = new StubStore();
client.store.getRooms = () => [];
@@ -3062,6 +3065,8 @@ describe("MatrixClient", function () {
const room2 = new Room("room2", client, "@daryl:alexandria.example.com");
client.store = new StubStore();
client.store.getRooms = () => [room1, replacedRoom1, replacedRoom2, room2];
client.store.getRoom = (roomId: string) =>
client.store.getRooms().find((r) => r.roomId === roomId) || null;
room1.addLiveEvents([roomCreateEvent(room1.roomId, replacedRoom1.roomId)], { addToState: true });
room2.addLiveEvents([roomCreateEvent(room2.roomId, replacedRoom2.roomId)], { addToState: true });
replacedRoom1.addLiveEvents([tombstoneEvent(room1.roomId, replacedRoom1.roomId)], { addToState: true });
@@ -3127,6 +3132,24 @@ describe("MatrixClient", function () {
expect(rooms).toContain(room1);
expect(rooms).toContain(room2);
});
it("should ignore room replacements which are not reciprocated by the predecessor", () => {
const room1 = new Room("room1", client, "@carol:alexandria.example.com");
// Room 2 claims to replace room 1 but room 1 does not agree
const room2 = new Room("replacedRoom1", client, "@daryl:alexandria.example.com");
client.store = new StubStore();
client.store.getRooms = () => [room1, room2];
client.store.getRoom = (roomId: string) =>
client.store.getRooms().find((r) => r.roomId === roomId) || null;
room2.addLiveEvents([roomCreateEvent(room2.roomId, room1.roomId)], { addToState: true });
// When we ask for the visible rooms
const rooms = client.getVisibleRooms();
expect(rooms).toContain(room1);
expect(rooms).toContain(room2);
});
});
describe("getRoomUpgradeHistory", () => {
+62 -3
View File
@@ -17,7 +17,7 @@ limitations under the License.
import { encodeBase64, EventType, MatrixClient, type MatrixError, type MatrixEvent, type Room } from "../../../src";
import { KnownMembership } from "../../../src/@types/membership";
import { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession";
import { type EncryptionKeysEventContent } from "../../../src/matrixrtc/types";
import { Status, type EncryptionKeysEventContent } from "../../../src/matrixrtc/types";
import { secureRandomString } from "../../../src/randomstring";
import { makeMockEvent, makeMockRoom, membershipTemplate, makeKey, type MembershipData, mockRoomState } from "./mocks";
import { RTCEncryptionManager } from "../../../src/matrixrtc/RTCEncryptionManager.ts";
@@ -309,8 +309,19 @@ describe("MatrixRTCSession", () => {
expect(sess!.isJoined()).toEqual(true);
});
it("sends a notification when starting a call", async () => {
it("sends a notification when starting a call and emit DidSendCallNotification", async () => {
// Simulate a join, including the update to the room state
// Ensure sendEvent returns event IDs so the DidSendCallNotification payload includes them
sendEventMock
.mockResolvedValueOnce({ event_id: "legacy-evt" })
.mockResolvedValueOnce({ event_id: "new-evt" });
const didSendEventFn = jest.fn();
sess!.once(MatrixRTCSessionEvent.DidSendCallNotification, didSendEventFn);
// Create an additional listener to create a promise that resolves after the emission.
const didSendNotification = new Promise((resolve) => {
sess!.once(MatrixRTCSessionEvent.DidSendCallNotification, resolve);
});
sess!.joinRoomSession([mockFocus], mockFocus, { notificationType: "ring" });
await Promise.race([sentStateEvent, new Promise((resolve) => setTimeout(resolve, 5000))]);
mockRoomState(mockRoom, [{ ...membershipTemplate, user_id: client.getUserId()! }]);
@@ -322,7 +333,7 @@ describe("MatrixRTCSession", () => {
"notification_type": "ring",
"m.relates_to": {
event_id: ownMembershipId,
rel_type: "org.matrix.msc4075.rtc.notification.parent",
rel_type: "m.reference",
},
"lifetime": 30000,
"sender_ts": expect.any(Number),
@@ -335,6 +346,28 @@ describe("MatrixRTCSession", () => {
"notify_type": "ring",
"call_id": "",
});
await didSendNotification;
// And ensure we emitted the DidSendCallNotification event with both payloads
expect(didSendEventFn).toHaveBeenCalledWith(
{
"event_id": "new-evt",
"lifetime": 30000,
"m.mentions": { room: true, user_ids: [] },
"m.relates_to": {
event_id: expect.any(String),
rel_type: "m.reference",
},
"notification_type": "ring",
"sender_ts": expect.any(Number),
},
{
"application": "m.call",
"call_id": "",
"event_id": "legacy-evt",
"m.mentions": { room: true, user_ids: [] },
"notify_type": "ring",
},
);
});
it("doesn't send a notification when joining an existing call", async () => {
@@ -1226,5 +1259,31 @@ describe("MatrixRTCSession", () => {
}
});
});
describe("read status", () => {
it("returns the correct probablyLeft status", () => {
const mockRoom = makeMockRoom([membershipTemplate]);
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
expect(sess!.probablyLeft).toBe(undefined);
sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
expect(sess!.probablyLeft).toBe(false);
// Simulate the membership manager believing the user has left
const accessPrivateFieldsSession = sess as unknown as {
membershipManager: { state: { probablyLeft: boolean } };
};
accessPrivateFieldsSession.membershipManager.state.probablyLeft = true;
expect(sess!.probablyLeft).toBe(true);
});
it("returns membershipStatus once joinRoomSession got called", () => {
const mockRoom = makeMockRoom([membershipTemplate]);
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
expect(sess!.membershipStatus).toBe(undefined);
sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
expect(sess!.membershipStatus).toBe(Status.Connecting);
});
});
});
});
+66 -1
View File
@@ -16,7 +16,14 @@ limitations under the License.
import { type MockedFunction, type Mock } from "jest-mock";
import { EventType, HTTPError, MatrixError, UnsupportedDelayedEventsEndpointError, type Room } from "../../../src";
import {
type EmptyObject,
EventType,
HTTPError,
MatrixError,
UnsupportedDelayedEventsEndpointError,
type Room,
} from "../../../src";
import {
MembershipManagerEvent,
Status,
@@ -611,6 +618,7 @@ describe("MembershipManager", () => {
await testExpires(10_000, 1_000);
});
});
describe("status updates", () => {
it("starts 'Disconnected'", () => {
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
@@ -836,6 +844,63 @@ describe("MembershipManager", () => {
expect(client.sendStateEvent).toHaveBeenCalled();
});
});
describe("probablyLeft", () => {
it("emits probablyLeft when the membership manager could not hear back from the server for the duration of the delayed event", async () => {
const manager = new MembershipManager(
{ delayedLeaveEventDelayMs: 10000 },
room,
client,
() => undefined,
callSession,
);
const { promise: stuckPromise, reject: rejectStuckPromise } = Promise.withResolvers<EmptyObject>();
const probablyLeftEmit = jest.fn();
manager.on(MembershipManagerEvent.ProbablyLeft, probablyLeftEmit);
manager.join([focus], focusActive);
try {
// Let the scheduler run one iteration so that we can send the join state event
await waitForMockCall(client._unstable_updateDelayedEvent);
// We never resolve the delayed event so that we can test the probablyLeft event.
// This simulates the case where the server does not respond to the delayed event.
client._unstable_updateDelayedEvent = jest.fn(() => stuckPromise);
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
expect(manager.status).toBe(Status.Connected);
expect(probablyLeftEmit).not.toHaveBeenCalledWith(true);
// We expect the probablyLeft event to be emitted after the `delayedLeaveEventDelayMs` = 10000.
// We also track the calls to updated the delayed event that all will never resolve to simulate the server not responding.
// The numbers are a bit arbitrary since we use the local timeout that does not perfectly match the 5s check interval in this test.
await jest.advanceTimersByTimeAsync(5000);
// No emission after 5s
expect(probablyLeftEmit).not.toHaveBeenCalledWith(true);
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(1);
await jest.advanceTimersByTimeAsync(4999);
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(3);
expect(probablyLeftEmit).not.toHaveBeenCalledWith(true);
// Reset mocks before we setup the next delayed event restart by advancing the timers 1 more ms.
(client._unstable_updateDelayedEvent as Mock<any>).mockResolvedValue({});
// Emit after 10s
await jest.advanceTimersByTimeAsync(1);
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(4);
expect(probablyLeftEmit).toHaveBeenCalledWith(true);
// Mock a sync which does not include our own membership
await manager.onRTCSessionMemberUpdate([]);
// Wait for the current ongoing delayed event sending to finish
await jest.advanceTimersByTimeAsync(1);
// We should send a new state event and an associated delayed leave event.
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(2);
expect(client.sendStateEvent).toHaveBeenCalledTimes(2);
// At the same time we expect the probablyLeft event to be emitted with false so we are back operational.
expect(probablyLeftEmit).toHaveBeenCalledWith(false);
} finally {
rejectStuckPromise();
}
});
});
});
it("Should prefix log with MembershipManager used", () => {
+210 -1
View File
@@ -22,7 +22,7 @@ import { FeatureSupport, Thread, THREAD_RELATION_TYPE, ThreadEvent } from "../..
import { makeThreadEvent, mkThread, populateThread } from "../../test-utils/thread";
import { TestClient } from "../../TestClient";
import { emitPromise, mkEdit, mkMessage, mkReaction, mock } from "../../test-utils/test-utils";
import { Direction, EventStatus, EventType, MatrixEvent } from "../../../src";
import { Direction, EventStatus, EventType, MatrixEvent, RelationType } from "../../../src";
import { ReceiptType } from "../../../src/@types/read_receipts";
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../test-utils/client";
import { ReEmitter } from "../../../src/ReEmitter";
@@ -773,6 +773,215 @@ describe("Thread", () => {
expect(lastEvent).toBe(message1);
expect(firstEvent).toBe(message2);
});
it("Edit events are properly aggregated in threads with server-side support", async () => {
// This test reproduces the race condition bug from https://github.com/element-hq/element-web/issues/30617
// The bug occurs when edits arrive while the thread is not initialized,
// causing aggregation to fail because the target event isn't in the timeline yet
// Given a thread exists with server-side support enabled
const myUserId = "@alice:example.org";
const testClient = new TestClient(myUserId, "DEVICE", "ACCESS_TOKEN", undefined, {
timelineSupport: false,
});
const client = testClient.client;
client.supportsThreads = jest.fn().mockReturnValue(true);
const room = new Room("!room:z", client, myUserId, {
pendingEventOrdering: PendingEventOrdering.Detached,
});
jest.spyOn(client, "getRoom").mockReturnValue(room);
// Create a root event
const rootEvent = mkMessage({
room: room.roomId,
user: myUserId,
msg: "Root message",
event: true,
});
// Create thread manually - starts with initialEventsFetched = false
const thread = new Thread(rootEvent.getId()!, rootEvent, {
room: room,
client: client,
pendingEventOrdering: PendingEventOrdering.Detached,
});
// The thread is NOT initialized - this is the key to reproducing the bug!
expect(thread.initialEventsFetched).toBe(false);
// Create a message that will be edited
const originalMessage = mkMessage({
room: room.roomId,
user: myUserId,
msg: "Original message in thread",
relatesTo: {
"rel_type": THREAD_RELATION_TYPE.name,
"event_id": thread.id,
"m.in_reply_to": {
event_id: thread.id,
},
},
event: true,
});
// Create edit events BEFORE the original message is in the timeline
const edit1 = mkEdit(originalMessage, client, myUserId, room.roomId, "Edit 1");
const edit2 = mkEdit(originalMessage, client, myUserId, room.roomId, "Edit 2");
const edit3 = mkEdit(originalMessage, client, myUserId, room.roomId, "Final edit");
// CRITICAL: Add edits while thread is NOT initialized
// They will be queued in replayEvents and aggregation will be attempted but fail
await thread.addEvent(edit1, false);
// Check the aggregation state after adding first edit
// With our fix: edits should NOT be aggregated yet (thread not initialized)
// Without fix: edits would be aggregated but fail to link to target
const relationsAfterFirstEdit = thread.timelineSet.relations?.getChildEventsForEvent(
originalMessage.getId()!,
RelationType.Replace,
EventType.RoomMessage,
);
// With the fix, no aggregation happens yet (which is correct)
// Without the fix, aggregation would happen but fail silently
expect(relationsAfterFirstEdit).toBeUndefined();
// Add remaining edits
await thread.addEvent(edit2, false);
await thread.addEvent(edit3, false);
// Check that edits went to replayEvents
expect(thread.replayEvents).toHaveLength(3);
// Now initialize the thread and add the original message
thread.initialEventsFetched = true;
// Clear replayEvents and add the original message
const replayEvents = [...(thread.replayEvents || [])];
thread.replayEvents = [];
// Add original message first
await thread.addEvent(originalMessage, false);
// At this point, the original message should NOT have the edits aggregated yet
// because they were attempted when the target wasn't in timeline
const replacingEventBeforeReplay = originalMessage.replacingEvent();
// With the fix, edits should not be aggregated yet (pre-init)
expect(replacingEventBeforeReplay).toBeNull();
// Then replay the edits
for (const event of replayEvents) {
await thread.addEvent(event, false);
}
// After replay, check aggregation
const replacingEvent = originalMessage.replacingEvent();
// This should now work because edits were re-aggregated when replayed
expect(replacingEvent).toBe(edit3);
// The content should also be updated
expect(originalMessage.getContent().body).toBe("Final edit");
// Relations for replaces should now exist and include all edits in order
const replaceRels = thread.timelineSet.relations!.getChildEventsForEvent(
originalMessage.getId()!,
RelationType.Replace,
EventType.RoomMessage,
)!;
const replaceIds = replaceRels.getRelations().map((e) => e.getId());
expect(replaceIds).toHaveLength(3);
expect(replaceIds[0]).toBe(edit1.getId());
expect(replaceIds[1]).toBe(edit2.getId());
expect(replaceIds[2]).toBe(edit3.getId());
});
it("Reactions aggregate pre-init and remain idempotent on replay", async () => {
const myUserId = "@alice:example.org";
const testClient = new TestClient(myUserId, "DEVICE", "ACCESS_TOKEN", undefined, {
timelineSupport: false,
});
const client = testClient.client;
client.supportsThreads = jest.fn().mockReturnValue(true);
// Force server-side support so threads start uninitialised
const prevSupport = Thread.hasServerSideSupport;
Thread.setServerSideSupport(FeatureSupport.Stable);
try {
const room = new Room("!room:z", client, myUserId, {
pendingEventOrdering: PendingEventOrdering.Detached,
});
jest.spyOn(client, "getRoom").mockReturnValue(room);
// Create a root event and thread
const rootEvent = mkMessage({ room: room.roomId, user: myUserId, msg: "Root", event: true });
const thread = new Thread(rootEvent.getId()!, rootEvent, {
room,
client,
pendingEventOrdering: PendingEventOrdering.Detached,
});
expect(thread.initialEventsFetched).toBe(false);
// A message inside the thread to react to
const originalMessage = mkMessage({
room: room.roomId,
user: myUserId,
msg: "Thread message",
relatesTo: {
"rel_type": THREAD_RELATION_TYPE.name,
"event_id": thread.id,
"m.in_reply_to": { event_id: thread.id },
},
event: true,
});
// Create 2 reactions before the message is in the timeline (pre-init)
const reaction1 = mkReaction(originalMessage, client, myUserId, room.roomId);
const reaction2 = mkReaction(originalMessage, client, myUserId, room.roomId);
// Add reactions while thread is NOT initialised
thread.addEvent(reaction1, false);
thread.addEvent(reaction2, false);
// Relations should already include the reactions pre-init
const relsBefore = thread.timelineSet.relations!.getChildEventsForEvent(
originalMessage.getId()!,
RelationType.Annotation,
EventType.Reaction,
)!;
expect(relsBefore).toBeTruthy();
const beforeIds = new Set(relsBefore.getRelations().map((e) => e.getId()));
expect(beforeIds.size).toBe(2);
// Now initialise and replay
// Ensure reactions are queued for replay as well
expect(thread.replayEvents).toHaveLength(2);
const replay = [...(thread.replayEvents || [])];
thread.replayEvents = [];
thread.initialEventsFetched = true;
// Add the original message first so it becomes findable
thread.addEvent(originalMessage, false);
// Replay reactions
for (const ev of replay) thread.addEvent(ev, false);
// Ensure no duplicates after replay (idempotent aggregation)
const relsAfter = thread.timelineSet.relations!.getChildEventsForEvent(
originalMessage.getId()!,
RelationType.Annotation,
EventType.Reaction,
)!;
const afterIds = new Set(relsAfter.getRelations().map((e) => e.getId()));
expect(afterIds.size).toBe(2);
expect(afterIds).toEqual(beforeIds);
} finally {
// restore
Thread.setServerSideSupport(prevSupport);
}
});
});
});
});
@@ -2353,6 +2353,7 @@ describe("RustCrypto", () => {
beforeEach(async () => {
mockOlmMachine = {
queryKeysForUsers: jest.fn().mockReturnValue({}),
getReceivedRoomKeyBundleData: jest.fn(),
receiveRoomKeyBundle: jest.fn(),
} as unknown as Mocked<OlmMachine>;
@@ -2377,6 +2378,7 @@ describe("RustCrypto", () => {
it("does nothing if there is no key bundle", async () => {
mockOlmMachine.getReceivedRoomKeyBundleData.mockResolvedValue(undefined);
await rustCrypto.maybeAcceptKeyBundle("!room_id", "@bob:example.org");
expect(mockOlmMachine.queryKeysForUsers).toHaveBeenCalledTimes(1);
expect(mockOlmMachine.getReceivedRoomKeyBundleData).toHaveBeenCalledTimes(1);
expect(mockOlmMachine.getReceivedRoomKeyBundleData.mock.calls[0][0].toString()).toEqual("!room_id");
expect(mockOlmMachine.getReceivedRoomKeyBundleData.mock.calls[0][1].toString()).toEqual("@bob:example.org");
@@ -156,6 +156,8 @@ describe("VerificationRequest", () => {
);
const verificationRequestContent = JSON.parse(await bobUserIdentity.verificationRequestContent(methods));
todoFixupVerificationRequestContent(verificationRequestContent);
await bobOlmMachine.receiveVerificationEvent(
JSON.stringify({
type: "m.room.message",
@@ -289,6 +291,8 @@ describe("VerificationRequest", () => {
);
const verificationRequestContent = JSON.parse(await bobUserIdentity.verificationRequestContent(methods));
todoFixupVerificationRequestContent(verificationRequestContent);
await bobOlmMachine.receiveVerificationEvent(
JSON.stringify({
type: "m.room.message",
@@ -405,6 +409,8 @@ describe("VerificationRequest", () => {
);
const verificationRequestContent = JSON.parse(await bobUserIdentity.verificationRequestContent(methods));
todoFixupVerificationRequestContent(verificationRequestContent);
await bobOlmMachine.receiveVerificationEvent(
JSON.stringify({
type: "m.room.message",
@@ -460,6 +466,15 @@ describe("VerificationRequest", () => {
});
});
/**
* Needed until https://github.com/matrix-org/matrix-rust-sdk/issues/5643 is fixed.
*
* Modify the content of the supplied content to include `msgtype: m.key.verification.request`.
*/
function todoFixupVerificationRequestContent(content: any) {
content.msgtype = "m.key.verification.request";
}
describe("isVerificationEvent", () => {
it.each([
[EventType.KeyVerificationCancel],
+43
View File
@@ -571,6 +571,49 @@ describe("SyncAccumulator", function () {
expect(sa.getJSON().roomsData.knock["!knock:bar"]).toBeUndefined();
});
it("should delete knock state when room transitions from knock to join", () => {
const initKnockState = makeKnockState();
sa.accumulate(
syncSkeleton(
{},
{},
{},
{
knock_state: initKnockState,
},
),
);
expect(sa.getJSON().roomsData.knock["!knock:bar"].knock_state).toBe(initKnockState);
// Room transitions from knock to join (e.g., after approval and joining)
const joinState = {
account_data: { events: [] },
ephemeral: { events: [] },
unread_notifications: {},
state: {
events: [member("bob", KnownMembership.Join)],
},
};
const syncResponse = {
next_batch: "abc",
rooms: {
join: {
"!knock:bar": joinState,
},
invite: {},
leave: {},
},
} as unknown as ISyncResponse;
sa.accumulate(syncResponse);
expect(sa.getJSON().roomsData.knock["!knock:bar"]).toBeUndefined();
expect(sa.getJSON().roomsData.join["!knock:bar"].state?.events[0]?.content.membership).toEqual(
KnownMembership.Join,
);
});
it("should accumulate read receipts", () => {
const receipt1 = {
type: "m.receipt",
+3 -1
View File
@@ -53,6 +53,7 @@ import {
} from "../webrtc/callEventTypes.ts";
import {
type IRTCNotificationContent,
type IRTCDeclineContent,
type EncryptionKeysEventContent,
type ICallNotifyContent,
} from "../matrixrtc/types.ts";
@@ -152,6 +153,7 @@ export enum EventType {
// MatrixRTC events
CallNotify = "org.matrix.msc4075.call.notify",
RTCNotification = "org.matrix.msc4075.rtc.notification",
RTCDecline = "org.matrix.msc4310.rtc.decline",
}
export enum RelationType {
@@ -163,7 +165,6 @@ export enum RelationType {
// moreover, our tests currently use the unstable prefix. Use THREAD_RELATION_TYPE.name.
// Once we support *only* the stable prefix, THREAD_RELATION_TYPE can die and we can switch to this.
Thread = "m.thread",
unstable_RTCNotificationParent = "org.matrix.msc4075.rtc.notification.parent",
}
export enum MsgType {
@@ -332,6 +333,7 @@ export interface TimelineEvents {
[EventType.CallEncryptionKeysPrefix]: EncryptionKeysEventContent;
[EventType.CallNotify]: ICallNotifyContent;
[EventType.RTCNotification]: IRTCNotificationContent;
[EventType.RTCDecline]: IRTCDeclineContent;
[M_BEACON.name]: MBeaconEventContent;
[M_POLL_START.name]: PollStartEventContent;
[M_POLL_END.name]: PollEndEventContent;
+9 -2
View File
@@ -47,12 +47,19 @@ export const M_TOPIC = new NamespacedValue("m.topic");
/**
* The event content for an m.topic event (in content)
*/
export type MTopicContent = IMessageRendering[];
export type MTopicContent = { "m.text": IMessageRendering[] };
/**
* A previous incorrect form of m.topic used by matrix-js-sdk
* TODO remove this after a few releases
* https://github.com/matrix-org/matrix-js-sdk/pull/4984#pullrequestreview-3174251065
*/
export type MalformedMTopicEvent = { "m.topic": IMessageRendering[] };
/**
* The event definition for an m.topic event (in content)
*/
export type MTopicEvent = { "m.topic": MTopicContent };
export type MTopicEvent = { "m.topic": MTopicContent } | MalformedMTopicEvent;
/**
* The event content for an m.room.topic event
+18 -15
View File
@@ -542,6 +542,7 @@ export const UNSTABLE_MSC2666_QUERY_MUTUAL_ROOMS = "uk.half-shot.msc2666.query_m
export const UNSTABLE_MSC4140_DELAYED_EVENTS = "org.matrix.msc4140";
export const UNSTABLE_MSC4133_EXTENDED_PROFILES = "uk.tcpip.msc4133";
export const STABLE_MSC4133_EXTENDED_PROFILES = "uk.tcpip.msc4133.stable";
enum CrossSigningKeyType {
MasterKey = "master_key",
@@ -2140,21 +2141,14 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
public getVisibleRooms(msc3946ProcessDynamicPredecessor = false): Room[] {
const allRooms = this.store.getRooms();
const replacedRooms = new Set();
for (const r of allRooms) {
const predecessor = r.findPredecessor(msc3946ProcessDynamicPredecessor)?.roomId;
if (predecessor) {
replacedRooms.add(predecessor);
const visibleRooms = new Set(allRooms);
for (const room of visibleRooms) {
const predecessors = this.findPredecessorRooms(room, true, msc3946ProcessDynamicPredecessor);
for (const predecessor of predecessors) {
visibleRooms.delete(predecessor);
}
}
return allRooms.filter((r) => {
const tombstone = r.currentState.getStateEvents(EventType.RoomTombstone, "");
if (tombstone && replacedRooms.has(r.roomId)) {
return false;
}
return true;
});
return Array.from(visibleRooms);
}
/**
@@ -3589,6 +3583,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return await this.setRoomReadMarkersHttpRequest(roomId, rmEventId, rrEventId, rpEventId);
}
public sendRtcDecline(roomId: string, notificationEventId: string): Promise<ISendEventResponse> {
return this.sendEvent(roomId, EventType.RTCDecline, {
"m.relates_to": { event_id: notificationEventId, rel_type: RelationType.Reference },
});
}
/**
* Get a preview of the given URL as of (roughly) the given point in time,
* described as an object with OpenGraph keys and associated values.
@@ -3845,7 +3845,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
roomId: string,
includeFuture = true,
): Promise<{ [roomId: string]: Error | MatrixError | null }> {
const upgradeHistory = this.getRoomUpgradeHistory(roomId);
const upgradeHistory = this.getRoomUpgradeHistory(roomId, true);
let eligibleToLeave = upgradeHistory;
if (!includeFuture) {
@@ -6897,7 +6897,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns `true` if supported, otherwise `false`
*/
public async doesServerSupportExtendedProfiles(): Promise<boolean> {
return this.doesServerSupportUnstableFeature(UNSTABLE_MSC4133_EXTENDED_PROFILES);
return (
(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4133_EXTENDED_PROFILES)) ||
(await this.doesServerSupportUnstableFeature(STABLE_MSC4133_EXTENDED_PROFILES))
);
}
/**
+6 -2
View File
@@ -197,7 +197,7 @@ export const makeTopicContent: MakeTopicContent = (topic, htmlTopic) => {
if (isProvided(topic)) {
renderings.push({ body: topic, mimetype: "text/plain" });
}
return { topic, [M_TOPIC.name]: renderings };
return { topic, [M_TOPIC.name]: { "m.text": renderings } };
};
export type TopicState = {
@@ -206,7 +206,11 @@ export type TopicState = {
};
export const parseTopicContent = (content: MRoomTopicEventContent): TopicState => {
const mtopic = M_TOPIC.findIn<MTopicContent>(content);
const mtopicParent = M_TOPIC.findIn<MTopicContent>(content);
const mtopic = Array.isArray(mtopicParent) ? mtopicParent : mtopicParent?.["m.text"];
// TODO remove support for the old malformed m.topic arrays after a few releases (only allow array in m.text)
// https://github.com/matrix-org/matrix-js-sdk/pull/4984#pullrequestreview-3174251065
//const mtopic = M_TOPIC.findIn<MTopicContent>(content)?.["m.text"];
if (!Array.isArray(mtopic)) {
return { text: content.topic ?? undefined };
}
+16 -5
View File
@@ -17,13 +17,21 @@ limitations under the License.
import type { CallMembership } from "./CallMembership.ts";
import type { Focus } from "./focus.ts";
import type { Status } from "./types.ts";
import { type TypedEventEmitter } from "../models/typed-event-emitter.ts";
export enum MembershipManagerEvent {
StatusChanged = "StatusChanged",
/**
* Emitted when the membership manager has not heard back from the server for the duration
* of the delayed event and hence failed to restart the delayed event.
* This means that the user is probably not joined anymore and the leave event was distributed to other session members.
*/
ProbablyLeft = "ProbablyLeft",
}
export type MembershipManagerEventHandlerMap = {
[MembershipManagerEvent.StatusChanged]: (prefStatus: Status, newStatus: Status) => void;
[MembershipManagerEvent.ProbablyLeft]: (probablyLeft: boolean) => void;
};
/**
@@ -33,7 +41,8 @@ export type MembershipManagerEventHandlerMap = {
*
* @internal
*/
export interface IMembershipManager {
export interface IMembershipManager
extends TypedEventEmitter<MembershipManagerEvent, MembershipManagerEventHandlerMap> {
/**
* If we are trying to join, or have successfully joined the session.
* It does not reflect if the room state is already configured to represent us being joined.
@@ -62,6 +71,12 @@ export interface IMembershipManager {
*/
get ownMembership(): CallMembership | undefined;
/**
* If the membership manager has reason to believe that the hs sent a leave event
* and as a consequence the current user is perceived as left for other session participants.
*/
get probablyLeft(): boolean;
/**
* Start sending all necessary events to make this user participate in the RTC session.
* @param fociPreferred the list of preferred foci to use in the joined RTC membership event.
@@ -85,8 +100,4 @@ export interface IMembershipManager {
* @returns the used active focus in the currently joined session or undefined if not joined.
*/
getActiveFocus(): Focus | undefined;
// TypedEventEmitter methods:
on(event: MembershipManagerEvent.StatusChanged, listener: (oldStatus: Status, newStatus: Status) => void): this;
off(event: MembershipManagerEvent.StatusChanged, listener: (oldStatus: Status, newStatus: Status) => void): this;
}
+68 -18
View File
@@ -20,16 +20,27 @@ import { EventTimeline } from "../models/event-timeline.ts";
import { type Room } from "../models/room.ts";
import { type MatrixClient } from "../client.ts";
import { EventType, RelationType } from "../@types/event.ts";
import { KnownMembership } from "../@types/membership.ts";
import { type ISendEventResponse } from "../@types/requests.ts";
import { CallMembership } from "./CallMembership.ts";
import { RoomStateEvent } from "../models/room-state.ts";
import { type Focus } from "./focus.ts";
import { KnownMembership } from "../@types/membership.ts";
import { MembershipManager } from "./MembershipManager.ts";
import { EncryptionManager, type IEncryptionManager } from "./EncryptionManager.ts";
import { deepCompare, logDurationSync } from "../utils.ts";
import { type Statistics, type RTCNotificationType } from "./types.ts";
import {
type Statistics,
type RTCNotificationType,
type Status,
type IRTCNotificationContent,
type ICallNotifyContent,
} from "./types.ts";
import { RoomKeyTransport } from "./RoomKeyTransport.ts";
import type { IMembershipManager } from "./IMembershipManager.ts";
import {
MembershipManagerEvent,
type MembershipManagerEventHandlerMap,
type IMembershipManager,
} from "./IMembershipManager.ts";
import { RTCEncryptionManager } from "./RTCEncryptionManager.ts";
import {
RoomAndToDeviceEvents,
@@ -39,6 +50,9 @@ import {
import { TypedReEmitter } from "../ReEmitter.ts";
import { ToDeviceKeyTransport } from "./ToDeviceKeyTransport.ts";
/**
* Events emitted by MatrixRTCSession
*/
export enum MatrixRTCSessionEvent {
// A member joined, left, or updated a property of their membership.
MembershipsChanged = "memberships_changed",
@@ -50,6 +64,8 @@ export enum MatrixRTCSessionEvent {
EncryptionKeyChanged = "encryption_key_changed",
/** The membership manager had to shut down caused by an unrecoverable error */
MembershipManagerError = "membership_manager_error",
/** The RTCSession did send a call notification caused by joining the call as the first member */
DidSendCallNotification = "did_send_call_notification",
}
export type MatrixRTCSessionEventHandlerMap = {
@@ -64,6 +80,10 @@ export type MatrixRTCSessionEventHandlerMap = {
participantId: string,
) => void;
[MatrixRTCSessionEvent.MembershipManagerError]: (error: unknown) => void;
[MatrixRTCSessionEvent.DidSendCallNotification]: (
notificationContentNew: { event_id: string } & IRTCNotificationContent,
notificationContentLegacy: { event_id: string } & ICallNotifyContent,
) => void;
};
export interface SessionConfig {
@@ -209,8 +229,8 @@ export type JoinSessionConfig = SessionConfig & MembershipConfig & EncryptionCon
* This class doesn't deal with media at all, just membership & properties of a session.
*/
export class MatrixRTCSession extends TypedEventEmitter<
MatrixRTCSessionEvent | RoomAndToDeviceEvents,
MatrixRTCSessionEventHandlerMap & RoomAndToDeviceEventsHandlerMap
MatrixRTCSessionEvent | RoomAndToDeviceEvents | MembershipManagerEvent,
MatrixRTCSessionEventHandlerMap & RoomAndToDeviceEventsHandlerMap & MembershipManagerEventHandlerMap
> {
private membershipManager?: IMembershipManager;
private encryptionManager?: IEncryptionManager;
@@ -240,6 +260,14 @@ export class MatrixRTCSession extends TypedEventEmitter<
},
};
public get membershipStatus(): Status | undefined {
return this.membershipManager?.status;
}
public get probablyLeft(): boolean | undefined {
return this.membershipManager?.probablyLeft;
}
/**
* The callId (sessionId) of the call.
*
@@ -445,8 +473,8 @@ export class MatrixRTCSession extends TypedEventEmitter<
roomState?.off(RoomStateEvent.Members, this.onRoomMemberUpdate);
}
private reEmitter = new TypedReEmitter<
MatrixRTCSessionEvent | RoomAndToDeviceEvents,
MatrixRTCSessionEventHandlerMap & RoomAndToDeviceEventsHandlerMap
MatrixRTCSessionEvent | RoomAndToDeviceEvents | MembershipManagerEvent,
MatrixRTCSessionEventHandlerMap & RoomAndToDeviceEventsHandlerMap & MembershipManagerEventHandlerMap
>(this);
/**
@@ -479,6 +507,10 @@ export class MatrixRTCSession extends TypedEventEmitter<
this.logger,
);
this.reEmitter.reEmit(this.membershipManager!, [
MembershipManagerEvent.ProbablyLeft,
MembershipManagerEvent.StatusChanged,
]);
// Create Encryption manager
let transport;
if (joinConfig?.useExperimentalToDeviceTransport) {
@@ -636,29 +668,47 @@ export class MatrixRTCSession extends TypedEventEmitter<
* Sends a notification corresponding to the configured notify type.
*/
private sendCallNotify(parentEventId: string, notificationType: RTCNotificationType): void {
// Send legacy event:
this.client
.sendEvent(this.roomSubset.roomId, EventType.CallNotify, {
const sendLegacyNotificationEvent = async (): Promise<{
response: ISendEventResponse;
content: ICallNotifyContent;
}> => {
const content: ICallNotifyContent = {
"application": "m.call",
"m.mentions": { user_ids: [], room: true },
"notify_type": notificationType === "notification" ? "notify" : notificationType,
"call_id": this.callId!,
})
.catch((e) => this.logger.error("Failed to send call notification", e));
// Send new event:
this.client
.sendEvent(this.roomSubset.roomId, EventType.RTCNotification, {
};
const response = await this.client.sendEvent(this.roomSubset.roomId, EventType.CallNotify, content);
return { response, content };
};
const sendNewNotificationEvent = async (): Promise<{
response: ISendEventResponse;
content: IRTCNotificationContent;
}> => {
const content: IRTCNotificationContent = {
"m.mentions": { user_ids: [], room: true },
"notification_type": notificationType,
"m.relates_to": {
event_id: parentEventId,
rel_type: RelationType.unstable_RTCNotificationParent,
rel_type: RelationType.Reference,
},
"sender_ts": Date.now(),
"lifetime": 30_000, // 30 seconds
};
const response = await this.client.sendEvent(this.roomSubset.roomId, EventType.RTCNotification, content);
return { response, content };
};
void Promise.all([sendLegacyNotificationEvent(), sendNewNotificationEvent()])
.then(([legacy, newNotification]) => {
// Join event_id and origin event content
const legacyResult = { ...legacy.response, ...legacy.content };
const newResult = { ...newNotification.response, ...newNotification.content };
this.emit(MatrixRTCSessionEvent.DidSendCallNotification, newResult, legacyResult);
})
.catch((e) => this.logger.error("Failed to send call notification", e));
.catch(([errorLegacy, errorNew]) =>
this.logger.error("Failed to send call notification", errorLegacy, errorNew),
);
}
/**
+49 -4
View File
@@ -129,6 +129,12 @@ export interface MembershipManagerState {
rateLimitRetries: Map<MembershipActionType, number>;
/** Retry counter for other errors */
networkErrorRetries: Map<MembershipActionType, number>;
/** The time at which we expect the server to send the delayed leave event. */
expectedServerDelayLeaveTs?: number;
/** This is used to track if the client expects the scheduled delayed leave event to have
* been sent because restarting failed during the available time.
* Once we resend the delayed event or successfully restarted it will get unset. */
probablyLeft: boolean;
}
/**
@@ -343,6 +349,7 @@ export class MembershipManager
rateLimitRetries: new Map(),
networkErrorRetries: new Map(),
expireUpdateIterations: 1,
probablyLeft: false,
};
}
// Membership Event static parameters:
@@ -453,6 +460,8 @@ export class MembershipManager
this.stateKey,
)
.then((response) => {
this.state.expectedServerDelayLeaveTs = Date.now() + this.delayedLeaveEventDelayMs;
this.setAndEmitProbablyLeft(false);
// On success we reset retries and set delayId.
this.resetRateLimitCounter(MembershipActionType.SendDelayedEvent);
this.state.delayId = response.delay_id;
@@ -532,27 +541,58 @@ export class MembershipManager
});
}
private setAndEmitProbablyLeft(probablyLeft: boolean): void {
if (this.state.probablyLeft === probablyLeft) {
return;
}
this.state.probablyLeft = probablyLeft;
this.emit(MembershipManagerEvent.ProbablyLeft, this.state.probablyLeft);
}
private async restartDelayedEvent(delayId: string): Promise<ActionUpdate> {
// Compute the duration until we expect the server to send the delayed leave event.
const durationUntilServerDelayedLeave = this.state.expectedServerDelayLeaveTs
? this.state.expectedServerDelayLeaveTs - Date.now()
: undefined;
const abortPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new AbortError("Restart delayed event timed out before the HS responded"));
}, this.delayedLeaveEventRestartLocalTimeoutMs);
setTimeout(
() => {
reject(new AbortError("Restart delayed event timed out before the HS responded"));
},
// We abort immediately at the time where we expect the server to send the delayed leave event.
// At this point we want the catch block to run and set the `probablyLeft` state.
//
// While we are already in probablyLeft state, we use the unaltered delayedLeaveEventRestartLocalTimeoutMs.
durationUntilServerDelayedLeave !== undefined && !this.state.probablyLeft
? Math.min(this.delayedLeaveEventRestartLocalTimeoutMs, durationUntilServerDelayedLeave)
: this.delayedLeaveEventRestartLocalTimeoutMs,
);
});
// The obvious choice here would be to use the `IRequestOpts` to set the timeout. Since this call might be forwarded
// to the widget driver this information would ge lost. That is why we mimic the AbortError using the race.
// to the widget driver this information would get lost. That is why we mimic the AbortError using the race.
return await Promise.race([
this.client._unstable_updateDelayedEvent(delayId, UpdateDelayedEventAction.Restart),
abortPromise,
])
.then(() => {
// Whenever we successfully restart the delayed event we update the `state.expectedServerDelayLeaveTs`
// which stores the predicted timestamp at which the server will send the delayed leave event if there wont be any further
// successful restart requests.
this.state.expectedServerDelayLeaveTs = Date.now() + this.delayedLeaveEventDelayMs;
this.resetRateLimitCounter(MembershipActionType.RestartDelayedEvent);
this.setAndEmitProbablyLeft(false);
return createInsertActionUpdate(
MembershipActionType.RestartDelayedEvent,
this.delayedLeaveEventRestartMs,
);
})
.catch((e) => {
if (this.state.expectedServerDelayLeaveTs && this.state.expectedServerDelayLeaveTs <= Date.now()) {
// Once we reach this point it's likely that the server is sending the delayed leave event so we emit `probablyLeft = true`.
// It will emit `probablyLeft = false` once we notice about our leave through sync and successfully setup a new state event.
this.setAndEmitProbablyLeft(true);
}
const repeatActionType = MembershipActionType.RestartDelayedEvent;
if (this.isNotFoundError(e)) {
this.state.delayId = undefined;
@@ -607,6 +647,7 @@ export class MembershipManager
this.stateKey,
)
.then(() => {
this.setAndEmitProbablyLeft(false);
this.state.startTime = Date.now();
// The next update should already use twice the membershipEventExpiryTimeout
this.state.expireUpdateIterations = 1;
@@ -930,6 +971,10 @@ export class MembershipManager
this.logger.error("MembershipManager has an unknown state. Actions: ", actions);
return Status.Unknown;
}
public get probablyLeft(): boolean {
return this.state.probablyLeft;
}
}
function createInsertActionUpdate(type: MembershipActionType, offset?: number): ActionUpdate {
+6
View File
@@ -104,6 +104,12 @@ export interface IRTCNotificationContent extends RelationEvent {
"lifetime": number;
}
/**
* MSC4310 decline event content for `org.matrix.msc4310.rtc.decline`.
* Sent as a standard m.reference relation to an `org.matrix.msc4075.rtc.notification` event.
*/
export interface IRTCDeclineContent extends RelationEvent {}
export enum Status {
Disconnected = "Disconnected",
Connecting = "Connecting",
+8 -3
View File
@@ -439,6 +439,13 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
* has been initialised properly.
*/
this.replayEvents?.push(event);
// For annotations (reactions), aggregate immediately (pre-init) to keep
// reaction counts/summary visible while the thread is still initialising.
// Only aggregate as child: parent aggregation is unnecessary here.
if (event.isRelation(RelationType.Annotation)) {
this.timelineSet.relations?.aggregateChildEvent(event, this.timelineSet);
}
} else {
// Case 2: this is happening later, and we have a timeline. In
// this case, these events might be out-of order.
@@ -465,10 +472,8 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
} else {
this.addEventToTimeline(event, toStartOfTimeline);
}
// Aggregation is handled by EventTimelineSet when inserting/adding.
}
// Apply annotations and replace relations to the relations of the timeline only
this.timelineSet.relations?.aggregateParentEvent(event);
this.timelineSet.relations?.aggregateChildEvent(event, this.timelineSet);
}
public async processEvent(event: Optional<MatrixEvent>): Promise<void> {
+18 -3
View File
@@ -326,12 +326,22 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
* Implementation of {@link CryptoBackend.maybeAcceptKeyBundle}.
*/
public async maybeAcceptKeyBundle(roomId: string, inviter: string): Promise<void> {
// TODO: retry this if it gets interrupted or it fails.
// TODO: retry this if it gets interrupted or it fails. (https://github.com/matrix-org/matrix-rust-sdk/issues/5112)
// TODO: do this in the background.
// TODO: handle the bundle message arriving after the invite.
// TODO: handle the bundle message arriving after the invite (https://github.com/element-hq/element-web/issues/30740)
const logger = new LogSpan(this.logger, `maybeAcceptKeyBundle(${roomId}, ${inviter})`);
// Make sure we have an up-to-date idea of the inviter's cross-signing keys, so that we can check if the
// device that sent us the bundle data was correctly cross-signed.
//
// TODO: it would be nice to skip this step if we have an up-to-date copy of the inviter's cross-signing keys,
// but we don't have an easy way to check that. Possibly the rust side could trigger a key request and then
// block until it happens.
logger.info(`Checking inviter cross-signing keys`);
const request = this.olmMachine.queryKeysForUsers([new RustSdkCryptoJs.UserId(inviter)]);
await this.outgoingRequestProcessor.makeOutgoingRequest(request);
const bundleData = await this.olmMachine.getReceivedRoomKeyBundleData(
new RustSdkCryptoJs.RoomId(roomId),
new RustSdkCryptoJs.UserId(inviter),
@@ -1076,7 +1086,12 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
verificationMethodIdentifierToMethod(method),
);
// Get the request content to send to the DM room
const verificationEventContent: string = await userIdentity.verificationRequestContent(methods);
const verCont: string = await userIdentity.verificationRequestContent(methods);
// TODO: due to https://github.com/matrix-org/matrix-rust-sdk/issues/5643, we need to fix up the verification request content to include `msgtype`.
const verContObj = JSON.parse(verCont);
verContObj["msgtype"] = "m.key.verification.request";
const verificationEventContent: string = JSON.stringify(verContObj);
// Send the request content to send to the DM room
const eventId = await this.sendVerificationRequestContent(roomId, verificationEventContent);
+4 -1
View File
@@ -317,7 +317,10 @@ export class SyncAccumulator {
break;
case Category.Join:
if (this.inviteRooms[roomId]) {
if (this.knockRooms[roomId]) {
// delete knock state on join
delete this.knockRooms[roomId];
} else if (this.inviteRooms[roomId]) {
// (1)
// was previously invite, now join. We expect /sync to give
// the entire state and timeline on 'join', so delete previous
+5 -10
View File
@@ -17,24 +17,19 @@ limitations under the License.
/**
* Room versions strings that we know about and do not use hydra semantics.
*/
const HYDRA_ROOM_VERSIONS = ["org.matrix.hydra.11", "12"];
const PRE_HYDRA_ROOM_VERSIONS = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"];
/**
* Checks if the given room version is one where new "hydra" power level
* semantics (ie. room version 12 or later) should be used
* (see https://github.com/matrix-org/matrix-spec-proposals/pull/4289).
* This will return `true` for versions that are known to the js-sdk and
* use hydra: any room versions unknown to the js-sdk (experimental or
* otherwise) will cause the function to return `false`.
* This will return `false` for versions that are known to the js-sdk and
* do not use hydra: any room versions unknown to the js-sdk (experimental or
* otherwise) will cause the function to return true.
*
* @param roomVersion - The version of the room to check.
* @returns `true` if hydra semantics should be used for the room version, `false` otherwise.
*/
export function shouldUseHydraForRoomVersion(roomVersion: string): boolean {
// Future new room versions must obviously be added to the constant above,
// otherwise the js-sdk will use the old, pre-hydra semantics. At some point
// it would make sense to assume hydra for unknown versions but this will break
// any rooms using unknown versions, so at hydra switch time we've agreed all
// Element clients will only use hydra for the two specific hydra versions.
return HYDRA_ROOM_VERSIONS.includes(roomVersion);
return !PRE_HYDRA_ROOM_VERSIONS.includes(roomVersion);
}
+406 -361
View File
File diff suppressed because it is too large Load Diff