Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ef97df8ed0 | |||
| 0ce751c462 | |||
| 2706873948 | |||
| 4a9006aea6 | |||
| 43c72d5bf5 | |||
| e551b92a07 | |||
| 32f51e852b | |||
| 82aa04d894 | |||
| ff89c9ec42 | |||
| ccd825fb39 | |||
| b313eb5912 | |||
| 2b12675675 | |||
| 246788b874 | |||
| b0b80401aa | |||
| 6a0164f37f | |||
| f963d61bcb | |||
| b32619ad24 | |||
| 3d3c3ba55f | |||
| d62c658a72 | |||
| bdc4a69023 | |||
| ab892420b5 | |||
| ed607c48b0 | |||
| 2f1d654f14 | |||
| c4c7f94514 | |||
| 1fac06e223 | |||
| b80d0091d2 | |||
| 3a33c658bb | |||
| 81e42b9531 | |||
| 7f7ecd060d | |||
| 6126ee125a | |||
| 78f718ff82 | |||
| c1c1be0c5d | |||
| 1952eaa1ff | |||
| 8851f8b07c | |||
| 15eafe34b3 | |||
| f0d48236fa | |||
| dde9d48726 |
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user