diff --git a/.eslintrc.js b/.eslintrc.js
index 26865d55ec..7f8d0adb9c 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -1,8 +1,16 @@
+/*
+Copyright 2025 Element Creations Ltd.
+
+SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+Please see LICENSE files in the repository root for full details.
+*/
+
module.exports = {
plugins: ["matrix-org", "eslint-plugin-react-compiler"],
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/react", "plugin:matrix-org/a11y"],
parserOptions: {
project: ["./tsconfig.json"],
+ tsconfigRootDir: __dirname,
},
env: {
browser: true,
@@ -160,6 +168,10 @@ module.exports = {
group: ["@vector-im/compound-design-tokens/icons/*"],
message: "Please use @vector-im/compound-design-tokens/assets/web/icons/* instead",
},
+ {
+ group: ["**/packages/shared-components/**", "../packages/shared-components/**"],
+ message: "Please use @element-hq/web-shared-components",
+ },
],
},
],
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 34431799f4..ae2cf6294d 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -17,9 +17,16 @@
/playwright/e2e/crypto/ @element-hq/element-crypto-web-reviewers
/playwright/e2e/settings/encryption-user-tab/ @element-hq/element-crypto-web-reviewers
+
+/src/models/Call.ts @element-hq/element-call-reviewers
+/src/call-types.ts @element-hq/element-call-reviewers
+/src/components/views/voip @element-hq/element-call-reviewers
+/playwright/e2e/voip/element-call.spec.ts @element-hq/element-call-reviewers
+
# Ignore translations as those will be updated by GHA for Localazy download
/src/i18n/strings
/src/i18n/strings/en_EN.json @element-hq/element-web-reviewers
-# Ignore the synapse plugin as this is updated by GHA for docker image updating
+# Ignore the synapse & mas plugins as this is updated by GHA for docker image updating
/playwright/testcontainers/synapse.ts
+/playwright/testcontainers/mas.ts
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index ebde627fde..759266d4e0 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -2,6 +2,7 @@
## Checklist
+- [ ] I have read through [review guidelines](../docs/review.md) and [CONTRIBUTING.md](../CONTRIBUTING.md).
- [ ] Tests written for new code (and old code if feasible).
- [ ] New or updated `public`/`exported` symbols have accurate [TSDoc](https://tsdoc.org/) documentation.
- [ ] Linter and other CI checks pass.
diff --git a/.github/labels.yml b/.github/labels.yml
index f8adbe8e53..649e1a7407 100644
--- a/.github/labels.yml
+++ b/.github/labels.yml
@@ -279,3 +279,6 @@
- name: "Z-Flaky-Test-Disabled"
description: "The flaking test has been disabled"
color: "ededed"
+- name: "Z-Skip-Coverage"
+ description: "Skip SonarQube coverage for this PR"
+ color: "ededed"
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 68b9f4e703..74425f25b4 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -10,8 +10,7 @@ concurrency:
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
# develop pushes and repository_dispatch handled in build_develop.yaml
env:
- # These must be set for fetchdep.sh to get the right branch
- REPOSITORY: ${{ github.repository }}
+ # This must be set for fetchdep.sh to get the right branch
PR_NUMBER: ${{ github.event.pull_request.number }}
permissions: {} # No permissions required
jobs:
@@ -43,9 +42,9 @@ jobs:
run:
shell: bash
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
with:
# Disable cache on Windows as it is slower than not caching
# https://github.com/actions/setup-node/issues/975
@@ -56,15 +55,7 @@ jobs:
- run: yarn config set network-timeout 300000
- name: Fetch layered build
- id: layered_build
- env:
- # tell layered.sh to check out the right sha of the JS-SDK & EW, if they were given one
- JS_SDK_GITHUB_BASE_REF: ${{ inputs.matrix-js-sdk-sha }}
- run: |
- scripts/layered.sh
- JSSDK_SHA=$(git -C matrix-js-sdk rev-parse --short=12 HEAD)
- VECTOR_SHA=$(git rev-parse --short=12 HEAD)
- echo "VERSION=$VECTOR_SHA--js-$JSSDK_SHA" >> $GITHUB_OUTPUT
+ run: ./scripts/layered.sh
- name: Copy config
run: cp element.io/develop/config.json config.json
@@ -72,12 +63,10 @@ jobs:
- name: Build
env:
CI_PACKAGE: true
- VERSION: "${{ steps.layered_build.outputs.VERSION }}"
- run: |
- yarn build
+ run: VERSION=$(scripts/get-version-from-git.sh) yarn build
- name: Upload Artifact
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
+ uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
with:
name: webapp-${{ matrix.image }}
path: webapp
diff --git a/.github/workflows/build_debian.yaml b/.github/workflows/build_debian.yaml
index 247e5604ee..48c21c9dc5 100644
--- a/.github/workflows/build_debian.yaml
+++ b/.github/workflows/build_debian.yaml
@@ -14,7 +14,7 @@ jobs:
R2_URL: ${{ vars.CF_R2_S3_API }}
VERSION: ${{ github.ref_name }}
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Download package
run: |
@@ -62,7 +62,7 @@ jobs:
dpkg-gencontrol -v"$VERSION" -ldebian/tmp/DEBIAN/changelog
dpkg-deb -Zxz --root-owner-group --build debian/tmp element-web.deb
- - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
+ - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
with:
name: element-web.deb
path: element-web.deb
diff --git a/.github/workflows/build_develop.yml b/.github/workflows/build_develop.yml
index 9550bf9139..a923e1db1d 100644
--- a/.github/workflows/build_develop.yml
+++ b/.github/workflows/build_develop.yml
@@ -26,9 +26,9 @@ jobs:
R2_URL: ${{ vars.CF_R2_S3_API }}
R2_PUBLIC_URL: "https://element-web-develop.element.io"
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
with:
cache: "yarn"
node-version: "lts/*"
@@ -53,7 +53,7 @@ jobs:
- run: mv dist/element-*.tar.gz dist/develop.tar.gz
- - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
+ - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
with:
name: webapp
path: dist/develop.tar.gz
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 8b52e6764c..7abba47247 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -34,7 +34,7 @@ jobs:
env:
SITE: ${{ inputs.site || 'staging.element.io' }}
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Load GPG key
run: |
diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml
index 75a00af7aa..5c2e989e28 100644
--- a/.github/workflows/docker.yaml
+++ b/.github/workflows/docker.yaml
@@ -20,31 +20,31 @@ jobs:
env:
TEST_TAG: vectorim/element-web:test
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
with:
fetch-depth: 0 # needed for docker-package to be able to calculate the version
- name: Install Cosign
- uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3
+ uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3
if: github.event_name != 'pull_request'
- name: Set up QEMU
- uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3
+ uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
+ uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
with:
install: true
- name: Login to Docker Hub
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
+ uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
if: github.event_name != 'pull_request'
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
+ uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
if: github.event_name != 'pull_request'
with:
registry: ghcr.io
@@ -53,7 +53,7 @@ jobs:
- name: Build and load
id: test-build
- uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6
+ uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
with:
context: .
load: true
@@ -96,7 +96,7 @@ jobs:
- name: Docker meta
id: meta
- uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
+ uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5
if: github.event_name != 'pull_request'
with:
images: |
@@ -110,7 +110,7 @@ jobs:
- name: Build and push
id: build-and-push
- uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6
+ uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
if: github.event_name != 'pull_request'
with:
context: .
@@ -132,10 +132,23 @@ jobs:
cosign sign --yes ${images}
- name: Update repo description
- uses: peter-evans/dockerhub-description@432a30c9e07499fd01da9f8a49f0faf9e0ca5b77 # v4
+ uses: peter-evans/dockerhub-description@1b9a80c056b620d92cedb9d9b5a223409c68ddfa # v5
if: github.event_name != 'pull_request'
continue-on-error: true
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
repository: vectorim/element-web
+
+ - name: Repository Dispatch
+ uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4
+ if: github.event_name != 'pull_request'
+ with:
+ repository: element-hq/element-web-pro
+ token: ${{ secrets.ELEMENT_BOT_TOKEN }}
+ event-type: image-built
+ # Stable way to determine the :version
+ client-payload: |-
+ {
+ "base-ref": "${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}"
+ }
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index e7d69cf477..9020f7914b 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -17,23 +17,23 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Fetch element-desktop
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
with:
repository: element-hq/element-desktop
path: element-desktop
- name: Fetch element-web
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
with:
path: element-web
- name: Fetch matrix-js-sdk
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
with:
repository: matrix-org/matrix-js-sdk
path: matrix-js-sdk
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
with:
cache: "yarn"
cache-dependency-path: element-web/yarn.lock
@@ -43,13 +43,13 @@ jobs:
working-directory: element-web
run: |
yarn install --frozen-lockfile
- yarn ts-node ./scripts/gen-workflow-mermaid.ts ../element-desktop ../element-web ../matrix-js-sdk > docs/automations.md
+ yarn node ./scripts/gen-workflow-mermaid.ts ../element-desktop ../element-web ../matrix-js-sdk > docs/automations.md
echo "- [Automations](automations.md)" >> docs/SUMMARY.md
- name: Setup mdBook
uses: peaceiris/actions-mdbook@ee69d230fe19748b7abf22df32acaa93833fad08 # v2
with:
- mdbook-version: "0.4.10"
+ mdbook-version: "0.5.1"
- name: Install mdbook extensions
run: cargo install mdbook-combiner mdbook-mermaid
@@ -88,7 +88,7 @@ jobs:
run: mdbook build
- name: Upload artifact
- uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3
+ uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4
with:
path: ./book
diff --git a/.github/workflows/end-to-end-tests-netlify.yaml b/.github/workflows/end-to-end-tests-netlify.yaml
index 90a8c3d24b..049f4ea343 100644
--- a/.github/workflows/end-to-end-tests-netlify.yaml
+++ b/.github/workflows/end-to-end-tests-netlify.yaml
@@ -25,7 +25,7 @@ jobs:
actions: read
steps:
- name: Download HTML report
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
+ uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
diff --git a/.github/workflows/end-to-end-tests.yaml b/.github/workflows/end-to-end-tests.yaml
index b2d49d7703..1b366333b6 100644
--- a/.github/workflows/end-to-end-tests.yaml
+++ b/.github/workflows/end-to-end-tests.yaml
@@ -50,25 +50,20 @@ jobs:
runners-matrix: ${{ steps.runner-vars.outputs.matrix }}
steps:
- name: Checkout code
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
with:
repository: element-hq/element-web
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
with:
cache: "yarn"
node-version: "lts/*"
- name: Fetch layered build
- id: layered_build
env:
# tell layered.sh to check out the right sha of the JS-SDK & EW, if they were given one
JS_SDK_GITHUB_BASE_REF: ${{ inputs.matrix-js-sdk-sha }}
- run: |
- scripts/layered.sh
- JSSDK_SHA=$(git -C matrix-js-sdk rev-parse --short=12 HEAD)
- VECTOR_SHA=$(git rev-parse --short=12 HEAD)
- echo "VERSION=$VECTOR_SHA--js-$JSSDK_SHA" >> $GITHUB_OUTPUT
+ run: scripts/layered.sh
- name: Copy config
run: cp element.io/develop/config.json config.json
@@ -76,12 +71,10 @@ jobs:
- name: Build
env:
CI_PACKAGE: true
- VERSION: "${{ steps.layered_build.outputs.VERSION }}"
- run: |
- yarn build
+ run: VERSION=$(scripts/get-version-from-git.sh) yarn build
- name: Upload Artifact
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
+ uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
with:
name: webapp
path: webapp
@@ -89,7 +82,7 @@ jobs:
- name: Calculate runner variables
id: runner-vars
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const numRunners = parseInt(process.env.NUM_RUNNERS, 10);
@@ -129,18 +122,18 @@ jobs:
- runAllTests: false
project: Pinecone
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
with:
persist-credentials: false
repository: element-hq/element-web
- name: 📥 Download artifact
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
+ uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
with:
name: webapp
path: webapp
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
with:
cache: "yarn"
cache-dependency-path: yarn.lock
@@ -154,7 +147,7 @@ jobs:
run: echo "version=$(yarn list --pattern @playwright/test --depth=0 --json --non-interactive --no-progress | jq -r '.data.trees[].name')" >> $GITHUB_OUTPUT
- name: Cache playwright binaries
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
id: playwright-cache
with:
path: ~/.cache/ms-playwright
@@ -179,7 +172,7 @@ jobs:
- name: Upload blob report to GitHub Actions Artifacts
if: always()
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
+ uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
with:
name: all-blob-reports-${{ matrix.project }}-${{ matrix.runner }}
path: blob-report
@@ -201,13 +194,13 @@ jobs:
if: always()
runs-on: ubuntu-24.04
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
if: inputs.skip != true
with:
persist-credentials: false
repository: element-hq/element-web
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
if: inputs.skip != true
with:
cache: "yarn"
@@ -219,7 +212,7 @@ jobs:
- name: Download blob reports from GitHub Actions Artifacts
if: inputs.skip != true
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
+ uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
with:
pattern: all-blob-reports-*
path: all-blob-reports
@@ -227,7 +220,7 @@ jobs:
- name: Merge into HTML Report
if: inputs.skip != true
- run: yarn playwright merge-reports --reporter=html,./playwright/flaky-reporter.ts,./playwright/stale-screenshot-reporter.ts ./all-blob-reports
+ run: yarn playwright merge-reports --reporter=html,./playwright/flaky-reporter.ts,@element-hq/element-web-playwright-common/lib/stale-screenshot-reporter.js ./all-blob-reports
env:
# Only pass creds to the flaky-reporter on main branch runs
GITHUB_TOKEN: ${{ github.ref_name == 'develop' && secrets.ELEMENT_BOT_TOKEN || '' }}
@@ -235,7 +228,7 @@ jobs:
# Upload the HTML report even if one of our reporters fails, this can happen when stale screenshots are detected
- name: Upload HTML report
if: always() && inputs.skip != true
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
+ uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
with:
name: html-report
path: playwright-report
diff --git a/.github/workflows/issue_closed.yml b/.github/workflows/issue_closed.yml
index 249f1eb342..375c2e7184 100644
--- a/.github/workflows/issue_closed.yml
+++ b/.github/workflows/issue_closed.yml
@@ -10,7 +10,7 @@ jobs:
name: Tidy closed issues
runs-on: ubuntu-24.04
steps:
- - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
+ - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
id: main
with:
# PAT needed as the GITHUB_TOKEN won't be able to see cross-references from other orgs (matrix-org)
@@ -142,7 +142,7 @@ jobs:
});
}
}
- - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
+ - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
name: Close duplicate as Not Planned
if: steps.main.outputs.closeAsNotPlanned
with:
diff --git a/.github/workflows/netlify.yaml b/.github/workflows/netlify.yaml
index a7909265c7..ab2433c267 100644
--- a/.github/workflows/netlify.yaml
+++ b/.github/workflows/netlify.yaml
@@ -28,7 +28,7 @@ jobs:
Exercise caution. Use test accounts.
- name: 📥 Download artifact
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
+ uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
diff --git a/.github/workflows/pending-reviews.yaml b/.github/workflows/pending-reviews.yaml
index 199eb60daa..0474e60aa0 100644
--- a/.github/workflows/pending-reviews.yaml
+++ b/.github/workflows/pending-reviews.yaml
@@ -16,7 +16,7 @@ jobs:
URL: "https://github.com/pulls?q=is%3Apr+is%3Aopen+repo%3Amatrix-org%2Fmatrix-js-sdk+repo%3Amatrix-org%2Fmatrix-react-sdk+repo%3Aelement-hq%2Felement-web+repo%3Aelement-hq%2Felement-desktop+review-requested%3A%40me+sort%3Aupdated-desc+"
RELEASE_BLOCKERS_URL: "https://github.com/pulls?q=is%3Aopen+repo%3Amatrix-org%2Fmatrix-js-sdk+repo%3Amatrix-org%2Fmatrix-react-sdk+repo%3Aelement-hq%2Felement-web+repo%3Aelement-hq%2Felement-desktop+sort%3Aupdated-desc+label%3AX-Release-Blocker+"
steps:
- - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
+ - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
HS_URL: ${{ secrets.BETABOT_HS_URL }}
ROOM_ID: ${{ secrets.ROOM_ID }}
diff --git a/.github/workflows/playwright-image-updates.yaml b/.github/workflows/playwright-image-updates.yaml
index 4cbdb17bbd..1ce3d767a7 100644
--- a/.github/workflows/playwright-image-updates.yaml
+++ b/.github/workflows/playwright-image-updates.yaml
@@ -10,7 +10,7 @@ jobs:
permissions:
pull-requests: write
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Update synapse image
run: |
@@ -21,9 +21,18 @@ jobs:
env:
IMAGE: ghcr.io/element-hq/synapse:develop
+ - name: Update MAS image
+ run: |
+ docker pull "$IMAGE"
+ INSPECT=$(docker inspect --format='{{index .RepoDigests 0}}' "$IMAGE")
+ DIGEST=${INSPECT#*@}
+ sed -i "s/const TAG.*/const TAG = \"main@$DIGEST\";/" playwright/testcontainers/mas.ts
+ env:
+ IMAGE: ghcr.io/element-hq/matrix-authentication-service:main
+
- name: Create Pull Request
id: cpr
- uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7
+ uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
branch: actions/playwright-image-updates
diff --git a/.github/workflows/pull_request_base_branch.yaml b/.github/workflows/pull_request_base_branch.yaml
index fbdebfbed0..e79c37783b 100644
--- a/.github/workflows/pull_request_base_branch.yaml
+++ b/.github/workflows/pull_request_base_branch.yaml
@@ -8,7 +8,7 @@ jobs:
name: Check PR base branch
runs-on: ubuntu-24.04
steps:
- - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
+ - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const baseBranch = context.payload.pull_request.base.ref;
diff --git a/.github/workflows/release_prepare.yml b/.github/workflows/release_prepare.yml
index 2f36644c2e..7be70e6b00 100644
--- a/.github/workflows/release_prepare.yml
+++ b/.github/workflows/release_prepare.yml
@@ -41,7 +41,7 @@ jobs:
REPOS: matrix-js-sdk element-web element-desktop
steps:
- name: Checkout Element Desktop
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
if: inputs.element-desktop
with:
repository: element-hq/element-desktop
@@ -51,7 +51,7 @@ jobs:
fetch-tags: true
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
- name: Checkout Element Web
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
if: inputs.element-web
with:
repository: element-hq/element-web
@@ -61,7 +61,7 @@ jobs:
fetch-tags: true
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
- name: Checkout Matrix JS SDK
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
if: inputs.matrix-js-sdk
with:
repository: matrix-org/matrix-js-sdk
diff --git a/.github/workflows/shared-component-publish.yaml b/.github/workflows/shared-component-publish.yaml
new file mode 100644
index 0000000000..6869c17660
--- /dev/null
+++ b/.github/workflows/shared-component-publish.yaml
@@ -0,0 +1,40 @@
+name: Publish shared component npm package
+on:
+ workflow_dispatch: {}
+
+concurrency: release
+jobs:
+ publish:
+ name: "Publish"
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ id-token: write
+
+ steps:
+ - name: 🧮 Checkout code
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
+
+ - name: 🔧 Set up node environment
+ uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
+ with:
+ cache: "yarn"
+ node-version-file: ".node-version"
+ registry-url: "https://registry.npmjs.org"
+
+ # Ensure npm 11.5.1 or later is installed
+ - name: Update npm
+ run: npm install -g npm@latest
+
+ # Need to setup element web too as it needs the translations
+ - name: 🛠️ Setup EW
+ run: yarn install --pure-lockfile
+
+ - name: 🛠️ Setup
+ # When running `install` it also calls the `prepare` step which generates
+ # a build
+ run: yarn --cwd packages/shared-components install --pure-lockfile
+
+ - name: 🚀 Publish to npm
+ working-directory: packages/shared-components
+ run: npm publish --access public --tag test --provenance
diff --git a/.github/workflows/shared-component-visual-tests-netlify.yaml b/.github/workflows/shared-component-visual-tests-netlify.yaml
new file mode 100644
index 0000000000..816d899836
--- /dev/null
+++ b/.github/workflows/shared-component-visual-tests-netlify.yaml
@@ -0,0 +1,51 @@
+# Triggers after the shared component tests have finished,
+# It uploads the received images and diffs to netlify, printing the URLs to the console
+name: Upload Shared Component Visual Test Diffs
+on:
+ workflow_run:
+ workflows: ["Shared Component Visual Tests"]
+ types:
+ - completed
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.run_id }}
+ cancel-in-progress: ${{ github.event.workflow_run.event == 'pull_request' }}
+
+permissions: {}
+
+jobs:
+ report:
+ if: github.event.workflow_run.conclusion == 'failure'
+ name: Upload Diffs
+ runs-on: ubuntu-24.04
+ environment: Netlify
+ permissions:
+ actions: read
+ deployments: write
+ steps:
+ - name: Install tree
+ run: "sudo apt-get install -y tree"
+
+ - name: Download Diffs
+ uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ run-id: ${{ github.event.workflow_run.id }}
+ name: received-images
+ path: received-images
+
+ - name: Generate Index
+ run: "cd received-images && tree -L 1 --noreport -H '' -o index.html ."
+
+ - name: 📤 Deploy to Netlify
+ uses: matrix-org/netlify-pr-preview@9805cd123fc9a7e421e35340a05e1ebc5dee46b5 # v3
+ with:
+ path: received-images
+ owner: ${{ github.event.workflow_run.head_repository.owner.login }}
+ branch: ${{ github.event.workflow_run.head_branch }}
+ revision: ${{ github.event.workflow_run.head_sha }}
+ token: ${{ secrets.NETLIFY_AUTH_TOKEN }}
+ site_id: ${{ vars.NETLIFY_SITE_ID }}
+ desc: Shared Component Visual Diffs
+ deployment_env: SharedComponentDiffs
+ prefix: "diffs-"
diff --git a/.github/workflows/shared-component-visual-tests.yaml b/.github/workflows/shared-component-visual-tests.yaml
new file mode 100644
index 0000000000..8f9cbdae31
--- /dev/null
+++ b/.github/workflows/shared-component-visual-tests.yaml
@@ -0,0 +1,66 @@
+name: Shared Component Visual Tests
+on:
+ pull_request: {}
+ merge_group:
+ types: [checks_requested]
+ push:
+ branches: [develop, master]
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
+ cancel-in-progress: true
+
+permissions: {} # No permissions required
+
+jobs:
+ testStorybook:
+ name: "Run Visual Tests"
+ runs-on: ubuntu-24.04
+ permissions:
+ actions: read
+ issues: read
+ pull-requests: read
+ steps:
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
+ with:
+ persist-credentials: false
+ repository: element-hq/element-web
+
+ - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
+ with:
+ cache: "yarn"
+ node-version: "lts/*"
+
+ - name: Install element web dependencies
+ run: yarn install --frozen-lockfile
+
+ - name: Install dependencies
+ working-directory: packages/shared-components
+ run: yarn install --frozen-lockfile
+
+ - name: Get installed Playwright version
+ working-directory: packages/shared-components
+ id: playwright
+ run: echo "version=$(yarn list --pattern @playwright/test --depth=0 --json --non-interactive --no-progress | jq -r '.data.trees[].name')" >> $GITHUB_OUTPUT
+
+ - name: Cache playwright binaries
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
+ id: playwright-cache
+ with:
+ path: ~/.cache/ms-playwright
+ key: ${{ runner.os }}-${{ runner.arch }}-playwright-${{ steps.playwright.outputs.version }}-onlyshell
+
+ - name: Install Playwright browsers
+ working-directory: packages/shared-components
+ if: steps.playwright-cache.outputs.cache-hit != 'true'
+ run: "yarn playwright install --with-deps --only-shell"
+
+ - name: Run Visual tests
+ run: "yarn --cwd packages/shared-components test:storybook:ci"
+
+ - name: Upload received images & diffs
+ if: always()
+ uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
+ with:
+ name: received-images
+ path: packages/shared-components/playwright/shared-component-received
diff --git a/.github/workflows/static_analysis.yaml b/.github/workflows/static_analysis.yaml
index d63e0da8ed..9f90572371 100644
--- a/.github/workflows/static_analysis.yaml
+++ b/.github/workflows/static_analysis.yaml
@@ -12,8 +12,7 @@ concurrency:
cancel-in-progress: true
env:
- # These must be set for fetchdep.sh to get the right branch
- REPOSITORY: ${{ github.repository }}
+ # This must be set for fetchdep.sh to get the right branch
PR_NUMBER: ${{ github.event.pull_request.number }}
permissions: {} # No permissions required
@@ -23,9 +22,9 @@ jobs:
name: "Typescript Syntax Check"
runs-on: ubuntu-24.04
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
with:
cache: "yarn"
node-version: "lts/*"
@@ -36,6 +35,12 @@ jobs:
- name: Typecheck
run: "yarn run lint:types"
+ - name: Install Shared Component Dependencies
+ run: "yarn --cwd packages/shared-components install"
+
+ - name: Typecheck Shared Components
+ run: "yarn --cwd packages/shared-components run lint:types"
+
i18n_lint:
name: "i18n Check"
uses: matrix-org/matrix-web-i18n/.github/workflows/i18n_check.yml@main
@@ -52,12 +57,13 @@ jobs:
error|misconfigured
welcome_to_element
devtools|settings|elementCallUrl
+ labs|sliding_sync_description
rethemendex_lint:
name: "Rethemendex Check"
runs-on: ubuntu-24.04
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- run: ./res/css/rethemendex.sh
@@ -67,9 +73,9 @@ jobs:
name: "ESLint"
runs-on: ubuntu-24.04
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
with:
cache: "yarn"
node-version: "lts/*"
@@ -81,13 +87,19 @@ jobs:
- name: Run Linter
run: "yarn run lint:js"
+ - name: Install Shared Component Deps
+ run: "yarn --cwd packages/shared-components install --frozen-lockfile"
+
+ - name: Run Linter
+ run: "yarn --cwd packages/shared-components run lint:js"
+
style_lint:
name: "Style Lint"
runs-on: ubuntu-24.04
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
with:
cache: "yarn"
node-version: "lts/*"
@@ -103,9 +115,9 @@ jobs:
name: "Workflow Lint"
runs-on: ubuntu-24.04
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
with:
cache: "yarn"
node-version: "lts/*"
@@ -121,9 +133,9 @@ jobs:
name: "Analyse Dead Code"
runs-on: ubuntu-24.04
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
with:
cache: "yarn"
node-version: "lts/*"
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 276c53c098..95382b180c 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -29,8 +29,8 @@ env:
permissions: {}
jobs:
- jest:
- name: Jest
+ jest_ew:
+ name: Jest (Element Web)
runs-on: ubuntu-24.04
strategy:
fail-fast: false
@@ -39,12 +39,12 @@ jobs:
runner: [1, 2]
steps:
- name: Checkout code
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
with:
repository: ${{ inputs.matrix-js-sdk-sha && 'element-hq/element-web' || github.repository }}
- name: Yarn cache
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
with:
node-version: "lts/*"
cache: "yarn"
@@ -55,7 +55,7 @@ jobs:
JS_SDK_GITHUB_BASE_REF: ${{ inputs.matrix-js-sdk-sha }}
- name: Jest Cache
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: /tmp/jest_cache
key: ${{ hashFiles('**/yarn.lock') }}
@@ -84,7 +84,7 @@ jobs:
- name: Upload Artifact
if: env.ENABLE_COVERAGE == 'true'
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
+ uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
with:
name: coverage-${{ matrix.runner }}
path: |
@@ -93,18 +93,18 @@ jobs:
complete:
name: jest-tests
- needs: jest
+ needs: jest_ew
if: always()
runs-on: ubuntu-24.04
permissions:
statuses: write
steps:
- - if: needs.jest.result != 'skipped' && needs.jest.result != 'success'
+ - if: needs.jest_ew.result != 'skipped' && needs.jest_ew.result != 'success'
run: exit 1
- name: Skip SonarCloud in merge queue
if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true'
- uses: guibranco/github-status-action-v2@5f2b01ce1394109f70954ae6b69ef41cf7928e63
+ uses: guibranco/github-status-action-v2@5530c593759f489bba08272e96986ffc571c1ea1
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: success
@@ -112,3 +112,56 @@ jobs:
context: SonarCloud Code Analysis
sha: ${{ github.sha }}
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
+
+ jest_sc:
+ name: Jest (Shared Components)
+ runs-on: ubuntu-24.04
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
+ with:
+ repository: ${{ inputs.matrix-js-sdk-sha && 'element-hq/element-web' || github.repository }}
+
+ - name: Yarn cache
+ uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
+ with:
+ node-version: "lts/*"
+ cache: "yarn"
+
+ - name: Install EW Deps
+ run: "yarn install"
+
+ - name: Install Shared Component Deps
+ working-directory: "packages/shared-components"
+ run: "yarn install"
+
+ - name: Jest Cache
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
+ with:
+ path: /tmp/jest_cache
+ key: ${{ hashFiles('**/yarn.lock') }}
+
+ - name: Get number of CPU cores
+ id: cpu-cores
+ uses: SimenB/github-actions-cpu-cores@97ba232459a8e02ff6121db9362b09661c875ab8 # v2
+
+ - name: Run tests
+ working-directory: "packages/shared-components"
+ run: |
+ yarn test \
+ --coverage=${{ env.ENABLE_COVERAGE }} \
+ --ci \
+ --max-workers ${{ steps.cpu-cores.outputs.count }} \
+ --cacheDirectory /tmp/jest_cache
+ env:
+ # tell jest to use coloured output
+ FORCE_COLOR: true
+
+ - name: Upload Artifact
+ if: env.ENABLE_COVERAGE == 'true'
+ uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
+ with:
+ name: coverage-sharedcomponents
+ path: |
+ packages/shared-components/coverage
+ !packages/shared-components/coverage/lcov-report
diff --git a/.github/workflows/triage-assigned.yml b/.github/workflows/triage-assigned.yml
index f190122a1c..b16f626c15 100644
--- a/.github/workflows/triage-assigned.yml
+++ b/.github/workflows/triage-assigned.yml
@@ -15,7 +15,7 @@ jobs:
contains(github.event.issue.assignees.*.login, 'dbkr') ||
contains(github.event.issue.assignees.*.login, 'MidhunSureshR')
steps:
- - uses: actions/add-to-project@main
+ - uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
with:
project-url: https://github.com/orgs/element-hq/projects/67
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
diff --git a/.github/workflows/triage-incoming.yml b/.github/workflows/triage-incoming.yml
index b084b4d55e..d81322bc8c 100644
--- a/.github/workflows/triage-incoming.yml
+++ b/.github/workflows/triage-incoming.yml
@@ -10,7 +10,7 @@ jobs:
automate-project-columns:
runs-on: ubuntu-24.04
steps:
- - uses: actions/add-to-project@main
+ - uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
with:
project-url: https://github.com/orgs/element-hq/projects/120
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
diff --git a/.github/workflows/triage-labelled.yml b/.github/workflows/triage-labelled.yml
index e1849e0efc..496cfd53df 100644
--- a/.github/workflows/triage-labelled.yml
+++ b/.github/workflows/triage-labelled.yml
@@ -27,7 +27,7 @@ jobs:
contains(github.event.issue.labels.*.name, 'A-Rich-Text-Editor') ||
contains(github.event.issue.labels.*.name, 'A-Element-Call')
steps:
- - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
+ - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
github.rest.issues.addLabels({
@@ -44,7 +44,7 @@ jobs:
contains(github.event.issue.labels.*.name, 'good first issue') ||
contains(github.event.issue.labels.*.name, 'Hacktoberfest')
steps:
- - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
+ - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
github.rest.issues.addLabels({
@@ -112,7 +112,7 @@ jobs:
contains(github.event.issue.labels.*.name, 'O-Frequent') ||
contains(github.event.issue.labels.*.name, 'A11y'))
steps:
- - uses: actions/add-to-project@main
+ - uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
with:
project-url: https://github.com/orgs/element-hq/projects/18
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
@@ -123,7 +123,7 @@ jobs:
if: >
contains(github.event.issue.labels.*.name, 'X-Needs-Product')
steps:
- - uses: actions/add-to-project@main
+ - uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
with:
project-url: https://github.com/orgs/element-hq/projects/28
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
@@ -134,7 +134,7 @@ jobs:
if: >
contains(github.event.issue.labels.*.name, 'A-New-Search-Experience')
steps:
- - uses: actions/add-to-project@main
+ - uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
with:
project-url: https://github.com/orgs/element-hq/projects/48
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
@@ -145,7 +145,7 @@ jobs:
if: >
contains(github.event.issue.labels.*.name, 'Team: VoIP')
steps:
- - uses: actions/add-to-project@main
+ - uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
with:
project-url: https://github.com/orgs/element-hq/projects/41
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
@@ -156,7 +156,7 @@ jobs:
if: >
contains(github.event.issue.labels.*.name, 'Team: Crypto')
steps:
- - uses: actions/add-to-project@main
+ - uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
with:
project-url: https://github.com/orgs/element-hq/projects/76
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
@@ -172,7 +172,7 @@ jobs:
contains(github.event.issue.labels.*.name, 'A-Testing') ||
contains(github.event.issue.labels.*.name, 'Z-Flaky-Test')
steps:
- - uses: actions/add-to-project@main
+ - uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
with:
project-url: https://github.com/orgs/element-hq/projects/101
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
diff --git a/.github/workflows/triage-move-review-requests.yml b/.github/workflows/triage-move-review-requests.yml
index d3bcda270b..4d8745f4bd 100644
--- a/.github/workflows/triage-move-review-requests.yml
+++ b/.github/workflows/triage-move-review-requests.yml
@@ -9,7 +9,7 @@ jobs:
name: Move PRs asking for design review to the design board
runs-on: ubuntu-24.04
steps:
- - uses: octokit/graphql-action@v2.x
+ - uses: octokit/graphql-action@abaeca7ba4f0325d63b8de7ef943c2418d161b93 # v3.0.0
id: find_team_members
with:
headers: '{"GraphQL-Features": "projects_next_graphql"}'
@@ -52,7 +52,7 @@ jobs:
fi
env:
TEAM: "design"
- - uses: octokit/graphql-action@v2.x
+ - uses: octokit/graphql-action@abaeca7ba4f0325d63b8de7ef943c2418d161b93 # v3.0.0
id: add_to_project
if: steps.any_matching_reviewers.outputs.match == 'true'
with:
@@ -76,7 +76,7 @@ jobs:
name: Move PRs asking for design review to the design board
runs-on: ubuntu-24.04
steps:
- - uses: octokit/graphql-action@v2.x
+ - uses: octokit/graphql-action@abaeca7ba4f0325d63b8de7ef943c2418d161b93 # v3.0.0
id: find_team_members
with:
headers: '{"GraphQL-Features": "projects_next_graphql"}'
@@ -119,7 +119,7 @@ jobs:
fi
env:
TEAM: "product"
- - uses: octokit/graphql-action@v2.x
+ - uses: octokit/graphql-action@abaeca7ba4f0325d63b8de7ef943c2418d161b93 # v3.0.0
id: add_to_project
if: steps.any_matching_reviewers.outputs.match == 'true'
with:
diff --git a/.github/workflows/triage-stale.yml b/.github/workflows/triage-stale.yml
index f76cd299cc..c7a9d1ba31 100644
--- a/.github/workflows/triage-stale.yml
+++ b/.github/workflows/triage-stale.yml
@@ -12,15 +12,17 @@ jobs:
issues: write
pull-requests: write
steps:
- - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9
+ - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10
with:
operations-per-run: 100
+
# Flaky test issue closing
- only-issue-labels: "Z-Flaky-Test"
+ any-of-issue-labels: "Z-Flaky-Test-Chrome,Z-Flaky-Test-Firefox,Z-Flaky-Test-Webkit"
days-before-issue-stale: 14
days-before-issue-close: 0
close-issue-message: "This flaky test issue has not been updated in 14 days. It is being closed as presumed resolved."
exempt-issue-labels: "Z-Flaky-Test-Disabled"
+
# Stale PR closing
days-before-pr-stale: 180
days-before-pr-close: 0
diff --git a/.github/workflows/triage-unlabelled.yml b/.github/workflows/triage-unlabelled.yml
index d3bda6df1f..71396be804 100644
--- a/.github/workflows/triage-unlabelled.yml
+++ b/.github/workflows/triage-unlabelled.yml
@@ -5,44 +5,25 @@ on:
types: [unlabeled]
permissions: {}
jobs:
- Move_Unabeled_Issue_On_Project_Board:
+ move_no_longer_needs_info_issues:
name: Move no longer X-Needs-Info issues to Triaged
runs-on: ubuntu-24.04
- permissions:
- repository-projects: read
if: >
- ${{
- !contains(github.event.issue.labels.*.name, 'X-Needs-Info') }}
- env:
- BOARD_NAME: "Issue triage"
- OWNER: ${{ github.repository_owner }}
- REPO: ${{ github.event.repository.name }}
- ISSUE: ${{ github.event.issue.number }}
+ !contains(github.event.issue.labels.*.name, 'X-Needs-Info')
steps:
- - name: Check if issue is already in "${{ env.BOARD_NAME }}"
- run: |
- json=$(curl -s -H 'Content-Type: application/json' -H "Authorization: bearer ${{ secrets.GITHUB_TOKEN }}" -X POST -d '{"query": "query($issue: Int!, $owner: String!, $repo: String!) { repository(owner: $owner, name: $repo) { issue(number: $issue) { projectCards { nodes { project { name } isArchived } } } } } ", "variables" : "{ \"issue\": '${ISSUE}', \"owner\": \"'${OWNER}'\", \"repo\": \"'${REPO}'\" }" }' https://api.github.com/graphql)
- if echo $json | jq '.data.repository.issue.projectCards.nodes | length'; then
- if [[ $(echo $json | jq '.data.repository.issue.projectCards.nodes[0].project.name') =~ "${BOARD_NAME}" ]]; then
- if [[ $(echo $json | jq '.data.repository.issue.projectCards.nodes[0].isArchived') == 'true' ]]; then
- echo "Issue is already in Project '$BOARD_NAME', but is archived - skipping workflow";
- echo "SKIP_ACTION=true" >> $GITHUB_ENV
- else
- echo "Issue is already in Project '$BOARD_NAME', proceeding";
- echo "ALREADY_IN_BOARD=true" >> $GITHUB_ENV
- fi
- else
- echo "Issue is not in project '$BOARD_NAME', cancelling this workflow"
- echo "ALREADY_IN_BOARD=false" >> $GITHUB_ENV
- fi
- fi
- - name: Move issue
- uses: alex-page/github-project-automation-plus@303f24a24c67ce7adf565a07e96720faf126fe36
- if: ${{ env.ALREADY_IN_BOARD == 'true' && env.SKIP_ACTION != 'true' }}
+ - id: set_fields
+ uses: nipe0324/update-project-v2-item-field@c4af58452d1c5a788c1ea4f20e073fa722ec4a6b #v2.0.2
with:
- project: Issue triage
- column: Triaged
- repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
+ project-url: ${{ env.PROJECT_URL }}
+ github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
+ skip-update-script: |
+ const isIssue = item.type === 'ISSUE'
+ const status = item.fieldValues['Status']
+ return !isIssue || status !== 'Needs info'
+ field-name: Status
+ field-value: "Triaged"
+ env:
+ PROJECT_URL: https://github.com/orgs/element-hq/projects/120
remove_Z-Labs_label:
name: Remove Z-Labs label when features behind labs flags are removed
@@ -62,7 +43,7 @@ jobs:
contains(github.event.issue.labels.*.name, 'A-Element-Call')) &&
contains(github.event.issue.labels.*.name, 'Z-Labs')
steps:
- - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
+ - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
github.rest.issues.removeLabel({
diff --git a/.github/workflows/update-jitsi.yml b/.github/workflows/update-jitsi.yml
index da386c544d..67e3fb19c5 100644
--- a/.github/workflows/update-jitsi.yml
+++ b/.github/workflows/update-jitsi.yml
@@ -9,9 +9,9 @@ jobs:
update:
runs-on: ubuntu-24.04
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
with:
cache: "yarn"
node-version: "lts/*"
@@ -23,7 +23,7 @@ jobs:
run: "yarn update:jitsi"
- name: Create Pull Request
- uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7
+ uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
branch: actions/jitsi-update
diff --git a/.github/workflows/update-topics.yaml b/.github/workflows/update-topics.yaml
index 5ee9f2b608..c1fb78e3b8 100644
--- a/.github/workflows/update-topics.yaml
+++ b/.github/workflows/update-topics.yaml
@@ -22,17 +22,17 @@ jobs:
runs-on: ubuntu-24.04
environment: Matrix
steps:
- - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
+ - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
HS_URL: ${{ secrets.BETABOT_HS_URL }}
LOBBY_ROOM_ID: ${{ secrets.ROOM_ID }}
- PUBLIC_ROOM_ID: "!YTvKGNlinIzlkMTVRl:matrix.org"
- ANNOUNCEMENT_ROOM_ID: "!bijaLdadorKgNGtHdA:matrix.org"
+ PUBLIC_DISCUSSION_ROOM_ID: "!xUW4PpAe1CmThA3r2wI8IrgwwsK006-zqWdJCljpd10"
+ ANNOUNCEMENT_ROOM_ID: "!ars5ndgI6IIYZXECiJ-u8YljHNzShJn3nHdB-3rYI2M"
TOKEN: ${{ secrets.BETABOT_ACCESS_TOKEN }}
RELEASE_STATUS: "Release status: ${{ inputs.expected_status }} expected ${{ inputs.expected_date }}"
with:
script: |
- const { HS_URL, TOKEN, RELEASE_STATUS, LOBBY_ROOM_ID, PUBLIC_ROOM_ID, ANNOUNCEMENT_ROOM_ID } = process.env;
+ const { HS_URL, TOKEN, RELEASE_STATUS, LOBBY_ROOM_ID, PUBLIC_DISCUSSION_ROOM_ID, ANNOUNCEMENT_ROOM_ID } = process.env;
const repo = context.repo;
const { data } = await github.rest.repos.getLatestRelease({
@@ -71,13 +71,23 @@ jobs:
const data = await res.json();
console.log(roomId, "got event", data);
+ if (!regex.test(data.topic)) {
+ core.setFailed("Topic format is incorrect for room " + roomId);
+ return;
+ }
+
const topic = data.topic.replace(regex, releaseTopic);
if (topic === data.topic) {
console.log(roomId, "nothing to do");
return;
}
if (data["org.matrix.msc3765.topic"]) {
- data["org.matrix.msc3765.topic"].forEach(d => {
+ data["org.matrix.msc3765.topic"]?.["m.text"].forEach(d => {
+ d.body = d.body.replace(regex, releaseTopic);
+ });
+ }
+ if (data["m.topic"]) {
+ data["m.topic"]?.["m.text"].forEach(d => {
d.body = d.body.replace(regex, releaseTopic);
});
}
@@ -92,12 +102,18 @@ jobs:
});
if (res.ok) {
- console.log(roomId, "topic updated:", topic);
+ const resJson = res.json();
+ if (resJson.errcode) {
+ core.setFailed(`Error updating ${roomId}: ${resJson.error}`);
+ } else {
+ console.log(roomId, "topic updated:", topic);
+ }
} else {
- console.log(roomId, await res.text());
+ const errText = await res.text();
+ core.setFailed(`Error updating ${roomId}: ${errText}`);
}
}
await updateReleaseInTopic(LOBBY_ROOM_ID);
- await updateReleaseInTopic(PUBLIC_ROOM_ID);
+ await updateReleaseInTopic(PUBLIC_DISCUSSION_ROOM_ID);
await updateReleaseInTopic(ANNOUNCEMENT_ROOM_ID);
diff --git a/.gitignore b/.gitignore
index 429b317a4f..3d6d723ac3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,7 +4,6 @@
/key.pem
/lib
/node_modules
-/packages/
/webapp
/.npmrc
/*.log
@@ -31,3 +30,10 @@ electron/pub
/index.html
# version file and tarball created by `npm pack` / `yarn pack`
/git-revision.txt
+
+*storybook.log
+storybook-static
+
+/packages/shared-components/node_modules
+/packages/shared-components/dist
+/packages/shared-components/src/i18nKeys.d.ts
diff --git a/.node-version b/.node-version
index 2bd5a0a98a..a45fd52cc5 100644
--- a/.node-version
+++ b/.node-version
@@ -1 +1 @@
-22
+24
diff --git a/.prettierignore b/.prettierignore
index 46b1ac5b54..abaa4d3df3 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -2,7 +2,6 @@
/dist
/lib
/node_modules
-/packages/
/webapp
/*.log
yarn.lock
diff --git a/.stylelintrc.js b/.stylelintrc.js
index ffc6c345b9..3244d122c5 100644
--- a/.stylelintrc.js
+++ b/.stylelintrc.js
@@ -70,5 +70,13 @@ module.exports = {
],
},
],
+ "property-no-deprecated": [
+ true,
+ {
+ ignoreProperties: ["-webkit-box-orient", "word-wrap"],
+ },
+ ],
+ "nesting-selector-no-missing-scoping-root": null,
+ "no-invalid-position-declaration": null,
},
};
diff --git a/AUTHORS.rst b/AUTHORS.rst
index d027b59c99..a3f3be6586 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -19,3 +19,6 @@ include:
* Thom Cleary (https://github.com/thomcatdotrocks)
Small update for tarball deployment
+
+* Alexander (https://github.com/ioalexander)
+ Save image on CTRL + S shortcut
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 48a721c013..43546c1817 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,444 @@
+Changes in [1.12.7](https://github.com/element-hq/element-web/releases/tag/v1.12.7) (2025-12-16)
+================================================================================================
+## ✨ Features
+
+* Replace legacy icons with compound ([#31424](https://github.com/element-hq/element-web/pull/31424)). Contributed by @t3chguy.
+* Update polls UX to match EX Mobile and improve accessibility ([#31245](https://github.com/element-hq/element-web/pull/31245)). Contributed by @langleyd.
+* Add option to enable read receipt and marker when user interact with UI ([#31353](https://github.com/element-hq/element-web/pull/31353)). Contributed by @florianduros.
+* Introduce a hook to auto dispose view models ([#31178](https://github.com/element-hq/element-web/pull/31178)). Contributed by @MidhunSureshR.
+* Update settings toggles to use consistent design across app. ([#30169](https://github.com/element-hq/element-web/pull/30169)). Contributed by @Half-Shot.
+* Add ability to the room view to hide widgets ([#31400](https://github.com/element-hq/element-web/pull/31400)). Contributed by @langleyd.
+* call: Pass the echo cancellation and noise suppression settings to EC ([#31317](https://github.com/element-hq/element-web/pull/31317)). Contributed by @BillCarsonFr.
+* Tweak rendering of icons for a11y ([#31358](https://github.com/element-hq/element-web/pull/31358)). Contributed by @t3chguy.
+* Implement new `renderNotificationDecoration` from module API ([#31389](https://github.com/element-hq/element-web/pull/31389)). Contributed by @MidhunSureshR.
+* Replace more icons with compound ([#31381](https://github.com/element-hq/element-web/pull/31381)). Contributed by @t3chguy.
+* Replace more icons with compound ([#31378](https://github.com/element-hq/element-web/pull/31378)). Contributed by @t3chguy.
+* ``: Hide `Dismiss` button if `onClose` handler is not provided. ([#31362](https://github.com/element-hq/element-web/pull/31362)). Contributed by @kaylendog.
+* Replace batch of legacy icons with compound design tokens ([#31360](https://github.com/element-hq/element-web/pull/31360)). Contributed by @t3chguy.
+* MSC4380: Invite blocking ([#31268](https://github.com/element-hq/element-web/pull/31268)). Contributed by @richvdh.
+* Tweak rendering of icons for accessibility ([#31346](https://github.com/element-hq/element-web/pull/31346)). Contributed by @t3chguy.
+* Implement a shared `Banner` component. ([#31266](https://github.com/element-hq/element-web/pull/31266)). Contributed by @kaylendog.
+* Allow the Login screen to use the dark theme ([#31293](https://github.com/element-hq/element-web/pull/31293)). Contributed by @richvdh.
+
+## 🐛 Bug Fixes
+
+* [Backport staging] Amend e2e normal icon from lock-solid to info ([#31559](https://github.com/element-hq/element-web/pull/31559)). Contributed by @t3chguy.
+* [Backport staging] Fix CSS specificity causing icon issues in e2e verification ([#31548](https://github.com/element-hq/element-web/pull/31548)). Contributed by @RiotRobot.
+* [Backport staging] Fix e2e icons in CompleteSecurity \& SetupEncryptionBody ([#31522](https://github.com/element-hq/element-web/pull/31522)). Contributed by @RiotRobot.
+* [Backport staging] Remove an extra paragraph in advanced room settings ([#31511](https://github.com/element-hq/element-web/pull/31511)). Contributed by @RiotRobot.
+* [Backport staging] Don't show the key storage out of sync toast when backup disabled ([#31507](https://github.com/element-hq/element-web/pull/31507)). Contributed by @RiotRobot.
+* Fix composer button visibility in contrast colour mode ([#31255](https://github.com/element-hq/element-web/pull/31255)). Contributed by @t3chguy.
+* Ensure correct room version is used and permissions are appropriately sert when creating rooms ([#31464](https://github.com/element-hq/element-web/pull/31464)). Contributed by @Half-Shot.
+* Fix e2e icon rendering ([#31454](https://github.com/element-hq/element-web/pull/31454)). Contributed by @t3chguy.
+* EventIndexer: ensure we add initial checkpoints when the db is first opened ([#31448](https://github.com/element-hq/element-web/pull/31448)). Contributed by @richvdh.
+* Fix `/join ` command failing due to race condition ([#31433](https://github.com/element-hq/element-web/pull/31433)). Contributed by @MidhunSureshR.
+* MessageEventIndexDialog: distinguish indexed rooms ([#31436](https://github.com/element-hq/element-web/pull/31436)). Contributed by @richvdh.
+* Move `EditInPlace` out of `Form` (Fixes: reloading EW on EC url update) ([#31434](https://github.com/element-hq/element-web/pull/31434)). Contributed by @toger5.
+* Fixes issue where cursor would jump to the beginning of the input field after converting Japanese text and pressing Tab ([#31432](https://github.com/element-hq/element-web/pull/31432)). Contributed by @shinaoka.
+* Fix widgets getting stuck in loading states ([#31314](https://github.com/element-hq/element-web/pull/31314)). Contributed by @robintown.
+* Room list: fix room options remaining on room item after mouse leaving ([#31414](https://github.com/element-hq/element-web/pull/31414)). Contributed by @florianduros.
+* Make `RoomList.showMessagePreview` configurable by `config.json` ([#31419](https://github.com/element-hq/element-web/pull/31419)). Contributed by @florianduros.
+* Fix bug which caused app not to load correctly when `force_verification` is enabled ([#31265](https://github.com/element-hq/element-web/pull/31265)). Contributed by @richvdh.
+* Room list: display the menu option on the room list item when clicked/opened ([#31380](https://github.com/element-hq/element-web/pull/31380)). Contributed by @florianduros.
+* Fix handling of SVGs ([#31359](https://github.com/element-hq/element-web/pull/31359)). Contributed by @t3chguy.
+* Fix word wrapping in expanded left panel buttons ([#31377](https://github.com/element-hq/element-web/pull/31377)). Contributed by @t3chguy.
+* Fix aspect ratio on error view background ([#31361](https://github.com/element-hq/element-web/pull/31361)). Contributed by @t3chguy.
+* Fix failure to request persistent storage perms ([#31299](https://github.com/element-hq/element-web/pull/31299)). Contributed by @richvdh.
+* Fix calls sometimes not knowing that they're presented ([#31313](https://github.com/element-hq/element-web/pull/31313)). Contributed by @robintown.
+
+
+Changes in [1.12.6](https://github.com/element-hq/element-web/releases/tag/v1.12.6) (2025-12-03)
+================================================================================================
+This release fixes a bug where 1:1 calling was incorrectly not available if no Element Call focus was set.
+
+## 🐛 Bug Fixes
+
+* Add option to pick call options for voice calls. ([#31413](https://github.com/element-hq/element-web/pull/31413)).
+
+Changes in [1.12.5](https://github.com/element-hq/element-web/releases/tag/v1.12.5) (2025-12-02)
+================================================================================================
+## ✨ Features
+
+* Update Emojibase to v17 ([#31307](https://github.com/element-hq/element-web/pull/31307)). Contributed by @t3chguy.
+* Adds tooltip for compose menu ([#31122](https://github.com/element-hq/element-web/pull/31122)). Contributed by @byteplow.
+* Add option to hide pinned message banner in room view ([#31296](https://github.com/element-hq/element-web/pull/31296)). Contributed by @florianduros.
+* update twemoji to not monochromise emoji with BLACK in their name ([#31281](https://github.com/element-hq/element-web/pull/31281)). Contributed by @ara4n.
+* upgrade to twemoji 17.0.2 and fix #14695 ([#31267](https://github.com/element-hq/element-web/pull/31267)). Contributed by @ara4n.
+* Add options to hide right panel in room view ([#31252](https://github.com/element-hq/element-web/pull/31252)). Contributed by @florianduros.
+* Delayed event management: split endpoints, no auth ([#31183](https://github.com/element-hq/element-web/pull/31183)). Contributed by @AndrewFerr.
+* Support using Element Call for voice calls in DMs ([#30817](https://github.com/element-hq/element-web/pull/30817)). Contributed by @Half-Shot.
+* Improve screen reader accessibility of auth pages ([#31236](https://github.com/element-hq/element-web/pull/31236)). Contributed by @t3chguy.
+* Add posthog tracking for key backup toasts ([#31195](https://github.com/element-hq/element-web/pull/31195)). Contributed by @Half-Shot.
+
+## 🐛 Bug Fixes
+
+* Return to using Fira Code as the default monospace font ([#31302](https://github.com/element-hq/element-web/pull/31302)). Contributed by @ara4n.
+* Fix case of home screen being displayed erroneously ([#31301](https://github.com/element-hq/element-web/pull/31301)). Contributed by @langleyd.
+* Fix message edition and reply when multiple rooms at displayed the same moment ([#31280](https://github.com/element-hq/element-web/pull/31280)). Contributed by @florianduros.
+* Key storage out of sync: reset key backup when needed ([#31279](https://github.com/element-hq/element-web/pull/31279)). Contributed by @uhoreg.
+* Fix invalid events crashing entire room rather than just their tile ([#31256](https://github.com/element-hq/element-web/pull/31256)). Contributed by @t3chguy.
+* Fix expand button of space panel getting cut off at the edges ([#31259](https://github.com/element-hq/element-web/pull/31259)). Contributed by @MidhunSureshR.
+* Fix pill buttons in dialogs ([#31246](https://github.com/element-hq/element-web/pull/31246)). Contributed by @dbkr.
+* Fix blank sections at the top and bottom of the member list when scrolling ([#31198](https://github.com/element-hq/element-web/pull/31198)). Contributed by @langleyd.
+* Fix emoji category selection with keyboard ([#31162](https://github.com/element-hq/element-web/pull/31162)). Contributed by @langleyd.
+
+
+Changes in [1.12.4](https://github.com/element-hq/element-web/releases/tag/v1.12.4) (2025-11-18)
+================================================================================================
+## ✨ Features
+
+* Apply aria-hidden to emoji in SAS verification ([#31204](https://github.com/element-hq/element-web/pull/31204)). Contributed by @t3chguy.
+* Add options to hide header and composer of room view for the module api ([#31095](https://github.com/element-hq/element-web/pull/31095)). Contributed by @florianduros.
+* Experimental Module API Additions ([#30863](https://github.com/element-hq/element-web/pull/30863)). Contributed by @dbkr.
+* Change polls to use fieldset/legend markup ([#31160](https://github.com/element-hq/element-web/pull/31160)). Contributed by @langleyd.
+* Use compound Button styles for Jitsi button ([#31159](https://github.com/element-hq/element-web/pull/31159)). Contributed by @Half-Shot.
+* Add FocusLock to emoji picker ([#31146](https://github.com/element-hq/element-web/pull/31146)). Contributed by @langleyd.
+* Move room name, avatar, and topic to IOpts. ([#30981](https://github.com/element-hq/element-web/pull/30981)). Contributed by @kaylendog.
+* Add a devtool for looking at users and their devices ([#30983](https://github.com/element-hq/element-web/pull/30983)). Contributed by @uhoreg.
+
+## 🐛 Bug Fixes
+
+* Fix room list handling of membership changes ([#31197](https://github.com/element-hq/element-web/pull/31197)). Contributed by @t3chguy.
+* Fix room list unable to be resized when displayed after a module ([#31186](https://github.com/element-hq/element-web/pull/31186)). Contributed by @florianduros.
+* Inhibit keyboard highlights in dialogs when effector is not in focus ([#31181](https://github.com/element-hq/element-web/pull/31181)). Contributed by @t3chguy.
+* Strip mentions from forwarded messages ([#30884](https://github.com/element-hq/element-web/pull/30884)). Contributed by @twassman.
+* Don't allow pin or edit of messages with a send status ([#31158](https://github.com/element-hq/element-web/pull/31158)). Contributed by @langleyd.
+* Hide room header buttons if the room hasn't been created yet. ([#31092](https://github.com/element-hq/element-web/pull/31092)). Contributed by @Half-Shot.
+* Fix screen readers not indicating the emoji picker search field is focused. ([#31128](https://github.com/element-hq/element-web/pull/31128)). Contributed by @langleyd.
+* Fix emoji picker highlight missing when not active element ([#31148](https://github.com/element-hq/element-web/pull/31148)). Contributed by @t3chguy.
+* Add relevant aria attribute for selected emoji in the emoji picker ([#31125](https://github.com/element-hq/element-web/pull/31125)). Contributed by @t3chguy.
+* Fix tooltips within context menu portals being unreliable ([#31129](https://github.com/element-hq/element-web/pull/31129)). Contributed by @t3chguy.
+* Avoid excessive re-render of room list and member list ([#31131](https://github.com/element-hq/element-web/pull/31131)). Contributed by @florianduros.
+* Make emoji picker height responsive. ([#31130](https://github.com/element-hq/element-web/pull/31130)). Contributed by @langleyd.
+* Emoji Picker: Focused emoji does not move with the arrow keys ([#30893](https://github.com/element-hq/element-web/pull/30893)). Contributed by @langleyd.
+* Fix audio player seek bar position ([#31127](https://github.com/element-hq/element-web/pull/31127)). Contributed by @florianduros.
+* Add aria label to emoji picker search ([#31126](https://github.com/element-hq/element-web/pull/31126)). Contributed by @langleyd.
+
+
+Changes in [1.12.3](https://github.com/element-hq/element-web/releases/tag/v1.12.3) (2025-11-04)
+================================================================================================
+## 🦖 Deprecations
+
+* Remove allowVoipWithNoMedia feature flag ([#31087](https://github.com/element-hq/element-web/pull/31087)). Contributed by @Half-Shot.
+
+## ✨ Features
+
+* Change module API to be an instance getter ([#31025](https://github.com/element-hq/element-web/pull/31025)). Contributed by @dbkr.
+
+## 🐛 Bug Fixes
+
+* Show hover elements when keyboard focus is within an event tile ([#31078](https://github.com/element-hq/element-web/pull/31078)). Contributed by @t3chguy.
+* Ensure toolbar navigation pattern works in MessageActionBar ([#31080](https://github.com/element-hq/element-web/pull/31080)). Contributed by @t3chguy.
+* Ensure sent markers are hidden when showing thread summary. ([#31076](https://github.com/element-hq/element-web/pull/31076)). Contributed by @Half-Shot.
+* Fix translation in dev mode ([#31045](https://github.com/element-hq/element-web/pull/31045)). Contributed by @florianduros.
+* Fix sort order in space hierarchy ([#30975](https://github.com/element-hq/element-web/pull/30975)). Contributed by @t3chguy.
+* New Room list: don't display message preview of thread ([#31043](https://github.com/element-hq/element-web/pull/31043)). Contributed by @florianduros.
+* Revert "A11y: move focus to right panel when opened" ([#30999](https://github.com/element-hq/element-web/pull/30999)). Contributed by @florianduros.
+* Fix highlights in messages (or search results) breaking links ([#30264](https://github.com/element-hq/element-web/pull/30264)). Contributed by @bojidar-bg.
+* Add prepare script ([#31030](https://github.com/element-hq/element-web/pull/31030)). Contributed by @dbkr.
+* Fix html exports by adding SDKContext ([#30987](https://github.com/element-hq/element-web/pull/30987)). Contributed by @t3chguy.
+
+
+Changes in [1.12.2](https://github.com/element-hq/element-web/releases/tag/v1.12.2) (2025-10-21)
+================================================================================================
+## ✨ Features
+
+* Room List: Extend the viewport to avoid so many black spots when scrolling the room list ([#30867](https://github.com/element-hq/element-web/pull/30867)). Contributed by @langleyd.
+* Hide calling buttons in room header before a room is created ([#30816](https://github.com/element-hq/element-web/pull/30816)). Contributed by @Half-Shot.
+* Improve invite dialog ui - Part 2 ([#30836](https://github.com/element-hq/element-web/pull/30836)). Contributed by @florianduros.
+
+## 🐛 Bug Fixes
+
+* Fix platform settings race condition and make auto-launch tri-state ([#30977](https://github.com/element-hq/element-web/pull/30977)). Contributed by @t3chguy.
+* Fix: member count in header and member list ([#30982](https://github.com/element-hq/element-web/pull/30982)). Contributed by @florianduros.
+* Fix duration of voice message in timeline ([#30973](https://github.com/element-hq/element-web/pull/30973)). Contributed by @florianduros.
+* Fix voice notes rendering at 00:00 when playback had not begun. ([#30961](https://github.com/element-hq/element-web/pull/30961)). Contributed by @Half-Shot.
+* Improve handling of animated images, add support for AVIF animations ([#30932](https://github.com/element-hq/element-web/pull/30932)). Contributed by @t3chguy.
+* Update key storage toggle when key storage status changes ([#30934](https://github.com/element-hq/element-web/pull/30934)). Contributed by @uhoreg.
+* Fix jitsi widget popout ([#30908](https://github.com/element-hq/element-web/pull/30908)). Contributed by @dbkr.
+* Improve keyboard navigation on invite dialog ([#30930](https://github.com/element-hq/element-web/pull/30930)). Contributed by @florianduros.
+* Prefer UIA flows with supported UIA stages ([#30926](https://github.com/element-hq/element-web/pull/30926)). Contributed by @richvdh.
+* Enhance accessibility of dropdown ([#30928](https://github.com/element-hq/element-web/pull/30928)). Contributed by @florianduros.
+* Improve accessibility of the `\ component ([#30907](https://github.com/element-hq/element-web/pull/30907)). Contributed by @MidhunSureshR.
+
+
+Changes in [1.12.1](https://github.com/element-hq/element-web/releases/tag/v1.12.1) (2025-10-07)
+================================================================================================
+## ✨ Features
+
+* New Room List: Change the order of filters to match those on mobile ([#30905](https://github.com/element-hq/element-web/pull/30905)). Contributed by @langleyd.
+* New Room List: Don't clear filters on space change ([#30903](https://github.com/element-hq/element-web/pull/30903)). Contributed by @langleyd.
+* Add release announcement for the sounds ([#30900](https://github.com/element-hq/element-web/pull/30900)). Contributed by @langleyd.
+* Rich Text Editor: Add emoji suggestion support ([#30873](https://github.com/element-hq/element-web/pull/30873)). Contributed by @langleyd.
+* feat: Disable session lock when running in element-desktop ([#30643](https://github.com/element-hq/element-web/pull/30643)). Contributed by @kaylendog.
+* Improve invite dialog ui - Part 1 ([#30764](https://github.com/element-hq/element-web/pull/30764)). Contributed by @florianduros.
+* Update Message Sound for Element ([#30804](https://github.com/element-hq/element-web/pull/30804)). Contributed by @beatdemon.
+* Add new and improved ringtone ([#30761](https://github.com/element-hq/element-web/pull/30761)). Contributed by @Half-Shot.
+* Disable RTE formatting buttons when the content contains a slash command ([#30802](https://github.com/element-hq/element-web/pull/30802)). Contributed by @langleyd.
+
+## 🐛 Bug Fixes
+
+* New Room List: Improve robustness of keyboard navigation ([#30888](https://github.com/element-hq/element-web/pull/30888)). Contributed by @langleyd.
+* Fix a11y issue on list in invite dialog ([#30878](https://github.com/element-hq/element-web/pull/30878)). Contributed by @florianduros.
+* Switch Export and Import Icons to match intuition ([#30805](https://github.com/element-hq/element-web/pull/30805)). Contributed by @micartey.
+* Hide breadcrumb option when new room list is enabled ([#30869](https://github.com/element-hq/element-web/pull/30869)). Contributed by @florianduros.
+* Avoid creating multiple call objects for the same widget ([#30839](https://github.com/element-hq/element-web/pull/30839)). Contributed by @robintown.
+* Add a test for #29882, which is fixed by matrix-org/matrix-js-sdk#5016 ([#30835](https://github.com/element-hq/element-web/pull/30835)). Contributed by @andybalaam.
+* fix: use `help_encryption_url` of config instead of hardcoded `https://element.io/help#encryption5` ([#30746](https://github.com/element-hq/element-web/pull/30746)). Contributed by @florianduros.
+* Fix html export when feature\_jump\_to\_date is enabled ([#30828](https://github.com/element-hq/element-web/pull/30828)). Contributed by @langleyd.
+* Fix #30439: "Forgot recovery key" should go to "reset" ([#30771](https://github.com/element-hq/element-web/pull/30771)). Contributed by @andybalaam.
+
+
+Changes in [1.12.0](https://github.com/element-hq/element-web/releases/tag/v1.12.0) (2025-09-23)
+================================================================================================
+## 🦖 Deprecations
+
+* Remove remaining support for outdated .well-known settings ([#30702](https://github.com/element-hq/element-web/pull/30702)). Contributed by @richvdh.
+
+## ✨ Features
+
+* Add decline button to call notification toast (use new notification event) ([#30729](https://github.com/element-hq/element-web/pull/30729)). Contributed by @toger5.
+* Use the new room list by default ([#30640](https://github.com/element-hq/element-web/pull/30640)). Contributed by @langleyd.
+* "Verify this device" redesign ([#30596](https://github.com/element-hq/element-web/pull/30596)). Contributed by @uhoreg.
+* Set Element Call "intents" when starting and answering DM calls. ([#30730](https://github.com/element-hq/element-web/pull/30730)). Contributed by @Half-Shot.
+* Add axe compliance for new room list ([#30700](https://github.com/element-hq/element-web/pull/30700)). Contributed by @langleyd.
+* Stop ringing and remove toast if another device answers a RTC call. ([#30728](https://github.com/element-hq/element-web/pull/30728)). Contributed by @Half-Shot.
+* Automatically adjust history visibility when making a room private ([#30713](https://github.com/element-hq/element-web/pull/30713)). Contributed by @Half-Shot.
+* Release announcement for new room list ([#30675](https://github.com/element-hq/element-web/pull/30675)). Contributed by @dbkr.
+
+## 🐛 Bug Fixes
+
+* [Backport staging] Room list: make the filter resize correctly ([#30795](https://github.com/element-hq/element-web/pull/30795)). Contributed by @RiotRobot.
+* [Backport staging] Avoid flicker of the room list filter on resize ([#30794](https://github.com/element-hq/element-web/pull/30794)). Contributed by @RiotRobot.
+* Don't show release announcements while toasts are displayed ([#30770](https://github.com/element-hq/element-web/pull/30770)). Contributed by @dbkr.
+* Fix enabling key backup not working if there is an untrusted key backup ([#30707](https://github.com/element-hq/element-web/pull/30707)). Contributed by @Half-Shot.
+* Force `preload` to be false when setting an intent on an Element Call. ([#30759](https://github.com/element-hq/element-web/pull/30759)). Contributed by @Half-Shot.
+* Fix handling of 413 server response when uploading media ([#30737](https://github.com/element-hq/element-web/pull/30737)). Contributed by @hughns.
+* Make landmark navigation work with new room list ([#30747](https://github.com/element-hq/element-web/pull/30747)). Contributed by @dbkr.
+* Prevent voice message from displaying spurious errors ([#30736](https://github.com/element-hq/element-web/pull/30736)). Contributed by @florianduros.
+* Align default avatar and fix colors in composer pills ([#30739](https://github.com/element-hq/element-web/pull/30739)). Contributed by @florianduros.
+* Use configured URL for link to desktop app in message search settings ([#30742](https://github.com/element-hq/element-web/pull/30742)). Contributed by @t3chguy.
+* Fix history visibility when creating space rooms ([#30745](https://github.com/element-hq/element-web/pull/30745)). Contributed by @dbkr.
+* Check HTML-encoded quotes when handling translations for embedded pages (such as welcome.html) ([#30743](https://github.com/element-hq/element-web/pull/30743)). Contributed by @Half-Shot.
+* Fix local room encryption status always not enabled ([#30461](https://github.com/element-hq/element-web/pull/30461)). Contributed by @BillCarsonFr.
+* fix: make url in topic in room intro clickable ([#30686](https://github.com/element-hq/element-web/pull/30686)). Contributed by @florianduros.
+* Block change recovery key button while a change is ongoing. ([#30664](https://github.com/element-hq/element-web/pull/30664)). Contributed by @Half-Shot.
+* Hide advanced settings during room creation when `UIFeature.advancedSettings=false` ([#30684](https://github.com/element-hq/element-web/pull/30684)). Contributed by @florianduros.
+* A11y: improve accessibility of pinned messages ([#30558](https://github.com/element-hq/element-web/pull/30558)). Contributed by @florianduros.
+
+
+Changes in [1.11.112](https://github.com/element-hq/element-web/releases/tag/v1.11.112) (2025-09-16)
+====================================================================================================
+Fix [CVE-2025-59161](https://www.cve.org/CVERecord?id=CVE-2025-59161) / [GHSA-m6c8-98f4-75rr](https://github.com/element-hq/element-web/security/advisories/GHSA-m6c8-98f4-75rr)
+
+
+Changes in [1.11.111](https://github.com/element-hq/element-web/releases/tag/v1.11.111) (2025-09-10)
+====================================================================================================
+## ✨ Features
+
+* Do not hide media from your own user by default ([#29797](https://github.com/element-hq/element-web/pull/29797)). Contributed by @Half-Shot.
+* Remember whether sidebar is shown for calls when switching rooms ([#30262](https://github.com/element-hq/element-web/pull/30262)). Contributed by @bojidar-bg.
+* Open the proper integration settings on integrations disabled error ([#30538](https://github.com/element-hq/element-web/pull/30538)). Contributed by @Half-Shot.
+* Show a "progress" dialog while invites are being sent ([#30561](https://github.com/element-hq/element-web/pull/30561)). Contributed by @richvdh.
+* Move the room list to the new ListView(backed by react-virtuoso) ([#30515](https://github.com/element-hq/element-web/pull/30515)). Contributed by @langleyd.
+
+## 🐛 Bug Fixes
+
+* [Backport staging] Ensure container starts if it is mounted with an empty /modules directory. ([#30705](https://github.com/element-hq/element-web/pull/30705)). Contributed by @RiotRobot.
+* Fix room joining over federation not specifying vias or using aliases ([#30641](https://github.com/element-hq/element-web/pull/30641)). Contributed by @t3chguy.
+* Fix stable-suffixed MSC4133 support ([#30649](https://github.com/element-hq/element-web/pull/30649)). Contributed by @dbkr.
+* Fix i18n of message when a setting is disabled ([#30646](https://github.com/element-hq/element-web/pull/30646)). Contributed by @dbkr.
+* ListView should not handle the arrow keys if there is a modifier applied ([#30633](https://github.com/element-hq/element-web/pull/30633)). Contributed by @langleyd.
+* Make BaseDialog's div keyboard focusable and fix test. ([#30631](https://github.com/element-hq/element-web/pull/30631)). Contributed by @langleyd.
+* Fix: Allow triple-click text selection to flow around pills ([#30349](https://github.com/element-hq/element-web/pull/30349)). Contributed by @AlirezaMrtz.
+* Watch for a 'join' action to know when the call is connected ([#29492](https://github.com/element-hq/element-web/pull/29492)). Contributed by @robintown.
+* Fix: add missing tooltip and aria-label to lock icon next to composer ([#30623](https://github.com/element-hq/element-web/pull/30623)). Contributed by @florianduros.
+* Don't render context menu when scrolling ([#30613](https://github.com/element-hq/element-web/pull/30613)). Contributed by @langleyd.
+
+
+Changes in [1.11.110](https://github.com/element-hq/element-web/releases/tag/v1.11.110) (2025-08-27)
+====================================================================================================
+## ✨ Features
+
+* Hide recovery key when re-entering it while creating or changing it ([#30499](https://github.com/element-hq/element-web/pull/30499)). Contributed by @andybalaam.
+* Add `?no_universal_links=true` to OIDC url so EX doesn't try to handle it ([#29439](https://github.com/element-hq/element-web/pull/29439)). Contributed by @t3chguy.
+* Show a blue lock for unencrypted rooms and hide the grey shield for encrypted rooms ([#30440](https://github.com/element-hq/element-web/pull/30440)). Contributed by @langleyd.
+* Add support for Module API 1.4 ([#30185](https://github.com/element-hq/element-web/pull/30185)). Contributed by @t3chguy.
+* MVVM - Introduce some helpers for snapshot management ([#30398](https://github.com/element-hq/element-web/pull/30398)). Contributed by @MidhunSureshR.
+
+## 🐛 Bug Fixes
+
+* A11y: move focus to right panel when opened ([#30553](https://github.com/element-hq/element-web/pull/30553)). Contributed by @florianduros.
+* Fix e2e warning icon should be white ([#30539](https://github.com/element-hq/element-web/pull/30539)). Contributed by @florianduros.
+* Remove NoOneHere disabled reason. ([#30524](https://github.com/element-hq/element-web/pull/30524)). Contributed by @toger5.
+* Fix downloading files with authenticated media API ([#30520](https://github.com/element-hq/element-web/pull/30520)). Contributed by @t3chguy.
+* Fix call permissions check confusion around element call ([#30521](https://github.com/element-hq/element-web/pull/30521)). Contributed by @t3chguy.
+* Fix line wrap around emoji verification ([#30523](https://github.com/element-hq/element-web/pull/30523)). Contributed by @t3chguy.
+* Don't highlight redacted events ([#30519](https://github.com/element-hq/element-web/pull/30519)). Contributed by @t3chguy.
+* Fix matrix.to links not being handled in the app ([#30522](https://github.com/element-hq/element-web/pull/30522)). Contributed by @t3chguy.
+* Fix issue of new room list taking up the full width ([#30459](https://github.com/element-hq/element-web/pull/30459)). Contributed by @langleyd.
+* Fix widget persistence in React development mode ([#30509](https://github.com/element-hq/element-web/pull/30509)). Contributed by @robintown.
+* Fix widget initialization in React development mode ([#30463](https://github.com/element-hq/element-web/pull/30463)). Contributed by @robintown.
+
+
+Changes in [1.11.109](https://github.com/element-hq/element-web/releases/tag/v1.11.109) (2025-08-11)
+====================================================================================================
+This release supports the upcoming v12 ("hydra") Matrix room version and is necessary to view and participate in these rooms.
+
+## ✨ Features
+
+* [Backport staging] Allow /upgraderoom command without developer mode enabled ([#30529](https://github.com/element-hq/element-web/pull/30529)). Contributed by @RiotRobot.
+* [Backport staging] Support for creator/owner power level ([#30526](https://github.com/element-hq/element-web/pull/30526)). Contributed by @RiotRobot.
+* New room list: change icon and label of menu item for to start a DM ([#30470](https://github.com/element-hq/element-web/pull/30470)). Contributed by @florianduros.
+* Implement the member list with virtuoso ([#29869](https://github.com/element-hq/element-web/pull/29869)). Contributed by @langleyd.
+* Add labs option for history sharing on invite ([#30313](https://github.com/element-hq/element-web/pull/30313)). Contributed by @richvdh.
+* Bump wysiwyg to 2.39.0 adding support for pasting rich text content in the Rich Text Edtior ([#30421](https://github.com/element-hq/element-web/pull/30421)). Contributed by @langleyd.
+* Support `EventShieldReason.MISMATCHED_SENDER` ([#30403](https://github.com/element-hq/element-web/pull/30403)). Contributed by @richvdh.
+* Change unencrypted and public pills to blue ([#30399](https://github.com/element-hq/element-web/pull/30399)). Contributed by @florianduros.
+* Change color of public room icon ([#30390](https://github.com/element-hq/element-web/pull/30390)). Contributed by @florianduros.
+* Script for updating storybook screenshots ([#30340](https://github.com/element-hq/element-web/pull/30340)). Contributed by @dbkr.
+* Add toggle to hide empty state in devtools ([#30352](https://github.com/element-hq/element-web/pull/30352)). Contributed by @toger5.
+
+## 🐛 Bug Fixes
+
+* [Backport staging] Use userId to filter users in non-federated rooms when showing the InviteDialog ([#30537](https://github.com/element-hq/element-web/pull/30537)). Contributed by @RiotRobot.
+* [Backport staging] Catch error when encountering invalid m.room.pinned\_events event ([#30536](https://github.com/element-hq/element-web/pull/30536)). Contributed by @RiotRobot.
+* Update for compatibility with v12 rooms ([#30452](https://github.com/element-hq/element-web/pull/30452)). Contributed by @dbkr.
+* New room list: fix tooltip on presence ([#30474](https://github.com/element-hq/element-web/pull/30474)). Contributed by @florianduros.
+* New room list: add tooltip for presence and room status ([#30472](https://github.com/element-hq/element-web/pull/30472)). Contributed by @florianduros.
+* Fix: Clicking on an item in the member list causes it to scroll to the top rather than show the profile view ([#30455](https://github.com/element-hq/element-web/pull/30455)). Contributed by @langleyd.
+* Put the 'decrypting' tooltip back ([#30446](https://github.com/element-hq/element-web/pull/30446)). Contributed by @dbkr.
+* Use server name explicitly for via. ([#30362](https://github.com/element-hq/element-web/pull/30362)). Contributed by @Half-Shot.
+* fix: replace hardcoded string in poll history dialog ([#30402](https://github.com/element-hq/element-web/pull/30402)). Contributed by @florianduros.
+* fix: replace hardcoded string on qr code back button ([#30401](https://github.com/element-hq/element-web/pull/30401)). Contributed by @florianduros.
+* Fix color of icon button with outline ([#30361](https://github.com/element-hq/element-web/pull/30361)). Contributed by @florianduros.
+
+
+Changes in [1.11.108](https://github.com/element-hq/element-web/releases/tag/v1.11.108) (2025-07-30)
+====================================================================================================
+## 🐛 Bug Fixes
+
+* [Backport staging] Fix downloaded attachments not being decrypted ([#30434](https://github.com/element-hq/element-web/pull/30434)). Contributed by @RiotRobot.
+
+
+Changes in [1.11.107](https://github.com/element-hq/element-web/releases/tag/v1.11.107) (2025-07-29)
+====================================================================================================
+## ✨ Features
+
+* Message preview should show tooltip with the full message on hover ([#30265](https://github.com/element-hq/element-web/pull/30265)). Contributed by @MidhunSureshR.
+* Support rendering notification badges on platforms that do their own icon overlays ([#30315](https://github.com/element-hq/element-web/pull/30315)). Contributed by @Half-Shot.
+* Add SubscriptionViewModel base class ([#30297](https://github.com/element-hq/element-web/pull/30297)). Contributed by @dbkr.
+* Enhancement: Save image on CTRL+S ([#30330](https://github.com/element-hq/element-web/pull/30330)). Contributed by @ioalexander.
+* Add quote functionality to MessageContextMenu (#29893) ([#30323](https://github.com/element-hq/element-web/pull/30323)). Contributed by @AlirezaMrtz.
+* Initial structure for shared component views ([#30216](https://github.com/element-hq/element-web/pull/30216)). Contributed by @dbkr.
+
+## 🐛 Bug Fixes
+
+* [Backport staging] Fix e2e shield being invisible in white mode for encrypted room ([#30411](https://github.com/element-hq/element-web/pull/30411)). Contributed by @RiotRobot.
+* Force ED titlebar color for new room list ([#30332](https://github.com/element-hq/element-web/pull/30332)). Contributed by @florianduros.
+* Add a background color to left panel for macos titlebar in element desktop ([#30328](https://github.com/element-hq/element-web/pull/30328)). Contributed by @florianduros.
+* Fix: Prevent page refresh on Enter key in right panel member search ([#30312](https://github.com/element-hq/element-web/pull/30312)). Contributed by @AlirezaMrtz.
+
+
+Changes in [1.11.106](https://github.com/element-hq/element-web/releases/tag/v1.11.106) (2025-07-15)
+====================================================================================================
+## ✨ Features
+
+* [Backport staging] Fix e2e icon colour ([#30304](https://github.com/element-hq/element-web/pull/30304)). Contributed by @RiotRobot.
+* Add support for module message hint `allowDownloadingMedia` ([#30252](https://github.com/element-hq/element-web/pull/30252)). Contributed by @Half-Shot.
+* Update the mobile\_guide page to the new design and link out to Element X by default. ([#30172](https://github.com/element-hq/element-web/pull/30172)). Contributed by @pixlwave.
+* Filter settings exported when rageshaking ([#30236](https://github.com/element-hq/element-web/pull/30236)). Contributed by @Half-Shot.
+* Allow Element Call to learn the room name ([#30213](https://github.com/element-hq/element-web/pull/30213)). Contributed by @robintown.
+
+## 🐛 Bug Fixes
+
+* [Backport staging] Fix missing image download button ([#30322](https://github.com/element-hq/element-web/pull/30322)). Contributed by @RiotRobot.
+* Fix transparent verification checkmark in dark mode ([#30235](https://github.com/element-hq/element-web/pull/30235)). Contributed by @Banbuii.
+* Fix logic in DeviceListener ([#30230](https://github.com/element-hq/element-web/pull/30230)). Contributed by @uhoreg.
+* Disable file drag-and-drop if insufficient permissions ([#30186](https://github.com/element-hq/element-web/pull/30186)). Contributed by @t3chguy.
+
+
+Changes in [1.11.105](https://github.com/element-hq/element-web/releases/tag/v1.11.105) (2025-07-01)
+====================================================================================================
+## ✨ Features
+
+* New room list: add context menu to room list item ([#29952](https://github.com/element-hq/element-web/pull/29952)). Contributed by @florianduros.
+* Support for custom message components via Module API ([#30074](https://github.com/element-hq/element-web/pull/30074)). Contributed by @Half-Shot.
+* Prompt users to set up recovery ([#30075](https://github.com/element-hq/element-web/pull/30075)). Contributed by @uhoreg.
+* Update `IconButton` colors ([#30124](https://github.com/element-hq/element-web/pull/30124)). Contributed by @florianduros.
+* New room list: filter list can be collapsed ([#29992](https://github.com/element-hq/element-web/pull/29992)). Contributed by @florianduros.
+* Show `EmptyRoomListView` when low priority filter matches zero rooms ([#30122](https://github.com/element-hq/element-web/pull/30122)). Contributed by @MidhunSureshR.
+
+## 🐛 Bug Fixes
+
+* Fix untranslatable string "People" in notifications beta ([#30165](https://github.com/element-hq/element-web/pull/30165)). Contributed by @t3chguy.
+* Force verification even after logging in via delegate ([#30141](https://github.com/element-hq/element-web/pull/30141)). Contributed by @andybalaam.
+* Hide add integrations button based on UIComponent.AddIntegrations ([#30140](https://github.com/element-hq/element-web/pull/30140)). Contributed by @t3chguy.
+* Use nav for new room list and label sections ([#30134](https://github.com/element-hq/element-web/pull/30134)). Contributed by @dbkr.
+* Spacestore should emit event after rebuilding home space ([#30132](https://github.com/element-hq/element-web/pull/30132)). Contributed by @MidhunSureshR.
+* Handle m.room.pinned\_events being invalid ([#30129](https://github.com/element-hq/element-web/pull/30129)). Contributed by @t3chguy.
+
+
+Changes in [1.11.104](https://github.com/element-hq/element-web/releases/tag/v1.11.104) (2025-06-17)
+====================================================================================================
+## ✨ Features
+
+* Update the mobile\_guide page to the new design. ([#30006](https://github.com/element-hq/element-web/pull/30006)). Contributed by @pixlwave.
+* Provide a devtool for manually verifying other devices ([#30094](https://github.com/element-hq/element-web/pull/30094)). Contributed by @andybalaam.
+* Implement MSC4155: Invite filtering ([#29603](https://github.com/element-hq/element-web/pull/29603)). Contributed by @Half-Shot.
+* Add low priority avatar decoration to room tile ([#30065](https://github.com/element-hq/element-web/pull/30065)). Contributed by @MidhunSureshR.
+* Add ability to prevent window content being captured by other apps (Desktop) ([#30098](https://github.com/element-hq/element-web/pull/30098)). Contributed by @t3chguy.
+* New room list: move message preview in user settings ([#30023](https://github.com/element-hq/element-web/pull/30023)). Contributed by @florianduros.
+* New room list: change room options icon ([#30029](https://github.com/element-hq/element-web/pull/30029)). Contributed by @florianduros.
+* RoomListStore: Sort low priority rooms to the bottom of the list ([#30070](https://github.com/element-hq/element-web/pull/30070)). Contributed by @MidhunSureshR.
+* Add low priority filter pill to the room list UI ([#30060](https://github.com/element-hq/element-web/pull/30060)). Contributed by @MidhunSureshR.
+* New room list: remove color gradient in space panel ([#29721](https://github.com/element-hq/element-web/pull/29721)). Contributed by @florianduros.
+* /share?msg=foo endpoint using forward message dialog ([#29874](https://github.com/element-hq/element-web/pull/29874)). Contributed by @ara4n.
+
+## 🐛 Bug Fixes
+
+* Do not send empty auth when setting up cross-signing keys ([#29914](https://github.com/element-hq/element-web/pull/29914)). Contributed by @gnieto.
+* Settings: flip local video feed by default ([#29501](https://github.com/element-hq/element-web/pull/29501)). Contributed by @jbtrystram.
+* AccessSecretStorageDialog: various fixes ([#30093](https://github.com/element-hq/element-web/pull/30093)). Contributed by @richvdh.
+* AccessSecretStorageDialog: fix inability to enter recovery key ([#30090](https://github.com/element-hq/element-web/pull/30090)). Contributed by @richvdh.
+* Fix failure to upload thumbnail causing image to send as file ([#30086](https://github.com/element-hq/element-web/pull/30086)). Contributed by @t3chguy.
+* Low priority menu item should be a toggle ([#30071](https://github.com/element-hq/element-web/pull/30071)). Contributed by @MidhunSureshR.
+* Add sanity checks to prevent users from ignoring themselves ([#30079](https://github.com/element-hq/element-web/pull/30079)). Contributed by @MidhunSureshR.
+* Fix issue with duplicate images ([#30073](https://github.com/element-hq/element-web/pull/30073)). Contributed by @fatlewis.
+* Handle errors returned from Seshat ([#30083](https://github.com/element-hq/element-web/pull/30083)). Contributed by @richvdh.
+
+
+Changes in [1.11.103](https://github.com/element-hq/element-web/releases/tag/v1.11.103) (2025-06-10)
+====================================================================================================
+## 🐛 Bug Fixes
+
++ Check the sender of an event matches owner of session, preventing sender spoofing by homeserver owners.
+[13c1d20](https://github.com/matrix-org/matrix-rust-sdk/commit/13c1d2048286bbabf5e7bc6b015aafee98f04d55) (High, [GHSA-x958-rvg6-956w](https://github.com/matrix-org/matrix-rust-sdk/security/advisories/GHSA-x958-rvg6-956w)).
+
+Changes in [1.11.102](https://github.com/element-hq/element-web/releases/tag/v1.11.102) (2025-06-03)
+====================================================================================================
+## ✨ Features
+
+* EW: Modernize the recovery key input modal ([#29819](https://github.com/element-hq/element-web/pull/29819)). Contributed by @uhoreg.
+* New room list: move secondary filters into primary filters ([#29972](https://github.com/element-hq/element-web/pull/29972)). Contributed by @florianduros.
+* Prompt the user when key storage is unexpectedly off ([#29912](https://github.com/element-hq/element-web/pull/29912)). Contributed by @andybalaam.
+* New room list: move sort menu in room list header ([#29983](https://github.com/element-hq/element-web/pull/29983)). Contributed by @florianduros.
+* New room list: rework spacing of room list item ([#29965](https://github.com/element-hq/element-web/pull/29965)). Contributed by @florianduros.
+* RLS: Remove forgotten room from skiplist ([#29933](https://github.com/element-hq/element-web/pull/29933)). Contributed by @MidhunSureshR.
+* Add room list sorting ([#29951](https://github.com/element-hq/element-web/pull/29951)). Contributed by @dbkr.
+* Don't use the minimised width(68px) on the new room list ([#29778](https://github.com/element-hq/element-web/pull/29778)). Contributed by @langleyd.
+
+## 🐛 Bug Fixes
+
+* [Backport staging] Close call options popup menu when option has been selected ([#30054](https://github.com/element-hq/element-web/pull/30054)). Contributed by @RiotRobot.
+* RoomListStoreV3: Only add new rooms that pass `VisibilityProvider` check ([#29974](https://github.com/element-hq/element-web/pull/29974)). Contributed by @MidhunSureshR.
+* Re-order primary filters ([#29957](https://github.com/element-hq/element-web/pull/29957)). Contributed by @dbkr.
+* Fix leaky CSS adding `!` to all H1 elements ([#29964](https://github.com/element-hq/element-web/pull/29964)). Contributed by @t3chguy.
+* Fix extensions panel style ([#29273](https://github.com/element-hq/element-web/pull/29273)). Contributed by @langleyd.
+* Fix state events being hidden from widgets in read\_events actions ([#29954](https://github.com/element-hq/element-web/pull/29954)). Contributed by @robintown.
+* Remove old filter test ([#29963](https://github.com/element-hq/element-web/pull/29963)). Contributed by @dbkr.
+
+
Changes in [1.11.101](https://github.com/element-hq/element-web/releases/tag/v1.11.101) (2025-05-20)
====================================================================================================
## ✨ Features
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index fd94f18f85..3ec59b2eff 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,6 +2,11 @@
Everyone is welcome to contribute code to Element Web, provided that they are willing to license their contributions to Element under a [Contributor License Agreement](https://cla-assistant.io/element-hq/element-web) (CLA). This ensures that their contribution will be made available under an OSI-approved open-source license, currently licensed under Affero General Public License v3 (AGPLv3) or General Public License v3 (GPLv3) at your choice.
+If you're contributing, or thinking about contributing, please come & chat to
+us in our development room, [#element-dev](https://matrix.to/#/#element-dev:matrix.org).
+This is the best place to ask questions about the code, how to work on the project
+or whether a change is likely to be accepted.
+
## How to contribute
The preferred and easiest way to contribute changes to the project is to fork
diff --git a/Dockerfile b/Dockerfile
index a1813cf66d..5ce43a7c4f 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,7 +1,7 @@
-# syntax=docker.io/docker/dockerfile:1.16-labs@sha256:bb5e2b225985193779991f3256d1901a0b3e6a0b284c7bffa0972064f4a6d458
+# syntax=docker.io/docker/dockerfile:1.20-labs@sha256:dbcde2ebc4abc8bb5c3c499b9c9a6876842bf5da243951cd2697f921a7aeb6a9
# Builder
-FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:f16d8e8af67bb6361231e932b8b3e7afa040cbfed181719a450b02c3821b26c1 AS builder
+FROM --platform=$BUILDPLATFORM node:24-bullseye@sha256:5583cbe5d3347db372d9a9100eba272b548ca1f53246b9b769334bcd0eef2c26 AS builder
# Support custom branch of the js-sdk. This also helps us build images of element-web develop.
ARG USE_CUSTOM_SDKS=false
@@ -19,7 +19,7 @@ RUN /src/scripts/docker-package.sh
RUN cp /src/config.sample.json /src/webapp/config.json
# App
-FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:2acffd86b1bdefb8fa6b48b6e9aadf75430e8ab9c43c54c515ea7df77897f987
+FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:a6bec37058b9047ece799c01d98dc6d5aa0542b6583cc69f187652f91331a752
# Need root user to install packages & manipulate the usr directory
USER root
diff --git a/README.md b/README.md
index 0f8a721f90..6f6e3172fa 100644
--- a/README.md
+++ b/README.md
@@ -27,7 +27,7 @@ Element has several tiers of support for different environments:
- Best effort
- Definition:
- Issues **accepted**, regressions **do not block** the release
- - The wider Element Products(including Element Call and the Enterprise Server Suite) do still not officially support these browsers.
+ - The wider Element Products (including Element Call and the Enterprise Server Suite) do still not officially support these browsers.
- The element web project and its contributors should keep the client functioning and gracefully degrade where other sibling features (E.g. Element Call) may not function.
- Last major release of Firefox ESR and Chrome/Edge Extended Stable
- Community Supported
diff --git a/book.toml b/book.toml
index 77fa4bfd5d..5139a801fb 100644
--- a/book.toml
+++ b/book.toml
@@ -4,7 +4,6 @@
title = "Element Web & Desktop"
authors = ["New Vector Ltd.", "The Matrix.org Foundation C.I.C."]
language = "en"
-multilingual = false
# The directory that documentation files are stored in
src = "docs"
diff --git a/code_style.md b/code_style.md
index 9f0501ccd8..6e7289d22e 100644
--- a/code_style.md
+++ b/code_style.md
@@ -79,7 +79,7 @@ Unless otherwise specified, the following applies to all code:
11. If a variable is not receiving a value on declaration, its type must be defined.
```typescript
- let errorMessage: Optional;
+ let errorMessage: string;
```
12. Objects can use shorthand declarations, including mixing of types.
@@ -127,7 +127,6 @@ Unless otherwise specified, the following applies to all code:
2. "Conflicted" typically refers to a getter which wants the same name as the underlying variable.
20. Prefer readonly members over getters backed by a variable, unless an internal setter is required.
21. Prefer Interfaces for object definitions, and types for parameter-value-only declarations.
-
1. Note that an explicit type is optional if not expected to be used outside of the function call,
unlike in this example:
@@ -161,7 +160,6 @@ Unless otherwise specified, the following applies to all code:
28. Export only what can be reused.
29. Prefer a type like `Optional` (`type Optional = T | null | undefined`) instead
of truly optional parameters.
-
1. A notable exception is when the likelihood of a bug is minimal, such as when a function
takes an argument that is more often not required than required. An example where the
`?` operator is inappropriate is when taking a room ID: typically the caller should
@@ -260,7 +258,6 @@ Inheriting all the rules of TypeScript, the following additionally apply:
12. Interdependence between stores should be kept to a minimum. Break functions and constants out to utilities
if at all possible.
13. A component should only use CSS class names in line with the component name.
-
1. When knowingly using a class name from another component, document it with a [comment](#comments).
14. Curly braces within JSX should be padded with a space, however properties on those components should not.
@@ -388,7 +385,6 @@ Note: We use PostCSS + some plugins to process our styles. It looks like SCSS, b
properties should be clearly documented.
4. Inside a function, there is no need to comment every line, but consider:
-
- before a particular multiline section of code within the function, give an overview of what it does,
to make it easier for a reader to follow the flow through the function as a whole.
- if it is anything less than obvious, explain _why_ we are doing a particular operation, with particular emphasis
diff --git a/config.sample.json b/config.sample.json
index 973bb67a02..1f8030080d 100644
--- a/config.sample.json
+++ b/config.sample.json
@@ -20,8 +20,7 @@
"https://scalar.vector.im/_matrix/integrations/v1",
"https://scalar.vector.im/api",
"https://scalar-staging.vector.im/_matrix/integrations/v1",
- "https://scalar-staging.vector.im/api",
- "https://scalar-staging.riot.im/scalar/api"
+ "https://scalar-staging.vector.im/api"
],
"default_widget_container_height": 280,
"default_country_code": "GB",
diff --git a/declaration.d.ts b/declaration.d.ts
new file mode 100644
index 0000000000..928c567c31
--- /dev/null
+++ b/declaration.d.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+declare module "*.module.css";
diff --git a/developer_guide.md b/developer_guide.md
index fa4bb9a239..2185a43a63 100644
--- a/developer_guide.md
+++ b/developer_guide.md
@@ -109,7 +109,7 @@ yarn test
### End-to-End tests
-See [matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk/#end-to-end-tests) for how to run the end-to-end tests.
+See [`docs/playwright.md`](./docs/playwright.md) for how to run the end-to-end tests.
## General github guidelines
diff --git a/docker/docker-entrypoint.d/18-load-element-modules.sh b/docker/docker-entrypoint.d/18-load-element-modules.sh
index 235c4edcf2..2230fea33e 100755
--- a/docker/docker-entrypoint.d/18-load-element-modules.sh
+++ b/docker/docker-entrypoint.d/18-load-element-modules.sh
@@ -14,10 +14,9 @@ entrypoint_log() {
mkdir -p /tmp/element-web-config
cp /app/config*.json /tmp/element-web-config/
-# If there are modules to be loaded
-if [ -d "/modules" ]; then
+# If the module directory exists AND the module directory has modules in it
+if [ -d "/modules" ] && [ "$( ls -A '/modules' )" ]; then
cd /modules
-
for MODULE in *
do
# If the module has a package.json, use its main field as the entrypoint
diff --git a/docs/MVVM-v1.md b/docs/MVVM-v1.md
new file mode 100644
index 0000000000..bbd02ccb06
--- /dev/null
+++ b/docs/MVVM-v1.md
@@ -0,0 +1,69 @@
+# MVVM
+
+_Deprecated_, see [MVVM.md](./MVVM.md) for the current version.
+
+General description of the pattern can be found [here](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel). But the gist of it is that you divide your code into three sections:
+
+1. Model: This is where the business logic and data resides.
+2. View Model: This code exists to provide the logic necessary for the UI. It directly uses the Model code.
+3. View: This is the UI code itself and depends on the view model.
+
+If you do MVVM right, your view should be dumb i.e it gets data from the view model and merely displays it.
+
+### Practical guidelines for MVVM in element-web
+
+#### Model
+
+This is anywhere your data or business logic comes from. If your view model is accessing something simple exposed from `matrix-js-sdk`, then the sdk is your model. If you're using something more high level in element-web to get your data/logic (eg: `MemberListStore`), then that becomes your model.
+
+#### View Model
+
+1. View model is always a custom react hook named like `useFooViewModel()`.
+2. The return type of your view model (known as view state) must be defined as a typescript interface:
+ ```ts
+ inteface FooViewState {
+ somethingUseful: string;
+ somethingElse: BarType;
+ update: () => Promise
+ ...
+ }
+ ```
+3. Any react state that your UI needs must be in the view model.
+
+#### View
+
+1. Views are simple react components (eg: `FooView`).
+2. Views usually start by calling the view model hook, eg:
+ ```tsx
+ const FooView: React.FC = (props: IProps) => {
+ const vm = useFooViewModel();
+ ....
+ return(
+
+ {vm.somethingUseful}
+
+ );
+ }
+ ```
+3. Views are also allowed to accept the view model as a prop, eg:
+ ```tsx
+ const FooView: React.FC = ({ vm }: IProps) => {
+ ....
+ return(
+
+ {vm.somethingUseful}
+
+ );
+ }
+ ```
+4. Multiple views can share the same view model if necessary.
+
+### Benefits
+
+1. MVVM forces a separation of concern i.e we will no longer have large react components that have a lot of state and rendering code mixed together. This improves code readability and makes it easier to introduce changes.
+2. Introduces the possibility of code reuse. You can reuse an old view model with a new view or vice versa.
+3. Adding to the point above, in future you could import element-web view models to your project and supply your own views thus creating something similar to the [hydrogen sdk](https://github.com/element-hq/hydrogen-web/blob/master/doc/SDK.md).
+
+### Example
+
+We started experimenting with MVVM in the redesigned memberlist, you can see the code [here](https://github.com/vector-im/element-web/blob/develop/src/components/views/rooms/MemberList/MemberListView.tsx).
diff --git a/docs/MVVM.md b/docs/MVVM.md
index 9dfb8e4776..6175e21853 100644
--- a/docs/MVVM.md
+++ b/docs/MVVM.md
@@ -10,58 +10,80 @@ If you do MVVM right, your view should be dumb i.e it gets data from the view mo
### Practical guidelines for MVVM in element-web
+A first documentation and implementation of MVVM was done in [MVVM-v1.md](MVVM-v1.md). This v1 version is now deprecated and this document describes the current implementation.
+
#### Model
This is anywhere your data or business logic comes from. If your view model is accessing something simple exposed from `matrix-js-sdk`, then the sdk is your model. If you're using something more high level in element-web to get your data/logic (eg: `MemberListStore`), then that becomes your model.
-#### View Model
-
-1. View model is always a custom react hook named like `useFooViewModel()`.
-2. The return type of your view model (known as view state) must be defined as a typescript interface:
- ```ts
- inteface FooViewState {
- somethingUseful: string;
- somethingElse: BarType;
- update: () => Promise
- ...
- }
- ```
-3. Any react state that your UI needs must be in the view model.
-
#### View
-1. Views are simple react components (eg: `FooView`).
-2. Views usually start by calling the view model hook, eg:
+1. Located in [`shared-components`](https://github.com/element-hq/element-web/tree/develop/packages/shared-components). Develop it in storybook!
+2. Views are simple react components (eg: `FooView`).
+3. Views use [useSyncExternalStore](https://react.dev/reference/react/useSyncExternalStore) internally where the view model is the external store.
+4. Views should define the interface of the view model they expect:
+
```tsx
- const FooView: React.FC = (props: IProps) => {
- const vm = useFooViewModel();
- ....
- return(
-
- {vm.somethingUseful}
-
- );
+ // Snapshot is the return type of your view model
+ interface FooViewSnapshot {
+ value: string;
+ }
+
+ // To call function on the view model
+ interface FooViewActions {
+ doSomething: () => void;
+ }
+
+ // ViewModel is a type defining the methods needed for `useSyncExternalStore`
+ // https://github.com/element-hq/element-web/blob/develop/packages/shared-components/src/ViewModel.ts
+ type FooViewModel = ViewModel & FooViewActions;
+
+ interface FooViewProps {
+ vm: FooViewModel;
+ }
+
+ function FooView({ vm }: FooViewProps) {
+ // useViewModel is a helper function that uses useSyncExternalStore under the hood
+ const { value } = useViewModel(vm);
+ return (
+
+ );
}
```
-3. Views are also allowed to accept the view model as a prop, eg:
- ```tsx
- const FooView: React.FC = ({ vm }: IProps) => {
- ....
- return(
-
- {vm.somethingUseful}
-
- );
+
+5. Multiple views can share the same view model if necessary.
+6. A full example is available [here](https://github.com/element-hq/element-web/blob/develop/packages/shared-components/src/audio/AudioPlayerView/AudioPlayerView.tsx)
+
+#### View Model
+
+1. A View model is a class extending [`BaseViewModel`](https://github.com/element-hq/element-web/blob/develop/src/viewmodels/base/BaseViewModel.ts).
+2. Implements the interface defined in the view (e.g `FooViewModel` in the example above).
+3. View models define a snapshot type that defines the data the view will consume. The snapshot is immutable and can only be changed by calling `this.snapshot.set(...)` in the view model. This will trigger a re-render in the view.
+
+ ```ts
+ interface Props {
+ propsValue: string;
+ }
+
+ class FooViewModel extends BaseViewModel implements FooViewModel {
+ constructor(props: Props) {
+ // Call super with initial snapshot
+ super(props, { value: "initial" });
+ }
+
+ public doSomething() {
+ // Call this.snapshot.set to update the snapshot
+ this.snapshot.set({ value: "changed" });
+ }
}
```
-4. Multiple views can share the same view model if necessary.
+
+4. A full example is available [here](https://github.com/element-hq/element-web/blob/develop/src/viewmodels/audio/AudioPlayerViewModel.ts)
### Benefits
1. MVVM forces a separation of concern i.e we will no longer have large react components that have a lot of state and rendering code mixed together. This improves code readability and makes it easier to introduce changes.
2. Introduces the possibility of code reuse. You can reuse an old view model with a new view or vice versa.
3. Adding to the point above, in future you could import element-web view models to your project and supply your own views thus creating something similar to the [hydrogen sdk](https://github.com/element-hq/hydrogen-web/blob/master/doc/SDK.md).
-
-### Example
-
-We started experimenting with MVVM in the redesigned memberlist, you can see the code [here](https://github.com/vector-im/element-web/blob/develop/src/components/views/rooms/MemberList/MemberListView.tsx).
diff --git a/docs/config.md b/docs/config.md
index f1ced14fd2..6f5315d257 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -130,32 +130,37 @@ complete re-branding/private labeling, a more personalised experience can be ach
6. `mobile_builds`: Optional. Like `desktop_builds`, except for the mobile apps. Also described in more detail down below.
7. `mobile_guide_toast`: When `true` (default), users accessing the Element Web instance from a mobile device will be prompted to
download the app instead.
-8. `update_base_url`: For the desktop app only, the URL where to acquire update packages. If specified, must be a path to a directory
+8. `mobile_guide_app_variant`: Optional. The mobile app that the user is prompted to download from the `/mobile_guide` page. When omitted
+ the mobile guide will be configured for the new Element X apps. Allowed values are as follows:
+ 1. `element`: Element X Android/iOS.
+ 2. `element-classic`: Element Classic Android/iOS.
+ 3. `element-pro`: Element Pro Android/iOS.
+9. `update_base_url`: For the desktop app only, the URL where to acquire update packages. If specified, must be a path to a directory
containing `macos` and `win32` directories, with the update packages within. Defaults to `https://packages.element.io/desktop/update/`
in production.
-9. `map_style_url`: Map tile server style URL for location sharing. e.g. `https://api.maptiler.com/maps/streets/style.json?key=YOUR_KEY_GOES_HERE`
- This setting is ignored if your homeserver provides `/.well-known/matrix/client` in its well-known location, and the JSON file
- at that location has a key `m.tile_server` (or the unstable version `org.matrix.msc3488.tile_server`). In this case, the
- configuration found in the well-known location is used instead.
-10. `welcome_user_id`: **DEPRECATED** An optional user ID to start a DM with after creating an account. Defaults to nothing (no DM created).
-11. `custom_translations_url`: An optional URL to allow overriding of translatable strings. The JSON file must be in a format of
+10. `map_style_url`: Map tile server style URL for location sharing. e.g. `https://api.maptiler.com/maps/streets/style.json?key=YOUR_KEY_GOES_HERE`
+ This setting is ignored if your homeserver provides `/.well-known/matrix/client` in its well-known location, and the JSON file
+ at that location has a key `m.tile_server` (or the unstable version `org.matrix.msc3488.tile_server`). In this case, the
+ configuration found in the well-known location is used instead.
+11. `welcome_user_id`: **DEPRECATED** An optional user ID to start a DM with after creating an account. Defaults to nothing (no DM created).
+12. `custom_translations_url`: An optional URL to allow overriding of translatable strings. The JSON file must be in a format of
`{"affected|translation|key": {"languageCode": "new string"}}`. See https://github.com/matrix-org/matrix-react-sdk/pull/7886 for details.
-12. `branding`: Options for configuring various assets used within the app. Described in more detail down below.
-13. `embedded_pages`: Further optional URLs for various assets used within the app. Described in more detail down below.
-14. `disable_3pid_login`: When `false` (default), **enables** the options to log in with email address or phone number. Set to
+13. `branding`: Options for configuring various assets used within the app. Described in more detail down below.
+14. `embedded_pages`: Further optional URLs for various assets used within the app. Described in more detail down below.
+15. `disable_3pid_login`: When `false` (default), **enables** the options to log in with email address or phone number. Set to
`true` to hide these options.
-15. `disable_login_language_selector`: When `false` (default), **enables** the language selector on the login pages. Set to `true`
+16. `disable_login_language_selector`: When `false` (default), **enables** the language selector on the login pages. Set to `true`
to hide this dropdown.
-16. `disable_guests`: When `false` (default), **enable** guest-related functionality (peeking/previewing rooms, etc) for unregistered
+17. `disable_guests`: When `false` (default), **enable** guest-related functionality (peeking/previewing rooms, etc) for unregistered
users. Set to `true` to disable this functionality.
-17. `user_notice`: Optional notice to show to the user, e.g. for sunsetting a deployment and pushing users to move in their own time.
+18. `user_notice`: Optional notice to show to the user, e.g. for sunsetting a deployment and pushing users to move in their own time.
Takes a configuration object as below:
1. `title`: Required. Title to show at the top of the notice.
2. `description`: Required. The description to use for the notice.
3. `show_once`: Optional. If true then the notice will only be shown once per device.
-18. `help_url`: The URL to point users to for help with the app, defaults to `https://element.io/help`.
-19. `help_encryption_url`: The URL to point users to for help with encryption, defaults to `https://element.io/help#encryption`.
-20. `force_verification`: If true, users must verify new logins (eg. with another device / their recovery key)
+19. `help_url`: The URL to point users to for help with the app, defaults to `https://element.io/help`.
+20. `help_encryption_url`: The URL to point users to for help with encryption, defaults to `https://element.io/help#encryption`.
+21. `force_verification`: If true, users must verify new logins (eg. with another device / their recovery key)
### `desktop_builds` and `mobile_builds`
@@ -402,7 +407,7 @@ The VoIP and Jitsi options are:
If you run your own rageshake server to collect bug reports, the following options may be of interest:
1. `bug_report_endpoint_url`: URL for where to submit rageshake logs to. Rageshakes include feedback submissions and bug reports. When
- not present in the config, the app will disable all rageshake functionality. Set to `https://element.io/bugreports/submit` to submit
+ not present in the config, the app will disable all rageshake functionality. Set to `https://rageshakes.element.io/api/submit` to submit
rageshakes to us, or use your own rageshake server.
2. `uisi_autorageshake_app`: If a user has enabled the "automatically send debug logs on decryption errors" flag, this option will be sent
alongside the rageshake so the rageshake server can filter them by app name. By default, this will be `element-auto-uisi`
@@ -445,8 +450,7 @@ If you would like to use Scalar, the integration manager maintained by Element,
"https://scalar.vector.im/_matrix/integrations/v1",
"https://scalar.vector.im/api",
"https://scalar-staging.vector.im/_matrix/integrations/v1",
- "https://scalar-staging.vector.im/api",
- "https://scalar-staging.riot.im/scalar/api"
+ "https://scalar-staging.vector.im/api"
]
}
```
@@ -581,6 +585,8 @@ Currently, the following UI feature flags are supported:
- `UIFeature.BulkUnverifiedSessionsReminder` - Display popup reminders to verify or remove unverified sessions. Defaults
to true.
- `UIFeature.locationSharing` - Whether or not location sharing menus will be shown.
+- `UIFeature.allowCreatingPublicRooms` - Whether or not public rooms can be created.
+- `UIFeature.allowCreatingPublicSpaces` - Whether or not public spaces can be created.
## Undocumented / developer options
diff --git a/docs/e2ee.md b/docs/e2ee.md
index 835c38a1d5..2f960416b8 100644
--- a/docs/e2ee.md
+++ b/docs/e2ee.md
@@ -38,45 +38,20 @@ When `force_disable` is true:
Note: If the server is configured to forcibly enable encryption for some or all rooms,
this behaviour will be overridden.
-# Secure backup
+# Setting up recovery
By default, Element strongly encourages (but does not require) users to set up
-Secure Backup so that cross-signing identity key and message keys can be
-recovered in case of a disaster where you lose access to all active devices.
+recovery so that you can access history on your new devices as well as retain access to your message history and cryptographic identity when you lose all of your devices.
-## Requiring secure backup
+## Removal of old settings
-To require Secure Backup to be configured before Element can be used, set the
-following on your homeserver's `/.well-known/matrix/client` config:
+Support for the configuration options `secure_backup_required` and `secure_backup_setup_methods`
+in the `/.well-known/matrix/client` config has been removed.
-```json
-{
- "io.element.e2ee": {
- "secure_backup_required": true
- }
-}
-```
-
-## Preferring setup methods
-
-By default, Element offers users a choice of a random key or user-chosen
-passphrase when setting up Secure Backup. If a homeserver admin would like to
-only offer one of these, you can signal this via the
-`/.well-known/matrix/client` config, for example:
-
-```json
-{
- "io.element.e2ee": {
- "secure_backup_setup_methods": ["passphrase"]
- }
-}
-```
-
-The field `secure_backup_setup_methods` is an array listing the methods the
-client should display. Supported values currently include `key` and
-`passphrase`. If the `secure_backup_setup_methods` field is not present or
-exists but does not contain any supported methods, Element will fallback to the
-default value of: `["key", "passphrase"]`.
+Setting up recovery is now always recommended to all users by showing a one-off toast and a
+permanent red dot on the _Encryption_ tab in the _Settings_ dialog. When creating a new
+recovery key, the UI only supports auto-generated keys. Using an existing (custom) passphrase
+still works, but is not exposed in the UI when setting up recovery.
# Compatibility
diff --git a/docs/install.md b/docs/install.md
index 5f9e6ddd2e..d80a844778 100644
--- a/docs/install.md
+++ b/docs/install.md
@@ -11,7 +11,7 @@ There are some exceptions like when using localhost, which is considered a [secu
1. Download the latest version from
1. Untar the tarball on your web server
1. Move (or symlink) the `element-x.x.x` directory to an appropriate name
-1. Configure the correct caching headers in your webserver (see below)
+1. Configure the correct caching headers in your webserver (see [README.md](../README.md#caching-requirements))
1. Configure the app by copying `config.sample.json` to `config.json` and
modifying it. See the [configuration docs](config.md) for details.
1. Enter the URL into your browser and log into Element!
diff --git a/docs/kubernetes.md b/docs/kubernetes.md
index cae8526e9c..39c10d330b 100644
--- a/docs/kubernetes.md
+++ b/docs/kubernetes.md
@@ -55,10 +55,9 @@ Then you can deploy it to your cluster with something like `kubectl apply -f my-
"https://scalar.vector.im/_matrix/integrations/v1",
"https://scalar.vector.im/api",
"https://scalar-staging.vector.im/_matrix/integrations/v1",
- "https://scalar-staging.vector.im/api",
- "https://scalar-staging.riot.im/scalar/api"
+ "https://scalar-staging.vector.im/api"
],
- "bug_report_endpoint_url": "https://element.io/bugreports/submit",
+ "bug_report_endpoint_url": "https://rageshakes.element.io/api/submit",
"defaultCountryCode": "GB",
"show_labs_settings": false,
"features": { },
diff --git a/docs/labs.md b/docs/labs.md
index 60f35dd4a4..a4ab78b08b 100644
--- a/docs/labs.md
+++ b/docs/labs.md
@@ -112,3 +112,25 @@ Enables knock feature for rooms. This allows users to ask to join a room.
## New room list (`feature_new_room_list`) [In Development]
Enable the new room list that is currently in development.
+
+## Exclude insecure devices when sending/receiving messages (`feature_exclude_insecure_devices`)
+
+Do not send or receive messages to/from devices that are not properly verified. Users with unverified devices will not
+receive your messages at all on those devices, and if they send messages, you will not be able to read them, but you
+will be aware that a message exists.
+
+## Share encrypted history with new members (`feature_share_history_on_invite`) [In Development]
+
+When inviting users to an encrypted room with shared history (i.e. a room with the "Who can read history?" setting set
+to "Members only (since the point in time of selecting this option)"), send the keys for previous messages to the
+invitee so they can read them.
+
+Both the inviter and the invitee must set this labs flag, before the invitation is sent.
+
+## Encrypted state events (MSC4362) (`feature_msc4362_encrypted_state_events`)
+
+Encrypt most of the state events in the room, including the room name and topic.
+
+WARNING: this means that users joining a room who do not have access to its history will not be able to see the name or
+topic of the room, or any other room state information. It also means the room name and topic are not available before
+joining a room.
diff --git a/element.io/app/config.json b/element.io/app/config.json
index 771df35091..4f7bde39fb 100644
--- a/element.io/app/config.json
+++ b/element.io/app/config.json
@@ -15,10 +15,9 @@
"https://scalar.vector.im/_matrix/integrations/v1",
"https://scalar.vector.im/api",
"https://scalar-staging.vector.im/_matrix/integrations/v1",
- "https://scalar-staging.vector.im/api",
- "https://scalar-staging.riot.im/scalar/api"
+ "https://scalar-staging.vector.im/api"
],
- "bug_report_endpoint_url": "https://element.io/bugreports/submit",
+ "bug_report_endpoint_url": "https://rageshakes.element.io/api/submit",
"uisi_autorageshake_app": "element-auto-uisi",
"show_labs_settings": false,
"room_directory": {
diff --git a/element.io/develop/config.json b/element.io/develop/config.json
index aaee51afd0..7cfe544478 100644
--- a/element.io/develop/config.json
+++ b/element.io/develop/config.json
@@ -15,10 +15,9 @@
"https://scalar.vector.im/_matrix/integrations/v1",
"https://scalar.vector.im/api",
"https://scalar-staging.vector.im/_matrix/integrations/v1",
- "https://scalar-staging.vector.im/api",
- "https://scalar-staging.riot.im/scalar/api"
+ "https://scalar-staging.vector.im/api"
],
- "bug_report_endpoint_url": "https://element.io/bugreports/submit",
+ "bug_report_endpoint_url": "https://rageshakes.element.io/api/submit",
"uisi_autorageshake_app": "element-auto-uisi",
"show_labs_settings": true,
"room_directory": {
diff --git a/jest.config.ts b/jest.config.ts
index ad31f2fecc..148d55c94c 100644
--- a/jest.config.ts
+++ b/jest.config.ts
@@ -22,6 +22,8 @@ const config: Config = {
setupFiles: ["jest-canvas-mock", "web-streams-polyfill/polyfill"],
setupFilesAfterEnv: ["/test/setupTests.ts"],
moduleNameMapper: {
+ // Support CSS module
+ "\\.(module.css)$": "identity-obj-proxy",
"\\.(css|scss|pcss)$": "/__mocks__/cssMock.js",
"\\.(gif|png|ttf|woff2)$": "/__mocks__/imageMock.js",
"\\.svg$": "/__mocks__/svg.js",
@@ -38,12 +40,14 @@ const config: Config = {
"^!!raw-loader!.*": "jest-raw-loader",
"recorderWorkletFactory": "/__mocks__/empty.js",
"^fetch-mock$": "/node_modules/fetch-mock",
- // Requires ESM which is incompatible with our current Jest setup
- "^@element-hq/element-web-module-api$": "/__mocks__/empty.js",
+ "counterpart": "/node_modules/counterpart",
},
- transformIgnorePatterns: ["/node_modules/(?!(mime|matrix-js-sdk)).+$"],
+ transformIgnorePatterns: [
+ "/node_modules/(?!(mime|matrix-js-sdk|uuid|p-retry|is-network-error|react-merge-refs|is-ip|ip-regex|super-regex|function-timeout|time-span|convert-hrtime|clone-regexp|is-regexp)).+$",
+ ],
collectCoverageFrom: [
"/src/**/*.{js,ts,tsx}",
+ "/packages/**/*.{js,ts,tsx}",
// getSessionLock is piped into a different JS context via stringification, and the coverage functionality is
// not available in that contest. So, turn off coverage instrumentation for it.
"!/src/utils/SessionLock.ts",
diff --git a/knip.ts b/knip.ts
index e129c11481..24f8b97343 100644
--- a/knip.ts
+++ b/knip.ts
@@ -2,18 +2,16 @@ import { KnipConfig } from "knip";
export default {
entry: [
- "src/vector/index.ts",
"src/serviceworker/index.ts",
"src/workers/*.worker.ts",
"src/utils/exportUtils/exportJS.js",
+ "src/vector/localstorage-fix.ts",
"scripts/**",
"playwright/**",
"test/**",
"res/decoder-ring/**",
"res/jitsi_external_api.min.js",
"docs/**",
- // Used by jest
- "__mocks__/maplibre-gl.js",
],
project: ["**/*.{js,ts,jsx,tsx}"],
ignore: [
@@ -22,6 +20,8 @@ export default {
"src/hooks/useTimeout.ts",
"src/components/views/elements/InfoTooltip.tsx",
"src/components/views/elements/StyledCheckbox.tsx",
+
+ "packages/**/*",
],
ignoreDependencies: [
// Required for `action-validator`
@@ -42,6 +42,18 @@ export default {
"util",
// Embedded into webapp
"@element-hq/element-call-embedded",
+ // Transitive dep of jest
+ "jsdom",
+
+ // Used by matrix-js-sdk, which means we have to include them as a
+ // dependency so that // we can run `tsc` (since we import the typescript
+ // source of js-sdk, rather than the transpiled and annotated JS like you
+ // would with a normal library).
+ "@types/content-type",
+ "@types/sdp-transform",
+
+ // Used in EW but failed because of "link:"
+ "@element-hq/web-shared-components",
],
ignoreBinaries: [
// Used in scripts & workflows
diff --git a/module_system/BuildConfig.ts b/module_system/BuildConfig.ts
index cfb805840d..b9454e8418 100644
--- a/module_system/BuildConfig.ts
+++ b/module_system/BuildConfig.ts
@@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details.
*/
import * as YAML from "yaml";
-import * as fs from "fs";
+import * as fs from "node:fs";
export type BuildConfig = {
// Dev note: make everything here optional for user safety. Invalid
diff --git a/module_system/installer.ts b/module_system/installer.ts
index 66df4e92b6..dad65459f4 100644
--- a/module_system/installer.ts
+++ b/module_system/installer.ts
@@ -5,11 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
-import * as fs from "fs";
-import * as childProcess from "child_process";
+import * as fs from "node:fs";
+import * as childProcess from "node:child_process";
import * as semver from "semver";
-import { type BuildConfig } from "./BuildConfig";
+import { type BuildConfig } from "./BuildConfig.ts";
// This expects to be run from ./scripts/install.ts
diff --git a/module_system/scripts/install.ts b/module_system/scripts/install.ts
index 0b6d2e1c29..5f96af1cd3 100644
--- a/module_system/scripts/install.ts
+++ b/module_system/scripts/install.ts
@@ -5,8 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
-import { readBuildConfig } from "../BuildConfig";
-import { installer } from "../installer";
+import { readBuildConfig } from "../BuildConfig.ts";
+import { installer } from "../installer.ts";
const buildConf = readBuildConfig();
installer(buildConf);
diff --git a/package.json b/package.json
index f5ff367e3d..0bbd0283dd 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "element-web",
- "version": "1.11.101",
+ "version": "1.12.7",
"description": "Element: the future of secure communication",
"author": "New Vector Ltd.",
"repository": {
@@ -29,7 +29,7 @@
"UserFriendlyError"
],
"scripts": {
- "i18n": "matrix-gen-i18n && yarn i18n:sort && yarn i18n:lint",
+ "i18n": "matrix-gen-i18n src res packages/shared-components/src && yarn i18n:sort && yarn i18n:lint",
"i18n:sort": "jq --sort-keys '.' src/i18n/strings/en_EN.json > src/i18n/strings/en_EN.json.tmp && mv src/i18n/strings/en_EN.json.tmp src/i18n/strings/en_EN.json",
"i18n:lint": "matrix-i18n-lint && prettier --log-level=silent --write src/i18n/strings/ --ignore-path /dev/null",
"i18n:diff": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && yarn i18n && matrix-compare-i18n-files src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json",
@@ -38,16 +38,16 @@
"clean": "rimraf lib webapp",
"build": "yarn clean && yarn build:genfiles && yarn build:bundle",
"build-stats": "yarn clean && yarn build:genfiles && yarn build:bundle-stats",
- "build:res": "ts-node scripts/copy-res.ts",
+ "build:res": "node scripts/copy-res.ts",
"build:genfiles": "yarn build:res && yarn build:module_system",
"build:modernizr": "modernizr -c .modernizr.json -d src/vector/modernizr.js",
"build:bundle": "webpack --progress --mode production",
"build:bundle-stats": "webpack --progress --mode production --json > webpack-stats.json",
- "build:module_system": "ts-node --project ./tsconfig.module_system.json module_system/scripts/install.ts",
+ "build:module_system": "node module_system/scripts/install.ts",
"dist": "./scripts/package.sh",
"start": "concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n modules,res \"yarn build:module_system\" \"yarn build:res\" && concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n res,element-js \"yarn start:res\" \"yarn start:js\"",
"start:https": "concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n res,element-js \"yarn start:res\" \"yarn start:js --server-type https\"",
- "start:res": "ts-node scripts/copy-res.ts -w",
+ "start:res": "node scripts/copy-res.ts -w",
"start:js": "webpack serve --output-path webapp --output-filename=bundles/_dev_/[name].js --output-chunk-filename=bundles/_dev_/[name].js --mode development",
"lint": "yarn lint:types && yarn lint:js && yarn lint:style && yarn lint:workflows",
"lint:js": "eslint --max-warnings 0 src test playwright module_system && prettier --check .",
@@ -65,38 +65,36 @@
"coverage": "yarn test --coverage",
"analyse:webpack-bundles": "webpack-bundle-analyzer webpack-stats.json webapp",
"update:jitsi": "curl -s https://meet.element.io/libs/external_api.min.js > ./res/jitsi_external_api.min.js",
+ "install": "yarn --cwd packages/shared-components install --frozen-lockfile",
"postinstall": "patch-package"
},
"resolutions": {
- "**/pretty-format/react-is": "19.1.0",
- "@playwright/test": "1.52.0",
- "@types/react": "19.1.6",
- "@types/react-dom": "19.1.5",
- "oidc-client-ts": "3.2.1",
+ "**/pretty-format/react-is": "19.2.1",
+ "@types/react": "19.2.7",
+ "@types/react-dom": "19.2.3",
+ "oidc-client-ts": "3.4.1",
"jwt-decode": "4.0.0",
- "caniuse-lite": "1.0.30001720",
+ "caniuse-lite": "1.0.30001759",
"testcontainers": "^11.0.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
"wrap-ansi": "npm:wrap-ansi@^7.0.0"
},
"dependencies": {
"@babel/runtime": "^7.12.5",
- "@element-hq/element-web-module-api": "1.0.0",
- "@fontsource/inconsolata": "^5",
+ "@element-hq/element-web-module-api": "1.9.0",
+ "@element-hq/web-shared-components": "link:packages/shared-components",
+ "@fontsource/fira-code": "^5",
"@fontsource/inter": "^5",
"@formatjs/intl-segmenter": "^11.5.7",
- "@giphy/js-fetch-api": "^5.6.0",
- "@giphy/react-components": "^10.0.1",
- "@matrix-org/analytics-events": "^0.29.2",
- "@matrix-org/emojibase-bindings": "^1.3.4",
+ "@matrix-org/analytics-events": "^0.30.0",
+ "@matrix-org/emojibase-bindings": "^1.5.0",
"@matrix-org/react-sdk-module-api": "^2.4.0",
"@matrix-org/spec": "^1.7.0",
- "@sentry/browser": "^9.0.0",
+ "@sentry/browser": "^10.0.0",
"@types/png-chunks-extract": "^1.0.2",
- "@types/react-virtualized": "^9.21.30",
- "@vector-im/compound-design-tokens": "^4.0.0",
- "@vector-im/compound-web": "^7.11.0",
- "@vector-im/matrix-wysiwyg": "2.38.3",
+ "@vector-im/compound-design-tokens": "6.4.3",
+ "@vector-im/compound-web": "^8.3.1",
+ "@vector-im/matrix-wysiwyg": "2.40.0",
"@zxcvbn-ts/core": "^3.0.4",
"@zxcvbn-ts/language-common": "^3.0.4",
"@zxcvbn-ts/language-en": "^3.0.2",
@@ -106,41 +104,40 @@
"browserslist": "^4.23.2",
"classnames": "^2.2.6",
"commonmark": "^0.31.0",
- "counterpart": "^0.18.6",
"css-tree": "^3.0.0",
"diff-dom": "^5.0.0",
"diff-match-patch": "^1.0.5",
"domutils": "^3.2.2",
- "emojibase-regex": "15.3.2",
+ "emojibase-regex": "^17.0.0",
"escape-html": "^1.0.3",
"file-saver": "^2.0.5",
- "filesize": "10.1.6",
+ "filesize": "11.0.13",
"github-markdown-css": "^5.5.1",
"glob-to-regexp": "^0.4.1",
"highlight.js": "^11.3.1",
"html-entities": "^2.0.0",
"html-react-parser": "^5.2.2",
- "is-ip": "^3.1.0",
- "js-xxhash": "^4.0.0",
+ "is-ip": "^5.0.0",
+ "js-xxhash": "^5.0.0",
"jsrsasign": "^11.0.0",
"jszip": "^3.7.0",
"katex": "^0.16.0",
- "linkify-react": "4.3.1",
- "linkify-string": "4.3.1",
- "linkifyjs": "4.3.1",
+ "linkify-html": "4.3.2",
+ "linkify-react": "4.3.2",
+ "linkify-string": "4.3.2",
+ "linkifyjs": "4.3.2",
"lodash": "^4.17.21",
"maplibre-gl": "^5.0.0",
"matrix-encrypt-attachment": "^1.0.3",
- "matrix-events-sdk": "0.0.1",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
- "matrix-widget-api": "^1.10.0",
+ "matrix-widget-api": "^1.15.0",
"memoize-one": "^6.0.0",
"mime": "^4.0.4",
"oidc-client-ts": "^3.0.1",
"opus-recorder": "^8.0.3",
"pako": "^2.0.3",
"png-chunks-extract": "^1.0.0",
- "posthog-js": "1.248.1",
+ "posthog-js": "1.302.2",
"qrcode": "1.5.4",
"re-resizable": "6.11.2",
"react": "^19.0.0",
@@ -148,17 +145,17 @@
"react-blurhash": "^0.3.0",
"react-dom": "^19.0.0",
"react-focus-lock": "^2.5.1",
+ "react-merge-refs": "^3.0.2",
"react-string-replace": "^1.1.1",
"react-transition-group": "^4.4.1",
- "react-virtualized": "^9.22.5",
+ "react-virtuoso": "^4.14.0",
"rfc4648": "^1.4.0",
"sanitize-filename": "^1.6.3",
"sanitize-html": "2.17.0",
- "styled-components": "^6.1.17",
"tar-js": "^0.3.0",
"temporal-polyfill": "^0.3.0",
- "ua-parser-js": "^1.0.2",
- "uuid": "^11.0.0",
+ "ua-parser-js": "1.0.40",
+ "uuid": "^13.0.0",
"what-input": "^5.2.10"
},
"devDependencies": {
@@ -182,21 +179,22 @@
"@babel/preset-react": "^7.12.10",
"@babel/preset-typescript": "^7.12.7",
"@babel/runtime": "^7.12.5",
- "@casualbot/jest-sonar-reporter": "2.2.7",
- "@element-hq/element-call-embedded": "0.12.0",
- "@element-hq/element-web-playwright-common": "^1.1.5",
+ "@casualbot/jest-sonar-reporter": "2.5.0",
+ "@element-hq/element-call-embedded": "0.16.3",
+ "@element-hq/element-web-playwright-common": "^2.0.0",
"@peculiar/webcrypto": "^1.4.3",
- "@playwright/test": "^1.50.1",
+ "@playwright/test": "1.57.0",
"@principalstudio/html-webpack-inject-preload": "^1.2.7",
- "@rrweb/types": "^2.0.0-alpha.18",
- "@sentry/webpack-plugin": "^3.0.0",
- "@stylistic/eslint-plugin": "^4.0.0",
+ "@sentry/webpack-plugin": "^4.0.0",
+ "@storybook/react-vite": "^10.0.7",
+ "@stylistic/eslint-plugin": "^5.0.0",
"@svgr/webpack": "^8.0.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.4.8",
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.5.2",
"@types/commonmark": "^0.27.4",
+ "@types/content-type": "^1.1.9",
"@types/counterpart": "^0.18.1",
"@types/css-tree": "^2.3.8",
"@types/diff-match-patch": "^1.0.32",
@@ -204,7 +202,7 @@
"@types/express": "^5.0.0",
"@types/file-saver": "^2.0.3",
"@types/glob-to-regexp": "^0.4.1",
- "@types/jest": "29.5.12",
+ "@types/jest": "30.0.0",
"@types/jitsi-meet": "^2.0.2",
"@types/jsrsasign": "^10.5.4",
"@types/katex": "^0.16.0",
@@ -215,53 +213,53 @@
"@types/node-fetch": "^2.6.2",
"@types/pako": "^2.0.0",
"@types/qrcode": "^1.3.5",
- "@types/react": "19.1.6",
+ "@types/react": "19.2.7",
"@types/react-beautiful-dnd": "^13.0.0",
- "@types/react-dom": "19.1.5",
+ "@types/react-dom": "19.2.3",
"@types/react-transition-group": "^4.4.0",
"@types/sanitize-html": "2.16.0",
+ "@types/sdp-transform": "^2.4.10",
"@types/semver": "^7.5.8",
"@types/tar-js": "^0.3.5",
"@types/ua-parser-js": "^0.7.36",
- "@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^8.19.0",
"@typescript-eslint/parser": "^8.19.0",
- "babel-jest": "^29.0.0",
+ "babel-jest": "^30.0.0",
"babel-loader": "^10.0.0",
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
"blob-polyfill": "^9.0.0",
- "chokidar": "^4.0.0",
+ "chokidar": "^5.0.0",
"concurrently": "^9.0.0",
"copy-webpack-plugin": "^13.0.0",
"core-js": "^3.38.1",
- "cronstrue": "^2.41.0",
+ "cronstrue": "^3.0.0",
"css-loader": "^7.0.0",
"css-minimizer-webpack-plugin": "^7.0.0",
- "dotenv": "^16.0.2",
+ "dotenv": "^17.0.0",
"eslint": "8.57.1",
"eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^10.0.0",
- "eslint-plugin-deprecate": "0.8.5",
+ "eslint-plugin-deprecate": "0.8.7",
"eslint-plugin-import": "^2.25.4",
- "eslint-plugin-jest": "^28.0.0",
+ "eslint-plugin-jest": "^29.0.0",
"eslint-plugin-jsx-a11y": "^6.5.1",
- "eslint-plugin-matrix-org": "^2.0.2",
+ "eslint-plugin-matrix-org": "^3.0.0",
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-compiler": "^19.0.0-beta-df7b47d-20241124",
- "eslint-plugin-react-hooks": "^5.0.0",
+ "eslint-plugin-react-hooks": "^7.0.0",
"eslint-plugin-unicorn": "^56.0.0",
"express": "^5.0.0",
"fake-indexeddb": "^6.0.0",
"fetch-mock": "9.11.0",
"fetch-mock-jest": "^1.5.1",
"file-loader": "^6.0.0",
- "glob": "^11.0.0",
"html-webpack-plugin": "^5.5.3",
"husky": "^9.0.0",
- "jest": "^29.6.2",
+ "identity-obj-proxy": "^3.0.0",
+ "jest": "^30.0.0",
"jest-canvas-mock": "^2.5.2",
- "jest-environment-jsdom": "^29.7.0",
- "jest-mock": "^29.6.2",
+ "jest-environment-jsdom": "^30.0.0",
+ "jest-mock": "^30.0.0",
"jest-raw-loader": "^1.0.1",
"jsqr": "^1.4.0",
"knip": "^5.36.2",
@@ -278,29 +276,29 @@
"postcss-hexrgba": "2.1.0",
"postcss-import": "16.1.0",
"postcss-loader": "8.1.1",
- "postcss-mixins": "^11.0.0",
+ "postcss-mixins": "^12.0.0",
"postcss-nested": "^7.0.0",
"postcss-preset-env": "^10.0.0",
"postcss-scss": "^4.0.4",
"postcss-simple-vars": "^7.0.1",
- "prettier": "3.5.3",
+ "prettier": "3.7.4",
"process": "^0.11.10",
"raw-loader": "^4.0.2",
"rimraf": "^6.0.0",
"semver": "^7.5.2",
"source-map-loader": "^5.0.0",
- "stylelint": "^16.13.0",
- "stylelint-config-standard": "^38.0.0",
+ "storybook": "^10.0.7",
+ "stylelint": "^16.23.0",
+ "stylelint-config-standard": "^39.0.0",
"stylelint-scss": "^6.0.0",
"stylelint-value-no-unknown-custom-properties": "^6.0.1",
"terser-webpack-plugin": "^5.3.9",
"testcontainers": "^11.0.0",
- "ts-node": "^10.9.1",
"typescript": "5.8.3",
"util": "^0.12.5",
"web-streams-polyfill": "^4.0.0",
"webpack": "^5.89.0",
- "webpack-bundle-analyzer": "^4.8.0",
+ "webpack-bundle-analyzer": "^5.0.0",
"webpack-cli": "^6.0.0",
"webpack-dev-server": "^5.0.0",
"webpack-retry-chunk-load-plugin": "^3.1.1",
@@ -313,7 +311,7 @@
"relativePaths": true
},
"engines": {
- "node": ">=20.0.0"
+ "node": ">=22.18"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
diff --git a/packages/shared-components/.eslintrc.js b/packages/shared-components/.eslintrc.js
new file mode 100644
index 0000000000..3cff936e9c
--- /dev/null
+++ b/packages/shared-components/.eslintrc.js
@@ -0,0 +1,72 @@
+/*
+Copyright 2025 Element Creations Ltd.
+
+SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+Please see LICENSE files in the repository root for full details.
+*/
+
+module.exports = {
+ root: true,
+ plugins: ["matrix-org", "eslint-plugin-react-compiler"],
+ extends: [
+ "plugin:matrix-org/babel",
+ "plugin:matrix-org/react",
+ "plugin:matrix-org/a11y",
+ "plugin:storybook/recommended",
+ ],
+ parserOptions: {
+ project: ["./tsconfig.json"],
+ tsconfigRootDir: __dirname,
+ },
+ env: {
+ browser: true,
+ node: true,
+ },
+ rules: {
+ // Bind or arrow functions in props causes performance issues (but we
+ // currently use them in some places).
+ // It's disabled here, but we should using it sparingly.
+ "react/jsx-no-bind": "off",
+ "react/jsx-key": ["error"],
+ "matrix-org/require-copyright-header": "error",
+ "react-compiler/react-compiler": "error",
+ },
+ overrides: [
+ {
+ files: ["src/**/*.{ts,tsx}", "test/**/*.{ts,tsx}"],
+ extends: ["plugin:matrix-org/typescript", "plugin:matrix-org/react"],
+ rules: {
+ "@typescript-eslint/explicit-function-return-type": [
+ "error",
+ {
+ allowExpressions: true,
+ },
+ ],
+
+ // Remove Babel things manually due to override limitations
+ "@babel/no-invalid-this": ["off"],
+
+ // We're okay being explicit at the moment
+ "@typescript-eslint/no-empty-interface": "off",
+ // We disable this while we're transitioning
+ "@typescript-eslint/no-explicit-any": "off",
+ // We'd rather not do this but we do
+ "@typescript-eslint/ban-ts-comment": "off",
+ // We're okay with assertion errors when we ask for them
+ "@typescript-eslint/no-non-null-assertion": "off",
+ "@typescript-eslint/no-empty-object-type": [
+ "error",
+ {
+ // We do this sometimes to brand interfaces
+ allowInterfaces: "with-single-extends",
+ },
+ ],
+ },
+ },
+ ],
+ settings: {
+ react: {
+ version: "detect",
+ },
+ },
+};
diff --git a/packages/shared-components/.prettierignore b/packages/shared-components/.prettierignore
new file mode 100644
index 0000000000..bbb6b1ef7f
--- /dev/null
+++ b/packages/shared-components/.prettierignore
@@ -0,0 +1,2 @@
+dist/
+i18n/i18nKeys.d.ts
diff --git a/packages/shared-components/.storybook/ElementTheme.ts b/packages/shared-components/.storybook/ElementTheme.ts
new file mode 100644
index 0000000000..0967697621
--- /dev/null
+++ b/packages/shared-components/.storybook/ElementTheme.ts
@@ -0,0 +1,28 @@
+/*
+Copyright 2025 New Vector Ltd.
+
+SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+Please see LICENSE files in the repository root for full details.
+*/
+
+import { create } from "storybook/theming";
+
+export default create({
+ base: "light",
+
+ // Colors
+ textColor: "#1b1d22",
+ colorSecondary: "#111111",
+
+ // UI
+ appBg: "#ffffff",
+ appContentBg: "#ffffff",
+
+ // Toolbar
+ barBg: "#ffffff",
+
+ brandTitle: "Element Web",
+ brandUrl: "https://github.com/element-hq/element-web",
+ brandImage: "https://element.io/images/logo-ele-secondary.svg",
+ brandTarget: "_self",
+});
diff --git a/packages/shared-components/.storybook/languageAddon.tsx b/packages/shared-components/.storybook/languageAddon.tsx
new file mode 100644
index 0000000000..e1483738f5
--- /dev/null
+++ b/packages/shared-components/.storybook/languageAddon.tsx
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+import { Addon, types, useGlobals } from "storybook/manager-api";
+import { WithTooltip, IconButton, TooltipLinkList } from "storybook/internal/components";
+import React from "react";
+import { GlobeIcon } from "@storybook/icons";
+
+// We can't import `shared/i18n.tsx` directly here.
+// The storybook addon doesn't seem to benefit the vite config of storybook and we can't resolve the alias in i18n.tsx.
+import json from "../../../webapp/i18n/languages.json";
+const languages = Object.keys(json).filter((lang) => lang !== "default");
+
+/**
+ * Returns the title of a language in the user's locale.
+ */
+function languageTitle(language: string): string {
+ return new Intl.DisplayNames([language], { type: "language", style: "short" }).of(language) || language;
+}
+
+export const languageAddon: Addon = {
+ title: "Language Selector",
+ type: types.TOOL,
+ render: ({ active }) => {
+ const [globals, updateGlobals] = useGlobals();
+ const selectedLanguage = globals.language || "en";
+
+ return (
+ {
+ return (
+ ({
+ id: language,
+ title: languageTitle(language),
+ active: selectedLanguage === language,
+ onClick: async () => {
+ // Update the global state with the selected language
+ updateGlobals({ language });
+ onHide();
+ },
+ }))}
+ />
+ );
+ }}
+ >
+
+
+ {languageTitle(selectedLanguage)}
+
+
+ );
+ },
+};
diff --git a/packages/shared-components/.storybook/main.ts b/packages/shared-components/.storybook/main.ts
new file mode 100644
index 0000000000..efc35752b5
--- /dev/null
+++ b/packages/shared-components/.storybook/main.ts
@@ -0,0 +1,46 @@
+/*
+Copyright 2025 New Vector Ltd.
+
+SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+Please see LICENSE files in the repository root for full details.
+*/
+
+import type { StorybookConfig } from "@storybook/react-vite";
+import path from "node:path";
+import { nodePolyfills } from "vite-plugin-node-polyfills";
+import { mergeConfig } from "vite";
+
+const config: StorybookConfig = {
+ stories: ["../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
+ staticDirs: ["../../../webapp"],
+ addons: ["@storybook/addon-docs", "@storybook/addon-designs", "@storybook/addon-a11y"],
+ framework: "@storybook/react-vite",
+ core: {
+ disableTelemetry: true,
+ },
+ typescript: {
+ reactDocgen: "react-docgen-typescript",
+ },
+ async viteFinal(config) {
+ return mergeConfig(config, {
+ resolve: {
+ alias: {
+ // Alias used by i18n.tsx
+ $webapp: path.resolve("../../webapp"),
+ },
+ },
+ // Needed for counterpart to work
+ plugins: [nodePolyfills({ include: ["process", "util"] })],
+ server: {
+ allowedHosts: ["localhost", ".docker.internal"],
+ },
+ });
+ },
+ refs: {
+ "compound-web": {
+ title: "Compound Web",
+ url: "https://element-hq.github.io/compound-web/",
+ },
+ },
+};
+export default config;
diff --git a/packages/shared-components/.storybook/manager.js b/packages/shared-components/.storybook/manager.js
new file mode 100644
index 0000000000..1b08ef7825
--- /dev/null
+++ b/packages/shared-components/.storybook/manager.js
@@ -0,0 +1,18 @@
+/*
+Copyright 2025 New Vector Ltd.
+
+SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+Please see LICENSE files in the repository root for full details.
+*/
+
+import React from "react";
+
+import { addons } from "storybook/manager-api";
+import ElementTheme from "./ElementTheme";
+import { languageAddon } from "./languageAddon";
+
+addons.setConfig({
+ theme: ElementTheme,
+});
+
+addons.register("elementhq/language", () => addons.add("language", languageAddon));
diff --git a/packages/shared-components/.storybook/preview.css b/packages/shared-components/.storybook/preview.css
new file mode 100644
index 0000000000..9f49585937
--- /dev/null
+++ b/packages/shared-components/.storybook/preview.css
@@ -0,0 +1,10 @@
+/*
+Copyright 2025 New Vector Ltd.
+
+SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+Please see LICENSE files in the repository root for full details.
+*/
+
+.docs-story {
+ background: var(--cpd-color-bg-canvas-default);
+}
diff --git a/packages/shared-components/.storybook/preview.tsx b/packages/shared-components/.storybook/preview.tsx
new file mode 100644
index 0000000000..91a6df646e
--- /dev/null
+++ b/packages/shared-components/.storybook/preview.tsx
@@ -0,0 +1,102 @@
+import type { ArgTypes, Preview, Decorator, ReactRenderer, StrictArgs } from "@storybook/react-vite";
+
+import "../../../res/css/shared.pcss";
+import "./preview.css";
+import React, { useLayoutEffect } from "react";
+import { setLanguage } from "../src/utils/i18n";
+import { TooltipProvider } from "@vector-im/compound-web";
+import { StoryContext } from "storybook/internal/csf";
+import { I18nApi, I18nContext } from "../src";
+
+export const globalTypes = {
+ theme: {
+ name: "Theme",
+ description: "Global theme for components",
+ toolbar: {
+ icon: "circlehollow",
+ title: "Theme",
+ items: [
+ { title: "System", value: "system", icon: "browser" },
+ { title: "Light", value: "light", icon: "sun" },
+ { title: "Light (high contrast)", value: "light-hc", icon: "sun" },
+ { title: "Dark", value: "dark", icon: "moon" },
+ { title: "Dark (high contrast)", value: "dark-hc", icon: "moon" },
+ ],
+ },
+ },
+ language: {
+ name: "Language",
+ description: "Global language for components",
+ },
+ initialGlobals: {
+ theme: "system",
+ language: "en",
+ },
+} satisfies ArgTypes;
+
+const allThemesClasses = globalTypes.theme.toolbar.items.map(({ value }) => `cpd-theme-${value}`);
+
+const ThemeSwitcher: React.FC<{
+ theme: string;
+}> = ({ theme }) => {
+ useLayoutEffect(() => {
+ document.documentElement.classList.remove(...allThemesClasses);
+ if (theme !== "system") {
+ document.documentElement.classList.add(`cpd-theme-${theme}`);
+ }
+ return () => document.documentElement.classList.remove(...allThemesClasses);
+ }, [theme]);
+
+ return null;
+};
+
+const withThemeProvider: Decorator = (Story, context) => {
+ return (
+ <>
+
+
+ >
+ );
+};
+
+async function languageLoader(context: StoryContext): Promise {
+ await setLanguage(context.globals.language);
+}
+
+const withTooltipProvider: Decorator = (Story) => {
+ return (
+
+
+
+ );
+};
+
+const withI18nProvider: Decorator = (Story) => {
+ return (
+
+
+
+ );
+};
+
+const preview: Preview = {
+ tags: ["autodocs"],
+ decorators: [withThemeProvider, withTooltipProvider, withI18nProvider],
+ parameters: {
+ options: {
+ storySort: {
+ method: "alphabetical",
+ },
+ },
+ a11y: {
+ /*
+ * Configure test behavior
+ * See: https://storybook.js.org/docs/next/writing-tests/accessibility-testing#test-behavior
+ */
+ test: "error",
+ },
+ },
+ loaders: [languageLoader],
+};
+
+export default preview;
diff --git a/packages/shared-components/.storybook/test-runner.ts b/packages/shared-components/.storybook/test-runner.ts
new file mode 100644
index 0000000000..5f0748115a
--- /dev/null
+++ b/packages/shared-components/.storybook/test-runner.ts
@@ -0,0 +1,34 @@
+/*
+Copyright 2025 New Vector Ltd.
+
+SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+Please see LICENSE files in the repository root for full details.
+*/
+
+import { waitForPageReady, TestRunnerConfig } from "@storybook/test-runner";
+import { toMatchImageSnapshot } from "jest-image-snapshot";
+
+const customSnapshotsDir = `${process.cwd()}/playwright/snapshots/`;
+const customReceivedDir = `${process.cwd()}/playwright/received/`;
+
+const config: TestRunnerConfig = {
+ setup() {
+ expect.extend({ toMatchImageSnapshot });
+ },
+ async postVisit(page, context) {
+ await waitForPageReady(page);
+
+ // If you want to take screenshot of multiple browsers, use
+ // page.context().browser().browserType().name() to get the browser name to prefix the file name
+ const image = await page.screenshot();
+ expect(image).toMatchImageSnapshot({
+ customSnapshotsDir,
+ customSnapshotIdentifier: `${context.id}-${process.platform}`,
+ storeReceivedOnFailure: true,
+ customReceivedDir,
+ customDiffDir: customReceivedDir,
+ });
+ },
+};
+
+export default config;
diff --git a/packages/shared-components/babel.config.js b/packages/shared-components/babel.config.js
new file mode 100644
index 0000000000..02ff2e43fe
--- /dev/null
+++ b/packages/shared-components/babel.config.js
@@ -0,0 +1,21 @@
+module.exports = {
+ sourceMaps: true,
+ presets: [
+ [
+ "@babel/preset-env",
+ {
+ include: ["@babel/plugin-transform-class-properties"],
+ },
+ ],
+ ["@babel/preset-typescript", { allowDeclareFields: true }],
+ "@babel/preset-react",
+ ],
+ plugins: [
+ "@babel/plugin-proposal-export-default-from",
+ "@babel/plugin-transform-numeric-separator",
+ "@babel/plugin-transform-object-rest-spread",
+ "@babel/plugin-transform-optional-chaining",
+ "@babel/plugin-transform-nullish-coalescing-operator",
+ "@babel/plugin-transform-runtime",
+ ],
+};
diff --git a/packages/shared-components/jest.config.ts b/packages/shared-components/jest.config.ts
new file mode 100644
index 0000000000..30a96a6cb1
--- /dev/null
+++ b/packages/shared-components/jest.config.ts
@@ -0,0 +1,58 @@
+/*
+Copyright 2025 Element Creations Ltd.
+
+SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+Please see LICENSE files in the repository root for full details.
+*/
+
+import { env } from "process";
+
+import type { Config } from "jest";
+
+const config: Config = {
+ testEnvironment: "jsdom",
+ testEnvironmentOptions: {
+ url: "http://localhost/",
+ },
+ testMatch: ["/src/**/*.test.[tj]s?(x)"],
+ setupFilesAfterEnv: ["/src/test/setupTests.ts"],
+ moduleNameMapper: {
+ // Support CSS module
+ "\\.(module.css)$": "identity-obj-proxy",
+ "\\.(css|scss|pcss)$": "/__mocks__/cssMock.js",
+ "\\.(gif|png|ttf|woff2)$": "/__mocks__/imageMock.js",
+ "\\.svg$": "/__mocks__/svg.js",
+ "\\$webapp/i18n/languages.json": "/../../__mocks__/languages.json",
+ "^react$": "/node_modules/react",
+ "^react-dom$": "/node_modules/react-dom",
+ "waveWorker\\.min\\.js": "/__mocks__/empty.js",
+ "context-filter-polyfill": "/__mocks__/empty.js",
+ "workers/(.+)Factory": "/__mocks__/workerFactoryMock.js",
+ },
+ transformIgnorePatterns: [
+ "/node_modules/(?!(mime|matrix-js-sdk|uuid|p-retry|is-network-error|react-merge-refs|@storybook|storybook)).+$",
+ ],
+ collectCoverageFrom: [
+ "/src/**/*.{js,ts,tsx}",
+ "/packages/**/*.{js,ts,tsx}",
+ // Coverage chokes on type definition files
+ "!/src/**/*.d.ts",
+ ],
+ coverageReporters: ["text-summary", "lcov"],
+ testResultsProcessor: "@casualbot/jest-sonar-reporter",
+ prettierPath: null,
+ moduleDirectories: ["node_modules", "./src/test/utils"],
+};
+
+// if we're running under GHA, enable the GHA reporter
+if (env["GITHUB_ACTIONS"] !== undefined) {
+ const reporters: Config["reporters"] = [["github-actions", { silent: false }], "summary"];
+
+ // if we're running against the develop branch, also enable the slow test reporter
+ if (env["GITHUB_REF"] == "refs/heads/develop") {
+ reporters.push("/../../test/slowReporter.cjs");
+ }
+ config.reporters = reporters;
+}
+
+export default config;
diff --git a/packages/shared-components/package.json b/packages/shared-components/package.json
new file mode 100644
index 0000000000..38c186a243
--- /dev/null
+++ b/packages/shared-components/package.json
@@ -0,0 +1,98 @@
+{
+ "name": "@element-hq/web-shared-components",
+ "version": "0.0.0-test.12",
+ "description": "Shared components for Element",
+ "author": "New Vector Ltd.",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/element-hq/element-web"
+ },
+ "exports": {
+ ".": {
+ "require": {
+ "style": "./dist/element-web-shared-components.css",
+ "types": "./dist/element-web-shared-components.d.ts",
+ "default": "./dist/element-web-shared-components.umd.js"
+ },
+ "import": {
+ "style": "./dist/element-web-shared-components.css",
+ "types": "./dist/element-web-shared-components.d.ts",
+ "default": "./dist/element-web-shared-components.mjs"
+ }
+ },
+ "./dist/element-web-shared-components.css": {
+ "require": "./dist/element-web-shared-components.css",
+ "import": "./dist/element-web-shared-components.css"
+ }
+ },
+ "types": "dist/element-web-shared-components.d.ts",
+ "files": [
+ "dist",
+ "src",
+ "LICENSE",
+ "README.md",
+ "package.json"
+ ],
+ "scripts": {
+ "test": "jest",
+ "prepare": "patch-package && yarn --cwd ../.. build:res && node scripts/gatherTranslationKeys.ts && vite build",
+ "storybook": "storybook dev -p 6007",
+ "build-storybook": "storybook build",
+ "lint": "yarn lint:types && yarn lint:js",
+ "lint:js": "eslint --max-warnings 0 src && prettier --check .",
+ "lint:types": "tsc --noEmit --jsx react",
+ "test:storybook": "test-storybook --url http://localhost:6007/",
+ "test:storybook:ci": "concurrently -k -s first -n \"SB,TEST\" \"yarn storybook --no-open\" \"wait-on tcp:6007 && yarn test-storybook --url http://localhost:6007/ --ci --maxWorkers=2\"",
+ "test:storybook:update": "playwright-screenshots --entrypoint /work/scripts/storybook-screenshot-update.sh --with-node-modules"
+ },
+ "resolutions": {
+ "playwright": "1.57.0"
+ },
+ "dependencies": {
+ "@element-hq/element-web-module-api": "^1.8.0",
+ "@vector-im/compound-design-tokens": "^6.3.0",
+ "classnames": "^2.5.1",
+ "counterpart": "^0.18.6",
+ "lodash": "^4.17.21",
+ "matrix-web-i18n": "^3.4.0",
+ "patch-package": "^8.0.1",
+ "react-merge-refs": "^3.0.2",
+ "temporal-polyfill": "^0.3.0"
+ },
+ "devDependencies": {
+ "@element-hq/element-web-playwright-common": "^2.0.0",
+ "@playwright/test": "1.57.0",
+ "@storybook/addon-a11y": "^10.0.7",
+ "@storybook/addon-designs": "^11.0.1",
+ "@storybook/addon-docs": "^10.0.7",
+ "@storybook/icons": "^2.0.0",
+ "@storybook/react-vite": "^10.0.7",
+ "@storybook/test-runner": "^0.24.1",
+ "@testing-library/dom": "^10.4.1",
+ "@testing-library/react": "^16.3.0",
+ "@types/counterpart": "^0.18.4",
+ "@types/jest-image-snapshot": "^6.4.0",
+ "@types/lodash": "^4.17.20",
+ "@types/react": "^19.2.2",
+ "concurrently": "^9.2.1",
+ "eslint": "8",
+ "eslint-plugin-matrix-org": "^3.0.0",
+ "eslint-plugin-storybook": "^10.0.7",
+ "jest": "^30.2.0",
+ "jest-image-snapshot": "^6.5.1",
+ "patch-package": "^8.0.1",
+ "prettier": "^3.6.2",
+ "storybook": "^10.0.7",
+ "typescript": "^5.9.3",
+ "vite": "^7.1.9",
+ "vite-plugin-dts": "^4.5.4",
+ "vite-plugin-node-polyfills": "^0.24.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
+ "peerDependencies": {
+ "@vector-im/compound-web": "^8.2.5"
+ }
+}
diff --git a/packages/shared-components/patches/@types+mdx+2.0.13.patch b/packages/shared-components/patches/@types+mdx+2.0.13.patch
new file mode 100644
index 0000000000..d3d02974f7
--- /dev/null
+++ b/packages/shared-components/patches/@types+mdx+2.0.13.patch
@@ -0,0 +1,46 @@
+diff --git a/node_modules/@types/mdx/types.d.ts b/node_modules/@types/mdx/types.d.ts
+index 498bb69..4e89216 100644
+--- a/node_modules/@types/mdx/types.d.ts
++++ b/node_modules/@types/mdx/types.d.ts
+@@ -5,7 +5,7 @@
+ */
+ // @ts-ignore JSX runtimes may optionally define JSX.ElementType. The MDX types need to work regardless whether this is
+ // defined or not.
+-type ElementType = any extends JSX.ElementType ? never : JSX.ElementType;
++type ElementType = any extends JSX.ElementType ? never : React.JSX.ElementType;
+
+ /**
+ * This matches any function component types that ar part of `ElementType`.
+@@ -20,12 +20,12 @@ type ClassElementType = Extract) =>
+ /**
+ * A valid JSX string component.
+ */
+-type StringComponent = Extract;
++type StringComponent = Extract;
+
+ /**
+ * A JSX element returned by MDX content.
+ */
+-export type Element = JSX.Element;
++export type Element = React.JSX.Element;
+
+ /**
+ * A valid JSX function component.
+@@ -44,7 +44,7 @@ type FunctionComponent = ElementType extends never
+ */
+ type ClassComponent = ElementType extends never
+ // If JSX.ElementType isn’t defined, the valid return type is a constructor that returns JSX.ElementClass
+- ? new(props: Props) => JSX.ElementClass
++ ? new(props: Props) => React.JSX.ElementClass
+ : ClassElementType extends never
+ // If JSX.ElementType is defined, but doesn’t allow constructors, function components are disallowed.
+ ? never
+@@ -70,7 +70,7 @@ interface NestedMDXComponents {
+ export type MDXComponents =
+ & NestedMDXComponents
+ & {
+- [Key in StringComponent]?: Component;
++ [Key in StringComponent]?: Component;
+ }
+ & {
+ /**
diff --git a/packages/shared-components/playwright/snapshots/audio-audioplayerview--default-linux.png b/packages/shared-components/playwright/snapshots/audio-audioplayerview--default-linux.png
new file mode 100644
index 0000000000..e4d6a84d23
Binary files /dev/null and b/packages/shared-components/playwright/snapshots/audio-audioplayerview--default-linux.png differ
diff --git a/packages/shared-components/playwright/snapshots/audio-audioplayerview--has-error-linux.png b/packages/shared-components/playwright/snapshots/audio-audioplayerview--has-error-linux.png
new file mode 100644
index 0000000000..36684675f7
Binary files /dev/null and b/packages/shared-components/playwright/snapshots/audio-audioplayerview--has-error-linux.png differ
diff --git a/packages/shared-components/playwright/snapshots/audio-audioplayerview--no-media-name-linux.png b/packages/shared-components/playwright/snapshots/audio-audioplayerview--no-media-name-linux.png
new file mode 100644
index 0000000000..c46e59ac21
Binary files /dev/null and b/packages/shared-components/playwright/snapshots/audio-audioplayerview--no-media-name-linux.png differ
diff --git a/packages/shared-components/playwright/snapshots/audio-audioplayerview--no-size-linux.png b/packages/shared-components/playwright/snapshots/audio-audioplayerview--no-size-linux.png
new file mode 100644
index 0000000000..928f6f0197
Binary files /dev/null and b/packages/shared-components/playwright/snapshots/audio-audioplayerview--no-size-linux.png differ
diff --git a/packages/shared-components/playwright/snapshots/audio-clock--default-linux.png b/packages/shared-components/playwright/snapshots/audio-clock--default-linux.png
new file mode 100644
index 0000000000..be66f4b70c
Binary files /dev/null and b/packages/shared-components/playwright/snapshots/audio-clock--default-linux.png differ
diff --git a/packages/shared-components/playwright/snapshots/audio-clock--lot-of-seconds-linux.png b/packages/shared-components/playwright/snapshots/audio-clock--lot-of-seconds-linux.png
new file mode 100644
index 0000000000..b4879e1a0c
Binary files /dev/null and b/packages/shared-components/playwright/snapshots/audio-clock--lot-of-seconds-linux.png differ
diff --git a/packages/shared-components/playwright/snapshots/audio-playpausebutton--default-linux.png b/packages/shared-components/playwright/snapshots/audio-playpausebutton--default-linux.png
new file mode 100644
index 0000000000..8c8baa7a65
Binary files /dev/null and b/packages/shared-components/playwright/snapshots/audio-playpausebutton--default-linux.png differ
diff --git a/packages/shared-components/playwright/snapshots/audio-playpausebutton--playing-linux.png b/packages/shared-components/playwright/snapshots/audio-playpausebutton--playing-linux.png
new file mode 100644
index 0000000000..53d58a4c2f
Binary files /dev/null and b/packages/shared-components/playwright/snapshots/audio-playpausebutton--playing-linux.png differ
diff --git a/packages/shared-components/playwright/snapshots/audio-seekbar--default-linux.png b/packages/shared-components/playwright/snapshots/audio-seekbar--default-linux.png
new file mode 100644
index 0000000000..60e51020cf
Binary files /dev/null and b/packages/shared-components/playwright/snapshots/audio-seekbar--default-linux.png differ
diff --git a/packages/shared-components/playwright/snapshots/audio-seekbar--disabled-linux.png b/packages/shared-components/playwright/snapshots/audio-seekbar--disabled-linux.png
new file mode 100644
index 0000000000..128f7e2ee5
Binary files /dev/null and b/packages/shared-components/playwright/snapshots/audio-seekbar--disabled-linux.png differ
diff --git a/packages/shared-components/playwright/snapshots/avatar-avatarwithdetails--default-linux.png b/packages/shared-components/playwright/snapshots/avatar-avatarwithdetails--default-linux.png
new file mode 100644
index 0000000000..ae339219e5
Binary files /dev/null and b/packages/shared-components/playwright/snapshots/avatar-avatarwithdetails--default-linux.png differ
diff --git a/packages/shared-components/playwright/snapshots/composer-historyvisiblebannerview--default-linux.png b/packages/shared-components/playwright/snapshots/composer-historyvisiblebannerview--default-linux.png
new file mode 100644
index 0000000000..7ab93a0cb8
Binary files /dev/null and b/packages/shared-components/playwright/snapshots/composer-historyvisiblebannerview--default-linux.png differ
diff --git a/packages/shared-components/playwright/snapshots/event-textualevent--default-linux.png b/packages/shared-components/playwright/snapshots/event-textualevent--default-linux.png
new file mode 100644
index 0000000000..16855e8448
Binary files /dev/null and b/packages/shared-components/playwright/snapshots/event-textualevent--default-linux.png differ
diff --git a/packages/shared-components/playwright/snapshots/messagebody-mediabody--default-linux.png b/packages/shared-components/playwright/snapshots/messagebody-mediabody--default-linux.png
new file mode 100644
index 0000000000..56b8072d2d
Binary files /dev/null and b/packages/shared-components/playwright/snapshots/messagebody-mediabody--default-linux.png differ
diff --git a/packages/shared-components/playwright/snapshots/pillinput-pill--default-linux.png b/packages/shared-components/playwright/snapshots/pillinput-pill--default-linux.png
new file mode 100644
index 0000000000..cd8eb65ba1
Binary files /dev/null and b/packages/shared-components/playwright/snapshots/pillinput-pill--default-linux.png differ
diff --git a/packages/shared-components/playwright/snapshots/pillinput-pill--without-close-button-linux.png b/packages/shared-components/playwright/snapshots/pillinput-pill--without-close-button-linux.png
new file mode 100644
index 0000000000..451de895dc
Binary files /dev/null and b/packages/shared-components/playwright/snapshots/pillinput-pill--without-close-button-linux.png differ
diff --git a/packages/shared-components/playwright/snapshots/pillinput-pillinput--default-linux.png b/packages/shared-components/playwright/snapshots/pillinput-pillinput--default-linux.png
new file mode 100644
index 0000000000..93bf6317a9
Binary files /dev/null and b/packages/shared-components/playwright/snapshots/pillinput-pillinput--default-linux.png differ
diff --git a/packages/shared-components/playwright/snapshots/pillinput-pillinput--no-child-linux.png b/packages/shared-components/playwright/snapshots/pillinput-pillinput--no-child-linux.png
new file mode 100644
index 0000000000..a303e726b5
Binary files /dev/null and b/packages/shared-components/playwright/snapshots/pillinput-pillinput--no-child-linux.png differ
diff --git a/packages/shared-components/playwright/snapshots/richlist-richitem--default-linux.png b/packages/shared-components/playwright/snapshots/richlist-richitem--default-linux.png
new file mode 100644
index 0000000000..9a5ad9eb0f
Binary files /dev/null and b/packages/shared-components/playwright/snapshots/richlist-richitem--default-linux.png differ
diff --git a/packages/shared-components/playwright/snapshots/richlist-richitem--hover-linux.png b/packages/shared-components/playwright/snapshots/richlist-richitem--hover-linux.png
new file mode 100644
index 0000000000..9a5ad9eb0f
Binary files /dev/null and b/packages/shared-components/playwright/snapshots/richlist-richitem--hover-linux.png differ
diff --git a/packages/shared-components/playwright/snapshots/richlist-richitem--selected-linux.png b/packages/shared-components/playwright/snapshots/richlist-richitem--selected-linux.png
new file mode 100644
index 0000000000..f9c92b066f
Binary files /dev/null and b/packages/shared-components/playwright/snapshots/richlist-richitem--selected-linux.png differ
diff --git a/packages/shared-components/playwright/snapshots/richlist-richitem--separator-linux.png b/packages/shared-components/playwright/snapshots/richlist-richitem--separator-linux.png
new file mode 100644
index 0000000000..a405f35902
Binary files /dev/null and b/packages/shared-components/playwright/snapshots/richlist-richitem--separator-linux.png differ
diff --git a/packages/shared-components/playwright/snapshots/richlist-richitem--without-timestamp-linux.png b/packages/shared-components/playwright/snapshots/richlist-richitem--without-timestamp-linux.png
new file mode 100644
index 0000000000..de5ecda7c3
Binary files /dev/null and b/packages/shared-components/playwright/snapshots/richlist-richitem--without-timestamp-linux.png differ
diff --git a/packages/shared-components/playwright/snapshots/richlist-richlist--default-linux.png b/packages/shared-components/playwright/snapshots/richlist-richlist--default-linux.png
new file mode 100644
index 0000000000..7919f356df
Binary files /dev/null and b/packages/shared-components/playwright/snapshots/richlist-richlist--default-linux.png differ
diff --git a/packages/shared-components/playwright/snapshots/richlist-richlist--empty-linux.png b/packages/shared-components/playwright/snapshots/richlist-richlist--empty-linux.png
new file mode 100644
index 0000000000..f655ecab96
Binary files /dev/null and b/packages/shared-components/playwright/snapshots/richlist-richlist--empty-linux.png differ
diff --git a/packages/shared-components/playwright/snapshots/room-banner--critical-linux.png b/packages/shared-components/playwright/snapshots/room-banner--critical-linux.png
new file mode 100644
index 0000000000..d920c59664
Binary files /dev/null and b/packages/shared-components/playwright/snapshots/room-banner--critical-linux.png differ
diff --git a/packages/shared-components/playwright/snapshots/room-banner--default-linux.png b/packages/shared-components/playwright/snapshots/room-banner--default-linux.png
new file mode 100644
index 0000000000..39079994d2
Binary files /dev/null and b/packages/shared-components/playwright/snapshots/room-banner--default-linux.png differ
diff --git a/packages/shared-components/playwright/snapshots/room-banner--info-linux.png b/packages/shared-components/playwright/snapshots/room-banner--info-linux.png
new file mode 100644
index 0000000000..7d93288e42
Binary files /dev/null and b/packages/shared-components/playwright/snapshots/room-banner--info-linux.png differ
diff --git a/packages/shared-components/playwright/snapshots/room-banner--success-linux.png b/packages/shared-components/playwright/snapshots/room-banner--success-linux.png
new file mode 100644
index 0000000000..8417f0aee3
Binary files /dev/null and b/packages/shared-components/playwright/snapshots/room-banner--success-linux.png differ
diff --git a/packages/shared-components/playwright/snapshots/room-banner--with-action-linux.png b/packages/shared-components/playwright/snapshots/room-banner--with-action-linux.png
new file mode 100644
index 0000000000..b3968f9db7
Binary files /dev/null and b/packages/shared-components/playwright/snapshots/room-banner--with-action-linux.png differ
diff --git a/packages/shared-components/playwright/snapshots/room-banner--with-avatar-image-linux.png b/packages/shared-components/playwright/snapshots/room-banner--with-avatar-image-linux.png
new file mode 100644
index 0000000000..d015a12a00
Binary files /dev/null and b/packages/shared-components/playwright/snapshots/room-banner--with-avatar-image-linux.png differ
diff --git a/packages/shared-components/playwright/snapshots/room-banner--with-loads-of-content-linux.png b/packages/shared-components/playwright/snapshots/room-banner--with-loads-of-content-linux.png
new file mode 100644
index 0000000000..85372e48fa
Binary files /dev/null and b/packages/shared-components/playwright/snapshots/room-banner--with-loads-of-content-linux.png differ
diff --git a/packages/shared-components/playwright/snapshots/room-banner--without-close-linux.png b/packages/shared-components/playwright/snapshots/room-banner--without-close-linux.png
new file mode 100644
index 0000000000..9645bd1d8a
Binary files /dev/null and b/packages/shared-components/playwright/snapshots/room-banner--without-close-linux.png differ
diff --git a/packages/shared-components/playwright/snapshots/room-list-roomlistsearchview--all-buttons-linux.png b/packages/shared-components/playwright/snapshots/room-list-roomlistsearchview--all-buttons-linux.png
new file mode 100644
index 0000000000..0d7f17d511
Binary files /dev/null and b/packages/shared-components/playwright/snapshots/room-list-roomlistsearchview--all-buttons-linux.png differ
diff --git a/packages/shared-components/playwright/snapshots/room-list-roomlistsearchview--default-linux.png b/packages/shared-components/playwright/snapshots/room-list-roomlistsearchview--default-linux.png
new file mode 100644
index 0000000000..2f41682551
Binary files /dev/null and b/packages/shared-components/playwright/snapshots/room-list-roomlistsearchview--default-linux.png differ
diff --git a/packages/shared-components/playwright/snapshots/room-list-roomlistsearchview--with-dial-pad-linux.png b/packages/shared-components/playwright/snapshots/room-list-roomlistsearchview--with-dial-pad-linux.png
new file mode 100644
index 0000000000..0d7f17d511
Binary files /dev/null and b/packages/shared-components/playwright/snapshots/room-list-roomlistsearchview--with-dial-pad-linux.png differ
diff --git a/packages/shared-components/playwright/snapshots/room-list-roomlistsearchview--without-explore-linux.png b/packages/shared-components/playwright/snapshots/room-list-roomlistsearchview--without-explore-linux.png
new file mode 100644
index 0000000000..e3e14ccfb4
Binary files /dev/null and b/packages/shared-components/playwright/snapshots/room-list-roomlistsearchview--without-explore-linux.png differ
diff --git a/packages/shared-components/scripts/gatherTranslationKeys.ts b/packages/shared-components/scripts/gatherTranslationKeys.ts
new file mode 100644
index 0000000000..37812df33b
--- /dev/null
+++ b/packages/shared-components/scripts/gatherTranslationKeys.ts
@@ -0,0 +1,67 @@
+/*
+Copyright 2025 Element Creations Ltd.
+
+SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+Please see LICENSE files in the repository root for full details.
+*/
+
+// Gathers all the translation keys from element-web's en_EN.json into a TypeScript type definition file
+// that exports a type `TranslationKey` which is a union of all supported translation keys.
+// This prevents having to import the json file and make typescript do the work as this results in vite-dts
+// generating an import to the json file in the .d.ts which doesn't work at runtime: this way, the type
+// gets put into the bundle.
+// XXX: It should *not* be in the 'src' directory, being a generated file, but if it isn't then the type
+// bundler won't bundle the types and will leave the file as a relative import, which will break.
+
+import * as fs from "fs";
+import * as path from "path";
+import { dirname } from "node:path";
+import { fileURLToPath } from "node:url";
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const i18nStringsPath = path.resolve(__dirname, "../../../src/i18n/strings/en_EN.json");
+const outPath = path.resolve(__dirname, "../src/i18nKeys.d.ts");
+
+function gatherKeys(obj: any, prefix: string[] = []): string[] {
+ if (typeof obj !== "object" || obj === null) return [];
+ let keys: string[] = [];
+ for (const key of Object.keys(obj)) {
+ const value = obj[key];
+
+ // add the path (for both leaves and intermediates as then we include plurals)
+ keys.push([...prefix, key].join("|"));
+ if (typeof value === "object" && value !== null) {
+ // If the value is an object, recurse
+ keys = keys.concat(gatherKeys(value, [...prefix, key]));
+ }
+ }
+ return keys;
+}
+
+function main() {
+ const json = JSON.parse(fs.readFileSync(i18nStringsPath, "utf8"));
+ const keys = gatherKeys(json);
+ const typeDef =
+ "/*\n" +
+ " * Copyright 2025 Element Creations Ltd.\n" +
+ " *\n" +
+ " * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial\n" +
+ " * Please see LICENSE files in the repository root for full details.\n" +
+ " */\n" +
+ "\n" +
+ "// This file is auto-generated by gatherTranslationKeys.ts\n" +
+ "// Do not edit manually.\n\n" +
+ "export type TranslationKey =\n" +
+ keys.map((k) => ` | \"${k}\"`).join("\n") +
+ ";\n";
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
+ fs.writeFileSync(outPath, typeDef, "utf8");
+ console.log(`Wrote ${keys.length} keys to ${outPath}`);
+}
+
+if (import.meta.url.startsWith("file:")) {
+ const modulePath = fileURLToPath(import.meta.url);
+ if (process.argv[1] === modulePath) {
+ main();
+ }
+}
diff --git a/packages/shared-components/scripts/storybook-screenshot-update.sh b/packages/shared-components/scripts/storybook-screenshot-update.sh
new file mode 100755
index 0000000000..6310d6887f
--- /dev/null
+++ b/packages/shared-components/scripts/storybook-screenshot-update.sh
@@ -0,0 +1,32 @@
+#!/bin/bash
+#
+# Update storybook screenshots
+#
+# This script should be used as the entrypoint parameter of the `playwright-screenshots` script. It
+# installs the yarn dependencies, and then runs `test-storybook` to update the storybook screenshots.
+#
+# It expects to find a storybook instance running at :6007 on the host machine. It also requires that
+# `playwright-screenshots` is given the `--with-node-modules` parameter.
+#
+# Example:
+#
+# test-storybook --url http://localhost:6007/
+# playwright-screenshots --entrypoint /work/scripts/storybook-screenshot-update.sh --with-node-modules
+#
+#
+# Note: even though this script is small, it is important because the alternative is running
+# `playwright-screenshots` twice in quick succession (once to do `yarn install`, a second to do the
+# actual updates): and that fails, because running `playwright-screenshots` without actually starting
+# Testcontainers leaves a ryuk container hanging around for up to 60s, which will block the second
+# invocation.
+
+set -e
+
+# First install dependencies. We have to do this within the playwright container rather than the host,
+# because we have which must be built for the right architecture (and some environments use a VM
+# to run docker containers, meaning that things inside a container use a different architecture than
+# those on the host).
+yarn
+
+# Now run the screenshot update
+/work/node_modules/.bin/test-storybook --url http://host.docker.internal:6007/ --updateSnapshot
diff --git a/packages/shared-components/src/@types/global.d.ts b/packages/shared-components/src/@types/global.d.ts
new file mode 100644
index 0000000000..1334c41737
--- /dev/null
+++ b/packages/shared-components/src/@types/global.d.ts
@@ -0,0 +1,8 @@
+/*
+Copyright 2025 New Vector Ltd.
+
+SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+Please see LICENSE files in the repository root for full details.
+*/
+
+declare module "*.css";
diff --git a/packages/shared-components/src/audio/AudioPlayerView/AudioPlayerView.module.css b/packages/shared-components/src/audio/AudioPlayerView/AudioPlayerView.module.css
new file mode 100644
index 0000000000..fc8c26aef7
--- /dev/null
+++ b/packages/shared-components/src/audio/AudioPlayerView/AudioPlayerView.module.css
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+.audioPlayer {
+ padding: var(--cpd-space-4x) var(--cpd-space-3x) var(--cpd-space-3x) var(--cpd-space-3x) !important;
+}
+
+.mediaInfo {
+ /* Makes the ellipsis on the file name work */
+ overflow: hidden;
+}
+
+.mediaName {
+ color: var(--cpd-color-text-primary);
+ font: var(--cpd-font-body-md-regular);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ max-width: 100%;
+}
+
+.byline {
+ font: var(--cpd-font-body-xs-regular);
+}
+
+.clock {
+ white-space: nowrap;
+}
+
+.error {
+ color: var(--cpd-color-text-critical-primary);
+}
diff --git a/packages/shared-components/src/audio/AudioPlayerView/AudioPlayerView.stories.tsx b/packages/shared-components/src/audio/AudioPlayerView/AudioPlayerView.stories.tsx
new file mode 100644
index 0000000000..18782f25cb
--- /dev/null
+++ b/packages/shared-components/src/audio/AudioPlayerView/AudioPlayerView.stories.tsx
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+import React, { type JSX } from "react";
+import { fn } from "storybook/test";
+
+import type { Meta, StoryFn } from "@storybook/react-vite";
+import { AudioPlayerView, type AudioPlayerViewActions, type AudioPlayerViewSnapshot } from "./AudioPlayerView";
+import { useMockedViewModel } from "../../useMockedViewModel";
+
+type AudioPlayerProps = AudioPlayerViewSnapshot & AudioPlayerViewActions;
+const AudioPlayerViewWrapper = ({ togglePlay, onKeyDown, onSeekbarChange, ...rest }: AudioPlayerProps): JSX.Element => {
+ const vm = useMockedViewModel(rest, {
+ togglePlay,
+ onKeyDown,
+ onSeekbarChange,
+ });
+ return ;
+};
+
+export default {
+ title: "Audio/AudioPlayerView",
+ component: AudioPlayerViewWrapper,
+ tags: ["autodocs"],
+ argTypes: {
+ playbackState: {
+ options: ["stopped", "playing", "paused", "decoding"],
+ control: { type: "select" },
+ },
+ },
+ args: {
+ mediaName: "Sample Audio",
+ durationSeconds: 300,
+ playedSeconds: 120,
+ percentComplete: 30,
+ playbackState: "stopped",
+ sizeBytes: 3500,
+ error: false,
+ togglePlay: fn(),
+ onKeyDown: fn(),
+ onSeekbarChange: fn(),
+ },
+} as Meta;
+
+const Template: StoryFn = (args) => ;
+
+export const Default = Template.bind({});
+
+export const NoMediaName = Template.bind({});
+NoMediaName.args = {
+ mediaName: undefined,
+};
+
+export const NoSize = Template.bind({});
+NoSize.args = {
+ sizeBytes: undefined,
+};
+
+export const HasError = Template.bind({});
+HasError.args = {
+ error: true,
+};
diff --git a/packages/shared-components/src/audio/AudioPlayerView/AudioPlayerView.test.tsx b/packages/shared-components/src/audio/AudioPlayerView/AudioPlayerView.test.tsx
new file mode 100644
index 0000000000..55e159f110
--- /dev/null
+++ b/packages/shared-components/src/audio/AudioPlayerView/AudioPlayerView.test.tsx
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+import { render, screen } from "jest-matrix-react";
+import { composeStories } from "@storybook/react-vite";
+import React from "react";
+import userEvent from "@testing-library/user-event";
+import { fireEvent } from "@testing-library/dom";
+
+import * as stories from "./AudioPlayerView.stories.tsx";
+import { AudioPlayerView, type AudioPlayerViewActions, type AudioPlayerViewSnapshot } from "./AudioPlayerView";
+import { MockViewModel } from "../../viewmodel/MockViewModel.ts";
+import { I18nContext } from "../../utils/i18nContext.ts";
+import { I18nApi } from "../../index.ts";
+
+const { Default, NoMediaName, NoSize, HasError } = composeStories(stories);
+
+describe("AudioPlayerView", () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it("renders the audio player in default state", () => {
+ const { container } = render();
+ expect(container).toMatchSnapshot();
+ });
+
+ it("renders the audio player without media name", () => {
+ const { container } = render();
+ expect(container).toMatchSnapshot();
+ });
+
+ it("renders the audio player without size", () => {
+ const { container } = render();
+ expect(container).toMatchSnapshot();
+ });
+
+ it("renders the audio player in error state", () => {
+ const { container } = render();
+ expect(container).toMatchSnapshot();
+ });
+
+ const onKeyDown = jest.fn();
+ const togglePlay = jest.fn();
+ const onSeekbarChange = jest.fn();
+
+ class AudioPlayerViewModel extends MockViewModel implements AudioPlayerViewActions {
+ public onKeyDown = onKeyDown;
+ public togglePlay = togglePlay;
+ public onSeekbarChange = onSeekbarChange;
+ }
+
+ it("should attach vm methods", async () => {
+ const user = userEvent.setup();
+ const vm = new AudioPlayerViewModel({
+ playbackState: "stopped",
+ mediaName: "Test Audio",
+ durationSeconds: 300,
+ playedSeconds: 120,
+ percentComplete: 30,
+ sizeBytes: 3500,
+ error: false,
+ });
+
+ render(, {
+ wrapper: ({ children }) => {children},
+ });
+ await user.click(screen.getByRole("button", { name: "Play" }));
+ expect(togglePlay).toHaveBeenCalled();
+
+ // user event doesn't support change events on sliders, so we use fireEvent
+ fireEvent.change(screen.getByRole("slider", { name: "Audio seek bar" }), { target: { value: "50" } });
+ expect(onSeekbarChange).toHaveBeenCalled();
+
+ await user.type(screen.getByLabelText("Audio player"), "{arrowup}");
+ expect(onKeyDown).toHaveBeenCalledWith(expect.objectContaining({ key: "ArrowUp" }));
+ });
+});
diff --git a/packages/shared-components/src/audio/AudioPlayerView/AudioPlayerView.tsx b/packages/shared-components/src/audio/AudioPlayerView/AudioPlayerView.tsx
new file mode 100644
index 0000000000..d3c9fb87ee
--- /dev/null
+++ b/packages/shared-components/src/audio/AudioPlayerView/AudioPlayerView.tsx
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+import React, { type ChangeEventHandler, type JSX, type KeyboardEventHandler, type MouseEventHandler } from "react";
+
+import { type ViewModel } from "../../viewmodel/ViewModel";
+import { useViewModel } from "../../useViewModel";
+import { MediaBody } from "../../message-body/MediaBody";
+import { Flex } from "../../utils/Flex";
+import styles from "./AudioPlayerView.module.css";
+import { PlayPauseButton } from "../PlayPauseButton";
+import { type PlaybackState } from "../playback";
+import { useI18n } from "../../utils/i18nContext";
+import { formatBytes } from "../../utils/FormattingUtils";
+import { Clock } from "../Clock";
+import { SeekBar } from "../SeekBar";
+
+export interface AudioPlayerViewSnapshot {
+ /**
+ * The playback state of the audio player.
+ */
+ playbackState: PlaybackState;
+ /**
+ * Name of the media being played.
+ * @default Fallback to "timeline|m.audio|unnamed_audio" string if not provided.
+ */
+ mediaName?: string;
+ /**
+ * Size of the audio file in bytes.
+ * Hided if not provided.
+ */
+ sizeBytes?: number;
+ /**
+ * The duration of the audio clip in seconds.
+ */
+ durationSeconds: number;
+ /**
+ * The percentage of the audio that has been played.
+ * Ranges from 0 to 100.
+ */
+ percentComplete: number;
+ /**
+ * The number of seconds that have been played.
+ */
+ playedSeconds: number;
+ /**
+ * Indicates if there was an error downloading the audio.
+ */
+ error: boolean;
+}
+
+export interface AudioPlayerViewActions {
+ /**
+ * Handles key down events for the audio player.
+ */
+ onKeyDown: KeyboardEventHandler;
+ /**
+ * Toggles the play/pause state of the audio player.
+ */
+ togglePlay: MouseEventHandler;
+ /**
+ * Handles changes to the seek bar.
+ */
+ onSeekbarChange: ChangeEventHandler;
+}
+
+/**
+ * The view model for the audio player.
+ */
+export type AudioPlayerViewModel = ViewModel & AudioPlayerViewActions;
+
+interface AudioPlayerViewProps {
+ /**
+ * The view model for the audio player.
+ */
+ vm: AudioPlayerViewModel;
+}
+
+/**
+ * AudioPlayer component displays an audio player with play/pause controls, seek bar, and media information.
+ * The component expects a view model that provides the current state of the audio playback,
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ */
+export function AudioPlayerView({ vm }: Readonly): JSX.Element {
+ const { translate: _t } = useI18n();
+
+ const {
+ playbackState,
+ mediaName = _t("timeline|m.audio|unnamed_audio"),
+ sizeBytes,
+ durationSeconds,
+ playedSeconds,
+ percentComplete,
+ error,
+ } = useViewModel(vm);
+ const fileSize = sizeBytes ? `(${formatBytes(sizeBytes)})` : null;
+ const disabled = playbackState === "decoding";
+
+ // tabIndex=0 to ensure that the whole component becomes a tab stop, where we handle keyboard
+ // events for accessibility
+ return (
+ <>
+
+
+
+
+
+ {mediaName}
+
+
+
+ {fileSize}
+
+
+
+
+
+
+
+
+ {error && {_t("timeline|m.audio|error_downloading_audio")}}
+ >
+ );
+}
diff --git a/packages/shared-components/src/audio/AudioPlayerView/__snapshots__/AudioPlayerView.test.tsx.snap b/packages/shared-components/src/audio/AudioPlayerView/__snapshots__/AudioPlayerView.test.tsx.snap
new file mode 100644
index 0000000000..85004e4ba5
--- /dev/null
+++ b/packages/shared-components/src/audio/AudioPlayerView/__snapshots__/AudioPlayerView.test.tsx.snap
@@ -0,0 +1,369 @@
+// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
+
+exports[`AudioPlayerView renders the audio player in default state 1`] = `
+
+
+
+
+
+
+ Sample Audio
+
+
+
+ (3.42 KB)
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`AudioPlayerView renders the audio player in error state 1`] = `
+
+
+
+
+
+
+ Sample Audio
+
+
+
+ (3.42 KB)
+
+
+
+
+
+
+
+
+
+ Error downloading audio
+
+
+`;
+
+exports[`AudioPlayerView renders the audio player without media name 1`] = `
+
+
+
+
+
+
+ Unnamed audio
+
+
+
+ (3.42 KB)
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`AudioPlayerView renders the audio player without size 1`] = `
+
+
+
+
+
+
+ Sample Audio
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/packages/shared-components/src/audio/AudioPlayerView/index.ts b/packages/shared-components/src/audio/AudioPlayerView/index.ts
new file mode 100644
index 0000000000..4075f81dde
--- /dev/null
+++ b/packages/shared-components/src/audio/AudioPlayerView/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+export type { AudioPlayerViewModel, AudioPlayerViewSnapshot } from "./AudioPlayerView";
+export { AudioPlayerView } from "./AudioPlayerView";
diff --git a/packages/shared-components/src/audio/Clock/Clock.stories.tsx b/packages/shared-components/src/audio/Clock/Clock.stories.tsx
new file mode 100644
index 0000000000..e05ee086d9
--- /dev/null
+++ b/packages/shared-components/src/audio/Clock/Clock.stories.tsx
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+import React from "react";
+
+import type { Meta, StoryFn } from "@storybook/react-vite";
+import { Clock } from "./Clock";
+
+export default {
+ title: "Audio/Clock",
+ component: Clock,
+ tags: ["autodocs"],
+ args: {
+ seconds: 20,
+ },
+} as Meta;
+
+const Template: StoryFn = (args) => ;
+
+export const Default = Template.bind({});
+
+export const LotOfSeconds = Template.bind({});
+LotOfSeconds.args = {
+ seconds: 99999999999999,
+};
diff --git a/packages/shared-components/src/audio/Clock/Clock.test.tsx b/packages/shared-components/src/audio/Clock/Clock.test.tsx
new file mode 100644
index 0000000000..fdbbf49518
--- /dev/null
+++ b/packages/shared-components/src/audio/Clock/Clock.test.tsx
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+import { composeStories } from "@storybook/react-vite";
+import { render } from "jest-matrix-react";
+import React from "react";
+
+import * as stories from "./Clock.stories.tsx";
+
+const { Default, LotOfSeconds } = composeStories(stories);
+
+describe("Clock", () => {
+ it("renders the clock", () => {
+ const { container } = render();
+ expect(container).toMatchSnapshot();
+ });
+
+ it("renders the clock with a lot of seconds", () => {
+ const { container } = render();
+ expect(container).toMatchSnapshot();
+ });
+});
diff --git a/src/components/views/audio_messages/Clock.tsx b/packages/shared-components/src/audio/Clock/Clock.tsx
similarity index 73%
rename from src/components/views/audio_messages/Clock.tsx
rename to packages/shared-components/src/audio/Clock/Clock.tsx
index d9f1a3d6c5..176044269d 100644
--- a/src/components/views/audio_messages/Clock.tsx
+++ b/packages/shared-components/src/audio/Clock/Clock.tsx
@@ -1,32 +1,26 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2021-2023 The Matrix.org Foundation C.I.C.
-
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React, { type HTMLProps } from "react";
import { Temporal } from "temporal-polyfill";
+import classNames from "classnames";
-import { formatSeconds } from "../../../DateUtils";
+import { formatSeconds } from "../../utils/DateUtils";
-interface Props extends Pick, "aria-live" | "role"> {
+export interface Props extends Pick, "aria-live" | "role" | "className"> {
seconds: number;
- formatFn: (seconds: number) => string;
}
/**
* Clock which represents time periods rather than absolute time.
- * Simply converts seconds using formatFn.
- * Defaulting to formatSeconds().
+ * Simply converts seconds using formatSeconds().
* Note that in this case hours will not be displayed, making it possible to see "82:29".
*/
-export default class Clock extends React.Component {
- public static defaultProps = {
- formatFn: formatSeconds,
- };
-
+export class Clock extends React.Component {
public shouldComponentUpdate(nextProps: Readonly): boolean {
const currentFloor = Math.floor(this.props.seconds);
const nextFloor = Math.floor(nextProps.seconds);
@@ -47,9 +41,10 @@ export default class Clock extends React.Component {
dateTime={this.calculateDuration(seconds)}
aria-live={this.props["aria-live"]}
role={role}
- className="mx_Clock"
+ /* Keep class for backward compatibility with parent component */
+ className={classNames("mx_Clock", this.props.className)}
>
- {this.props.formatFn(seconds)}
+ {formatSeconds(seconds)}
);
}
diff --git a/packages/shared-components/src/audio/Clock/__snapshots__/Clock.test.tsx.snap b/packages/shared-components/src/audio/Clock/__snapshots__/Clock.test.tsx.snap
new file mode 100644
index 0000000000..5ab0799d28
--- /dev/null
+++ b/packages/shared-components/src/audio/Clock/__snapshots__/Clock.test.tsx.snap
@@ -0,0 +1,23 @@
+// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
+
+exports[`Clock renders the clock 1`] = `
+
+
+
+`;
+
+exports[`Clock renders the clock with a lot of seconds 1`] = `
+
+
+
+`;
diff --git a/packages/shared-components/src/audio/Clock/index.tsx b/packages/shared-components/src/audio/Clock/index.tsx
new file mode 100644
index 0000000000..bc261bb283
--- /dev/null
+++ b/packages/shared-components/src/audio/Clock/index.tsx
@@ -0,0 +1,8 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+export { Clock } from "./Clock";
diff --git a/packages/shared-components/src/audio/PlayPauseButton/PlayPauseButton.module.css b/packages/shared-components/src/audio/PlayPauseButton/PlayPauseButton.module.css
new file mode 100644
index 0000000000..859f84bba9
--- /dev/null
+++ b/packages/shared-components/src/audio/PlayPauseButton/PlayPauseButton.module.css
@@ -0,0 +1,11 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+.button {
+ border-radius: 32px !important;
+ background-color: var(--cpd-color-bg-subtle-primary) !important;
+}
diff --git a/packages/shared-components/src/audio/PlayPauseButton/PlayPauseButton.stories.tsx b/packages/shared-components/src/audio/PlayPauseButton/PlayPauseButton.stories.tsx
new file mode 100644
index 0000000000..5bbcfbbf39
--- /dev/null
+++ b/packages/shared-components/src/audio/PlayPauseButton/PlayPauseButton.stories.tsx
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+import { fn } from "storybook/test";
+
+import { PlayPauseButton } from "./PlayPauseButton";
+import type { Meta, StoryObj } from "@storybook/react-vite";
+
+const meta = {
+ title: "Audio/PlayPauseButton",
+ component: PlayPauseButton,
+ tags: ["autodocs"],
+ args: {
+ togglePlay: fn(),
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
+export const Playing: Story = { args: { playing: true } };
diff --git a/packages/shared-components/src/audio/PlayPauseButton/PlayPauseButton.test.tsx b/packages/shared-components/src/audio/PlayPauseButton/PlayPauseButton.test.tsx
new file mode 100644
index 0000000000..3a032f5e02
--- /dev/null
+++ b/packages/shared-components/src/audio/PlayPauseButton/PlayPauseButton.test.tsx
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+import { composeStories } from "@storybook/react-vite";
+import { render } from "jest-matrix-react";
+import React from "react";
+import userEvent from "@testing-library/user-event";
+import { fn } from "storybook/test";
+
+import * as stories from "./PlayPauseButton.stories.tsx";
+
+const { Default, Playing } = composeStories(stories);
+
+describe("PlayPauseButton", () => {
+ it("renders the button in default state", () => {
+ const { container } = render();
+ expect(container).toMatchSnapshot();
+ });
+
+ it("renders the button in playing state", () => {
+ const { container } = render();
+ expect(container).toMatchSnapshot();
+ });
+
+ it("calls togglePlay when clicked", async () => {
+ const user = userEvent.setup();
+ const togglePlay = fn();
+
+ const { getByRole } = render();
+ await user.click(getByRole("button"));
+ expect(togglePlay).toHaveBeenCalled();
+ });
+});
diff --git a/packages/shared-components/src/audio/PlayPauseButton/PlayPauseButton.tsx b/packages/shared-components/src/audio/PlayPauseButton/PlayPauseButton.tsx
new file mode 100644
index 0000000000..cc2ab5bb65
--- /dev/null
+++ b/packages/shared-components/src/audio/PlayPauseButton/PlayPauseButton.tsx
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+import React, { type HTMLAttributes, type JSX, type MouseEventHandler } from "react";
+import { IconButton } from "@vector-im/compound-web";
+import Play from "@vector-im/compound-design-tokens/assets/web/icons/play-solid";
+import Pause from "@vector-im/compound-design-tokens/assets/web/icons/pause-solid";
+
+import styles from "./PlayPauseButton.module.css";
+import { useI18n } from "../../utils/i18nContext";
+
+export interface PlayPauseButtonProps extends HTMLAttributes {
+ /**
+ * Whether the button is disabled.
+ * @default false
+ */
+ disabled?: boolean;
+
+ /**
+ * Whether the audio is currently playing.
+ * @default false
+ */
+ playing?: boolean;
+
+ /**
+ * Function to toggle play/pause state.
+ */
+ togglePlay: MouseEventHandler;
+}
+
+/**
+ * A button component that toggles between play and pause states for audio playback.
+ *
+ * @example
+ * ```tsx
+ * {}} />
+ * ```
+ */
+export function PlayPauseButton({
+ disabled = false,
+ playing = false,
+ togglePlay,
+ ...rest
+}: Readonly): JSX.Element {
+ const { translate: _t } = useI18n();
+
+ const label = playing ? _t("action|pause") : _t("action|play");
+
+ return (
+
+ {playing ? : }
+
+ );
+}
diff --git a/packages/shared-components/src/audio/PlayPauseButton/__snapshots__/PlayPauseButton.test.tsx.snap b/packages/shared-components/src/audio/PlayPauseButton/__snapshots__/PlayPauseButton.test.tsx.snap
new file mode 100644
index 0000000000..3b98652cd1
--- /dev/null
+++ b/packages/shared-components/src/audio/PlayPauseButton/__snapshots__/PlayPauseButton.test.tsx.snap
@@ -0,0 +1,65 @@
+// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
+
+exports[`PlayPauseButton renders the button in default state 1`] = `
+
+
+
+`;
+
+exports[`PlayPauseButton renders the button in playing state 1`] = `
+
+
+
+`;
diff --git a/packages/shared-components/src/audio/PlayPauseButton/index.ts b/packages/shared-components/src/audio/PlayPauseButton/index.ts
new file mode 100644
index 0000000000..93a71cd739
--- /dev/null
+++ b/packages/shared-components/src/audio/PlayPauseButton/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+export { PlayPauseButton } from "./PlayPauseButton";
diff --git a/packages/shared-components/src/audio/SeekBar/SeekBar.module.css b/packages/shared-components/src/audio/SeekBar/SeekBar.module.css
new file mode 100644
index 0000000000..54e3b7479f
--- /dev/null
+++ b/packages/shared-components/src/audio/SeekBar/SeekBar.module.css
@@ -0,0 +1,99 @@
+/*
+Copyright 2024 New Vector Ltd.
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+Please see LICENSE files in the repository root for full details.
+*/
+
+/* CSS inspiration from: */
+/* * https://www.w3schools.com/howto/howto_js_rangeslider.asp */
+/* * https://stackoverflow.com/a/28283806 */
+/* * https://css-tricks.com/styling-cross-browser-compatible-range-inputs-css/ */
+
+.seekBar {
+ /* default, overridden in JS */
+ --fillTo: 1;
+
+ /* Dev note: we deliberately do not have the -ms-track (and friends) selectors because we don't */
+ /* need to support IE. */
+
+ appearance: none; /* default style override */
+
+ width: 100%;
+ height: 1px;
+ background: var(--cpd-color-gray-600);
+ outline: none; /* remove blue selection border */
+ position: relative; /* for before+after pseudo elements later on */
+
+ cursor: pointer;
+
+ &::-webkit-slider-thumb {
+ appearance: none; /* default style override */
+
+ /* Dev note: This needs to be duplicated with the -moz-range-thumb selector */
+ /* because otherwise Edge (webkit) will fail to see the styles and just refuse */
+ /* to apply them. */
+ width: 8px;
+ height: 8px;
+ border-radius: 8px;
+ background-color: var(--cpd-color-gray-800);
+ cursor: pointer;
+ }
+
+ &::-moz-range-thumb {
+ width: 8px;
+ height: 8px;
+ border-radius: 8px;
+ background-color: var(--cpd-color-gray-800);
+ cursor: pointer;
+
+ /* Firefox adds a border on the thumb */
+ border: none;
+ }
+
+ /* This is for webkit support, but we can't limit the functionality of it to just webkit */
+ /* browsers. Firefox responds to webkit-prefixed values now, which means we can't use media */
+ /* or support queries to selectively apply the rule. An upside is that this CSS doesn't work */
+ /* in firefox, so it's just wasted CPU/GPU time. */
+ &::before {
+ /* ::before to ensure it ends up under the thumb */
+ content: "";
+ background-color: var(--cpd-color-gray-800);
+
+ /* Absolute positioning to ensure it overlaps with the existing bar */
+ position: absolute;
+ top: 0;
+ left: 0;
+
+ /* Sizing to match the bar */
+ width: 100%;
+ height: 1px;
+
+ /* And finally dynamic width without overly hurting the rendering engine. */
+ transform-origin: 0 100%;
+ transform: scaleX(var(--fillTo));
+ }
+
+ /* This is firefox's built-in support for the above, with 100% less hacks. */
+ &::-moz-range-progress {
+ background-color: var(--cpd-color-gray-800);
+ height: 1px;
+ }
+
+ &:disabled {
+ opacity: 0.5;
+ }
+
+ /* Increase clickable area for the slider (approximately same size as browser default) */
+ /* We do it this way to keep the same padding and margins of the element, avoiding margin math. */
+ /* Source: https://front-back.com/expand-clickable-areas-for-a-better-touch-experience/ */
+ &::after {
+ content: "";
+ position: absolute;
+ top: -6px;
+ bottom: -6px;
+ left: 0;
+ right: 0;
+ }
+}
diff --git a/packages/shared-components/src/audio/SeekBar/SeekBar.stories.tsx b/packages/shared-components/src/audio/SeekBar/SeekBar.stories.tsx
new file mode 100644
index 0000000000..6a3ec48666
--- /dev/null
+++ b/packages/shared-components/src/audio/SeekBar/SeekBar.stories.tsx
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+import React from "react";
+import { useArgs } from "storybook/preview-api";
+
+import { SeekBar } from "./SeekBar";
+import type { Meta, StoryFn } from "@storybook/react-vite";
+
+export default {
+ title: "Audio/SeekBar",
+ component: SeekBar,
+ tags: ["autodocs"],
+ argTypes: {
+ value: {
+ control: { type: "range", min: 0, max: 100, step: 1 },
+ },
+ },
+ args: {
+ value: 50,
+ },
+} as Meta;
+
+const Template: StoryFn = (args) => {
+ const [, updateArgs] = useArgs();
+ return updateArgs({ value: parseInt(evt.target.value, 10) })} {...args} />;
+};
+
+export const Default = Template.bind({});
+
+export const Disabled = Template.bind({});
+Disabled.args = {
+ disabled: true,
+};
diff --git a/packages/shared-components/src/audio/SeekBar/SeekBar.test.tsx b/packages/shared-components/src/audio/SeekBar/SeekBar.test.tsx
new file mode 100644
index 0000000000..0d52ab3993
--- /dev/null
+++ b/packages/shared-components/src/audio/SeekBar/SeekBar.test.tsx
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+import { render } from "jest-matrix-react";
+import React from "react";
+import { composeStories } from "@storybook/react-vite";
+
+import * as stories from "./SeekBar.stories.tsx";
+const { Default } = composeStories(stories);
+
+describe("Seekbar", () => {
+ it("renders the clock", () => {
+ const { container } = render();
+ expect(container).toMatchSnapshot();
+ });
+});
diff --git a/packages/shared-components/src/audio/SeekBar/SeekBar.tsx b/packages/shared-components/src/audio/SeekBar/SeekBar.tsx
new file mode 100644
index 0000000000..a30e527eee
--- /dev/null
+++ b/packages/shared-components/src/audio/SeekBar/SeekBar.tsx
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+import React, { type CSSProperties, type JSX, useEffect, useMemo, useState } from "react";
+import { throttle } from "lodash";
+import classNames from "classnames";
+
+import style from "./SeekBar.module.css";
+import { useI18n } from "../../utils/i18nContext";
+
+export interface SeekBarProps extends React.InputHTMLAttributes {
+ /**
+ * The current value of the seek bar, between 0 and 100.
+ * @default 0
+ */
+ value?: number;
+}
+
+interface ISeekCSS extends CSSProperties {
+ "--fillTo": number;
+}
+
+/**
+ * A seek bar component for audio playback.
+ *
+ * @example
+ * ```tsx
+ * console.log("New value", e.target.value)} />
+ * ```
+ */
+export function SeekBar({ value = 0, className, ...rest }: Readonly): JSX.Element {
+ const { translate: _t } = useI18n();
+
+ const [newValue, setNewValue] = useState(value);
+ // Throttle the value setting to avoid excessive re-renders
+ const setThrottledValue = useMemo(() => throttle(setNewValue, 10), []);
+
+ useEffect(() => {
+ setThrottledValue(value);
+ }, [value, setThrottledValue]);
+
+ return (
+ e.stopPropagation()}
+ min={0}
+ max={100}
+ value={newValue}
+ step={1}
+ style={{ "--fillTo": newValue / 100 } as ISeekCSS}
+ aria-label={_t("a11y|seek_bar_label")}
+ {...rest}
+ />
+ );
+}
diff --git a/packages/shared-components/src/audio/SeekBar/__snapshots__/SeekBar.test.tsx.snap b/packages/shared-components/src/audio/SeekBar/__snapshots__/SeekBar.test.tsx.snap
new file mode 100644
index 0000000000..e3cc92279e
--- /dev/null
+++ b/packages/shared-components/src/audio/SeekBar/__snapshots__/SeekBar.test.tsx.snap
@@ -0,0 +1,16 @@
+// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
+
+exports[`Seekbar renders the clock 1`] = `
+
+
+
+`;
diff --git a/packages/shared-components/src/audio/SeekBar/index.ts b/packages/shared-components/src/audio/SeekBar/index.ts
new file mode 100644
index 0000000000..710310198e
--- /dev/null
+++ b/packages/shared-components/src/audio/SeekBar/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+export { SeekBar } from "./SeekBar";
diff --git a/packages/shared-components/src/audio/playback.ts b/packages/shared-components/src/audio/playback.ts
new file mode 100644
index 0000000000..d7b0fa7634
--- /dev/null
+++ b/packages/shared-components/src/audio/playback.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+/**
+ * Represents the possible states of playback.
+ * - "preparing": The audio is being prepared for playback (e.g., loading or buffering).
+ * - "decoding": The audio is being decoded and is not ready for playback.
+ * - "stopped": The playback has been stopped, with no progress on the timeline.
+ * - "paused": The playback is paused, with some progress on the timeline.
+ * - "playing": The playback is actively progressing through the timeline.
+ */
+export type PlaybackState = "decoding" | "stopped" | "paused" | "playing" | "preparing";
diff --git a/packages/shared-components/src/avatar/AvatarWithDetails/AvatarWithDetails.module.css b/packages/shared-components/src/avatar/AvatarWithDetails/AvatarWithDetails.module.css
new file mode 100644
index 0000000000..62e7a569bf
--- /dev/null
+++ b/packages/shared-components/src/avatar/AvatarWithDetails/AvatarWithDetails.module.css
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+.avatarWithDetails {
+ display: flex;
+ align-items: center;
+
+ border-radius: 12px;
+ background-color: var(--cpd-color-gray-200);
+ padding: var(--cpd-space-2x);
+ gap: var(--cpd-space-2x);
+
+ .title {
+ display: inline-block;
+
+ font-weight: var(--cpd-font-weight-semibold);
+ font-size: var(--cpd-font-size-body-md);
+
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .details {
+ font-size: var(--cpd-font-size-body-sm);
+ }
+}
diff --git a/packages/shared-components/src/avatar/AvatarWithDetails/AvatarWithDetails.stories.tsx b/packages/shared-components/src/avatar/AvatarWithDetails/AvatarWithDetails.stories.tsx
new file mode 100644
index 0000000000..9d8a7f5c30
--- /dev/null
+++ b/packages/shared-components/src/avatar/AvatarWithDetails/AvatarWithDetails.stories.tsx
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+import React from "react";
+import { type Meta, type StoryObj } from "@storybook/react-vite";
+
+import { AvatarWithDetails } from "./AvatarWithDetails";
+
+const meta = {
+ title: "Avatar/AvatarWithDetails",
+ component: AvatarWithDetails,
+ tags: ["autodocs"],
+ args: {
+ avatar: ,
+ details: "Details about the avatar go here",
+ title: "Room Name",
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+export const Default: Story = {};
diff --git a/packages/shared-components/src/avatar/AvatarWithDetails/AvatarWithDetails.test.tsx b/packages/shared-components/src/avatar/AvatarWithDetails/AvatarWithDetails.test.tsx
new file mode 100644
index 0000000000..f5d482613f
--- /dev/null
+++ b/packages/shared-components/src/avatar/AvatarWithDetails/AvatarWithDetails.test.tsx
@@ -0,0 +1,21 @@
+/*
+Copyright 2025 New Vector Ltd.
+
+SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+Please see LICENSE files in the repository root for full details.
+*/
+
+import { composeStories } from "@storybook/react-vite";
+import { render } from "jest-matrix-react";
+import React from "react";
+
+import * as stories from "./AvatarWithDetails.stories.tsx";
+
+const { Default } = composeStories(stories);
+
+describe("AvatarWithDetails", () => {
+ it("renders a textual event", () => {
+ const { container } = render();
+ expect(container).toMatchSnapshot();
+ });
+});
diff --git a/packages/shared-components/src/avatar/AvatarWithDetails/AvatarWithDetails.tsx b/packages/shared-components/src/avatar/AvatarWithDetails/AvatarWithDetails.tsx
new file mode 100644
index 0000000000..aab5729d5f
--- /dev/null
+++ b/packages/shared-components/src/avatar/AvatarWithDetails/AvatarWithDetails.tsx
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+import { type ComponentProps, type ElementType, type JSX, type PropsWithChildren } from "react";
+import React from "react";
+import classNames from "classnames";
+
+import styles from "./AvatarWithDetails.module.css";
+import { Flex } from "../../utils/Flex";
+
+export type AvatarWithDetailsProps = {
+ /**
+ * The HTML tag.
+ * @default "div"
+ */
+ as?: C;
+ /**
+ * The CSS class name.
+ */
+ className?: string;
+ /**
+ * The title/label next to the avatar. Usually the user or room name.
+ */
+ title: string;
+ /**
+ * A label with details to display under the avatar title.
+ * Commonly used to display the number of participants in a room.
+ */
+ details: React.ReactNode;
+ /** The avatar to display. */
+ avatar: React.ReactNode;
+} & ComponentProps;
+
+/**
+ * A component to display an avatar with a title next to it in a grey box.
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ */
+export function AvatarWithDetails({
+ as,
+ className,
+ details,
+ avatar,
+ title,
+ ...props
+}: PropsWithChildren>): JSX.Element {
+ const Component = as || "div";
+
+ return (
+
+ {avatar}
+
+ {title}
+ {details}
+
+
+ );
+}
diff --git a/packages/shared-components/src/avatar/AvatarWithDetails/__snapshots__/AvatarWithDetails.test.tsx.snap b/packages/shared-components/src/avatar/AvatarWithDetails/__snapshots__/AvatarWithDetails.test.tsx.snap
new file mode 100644
index 0000000000..7b927f2c3b
--- /dev/null
+++ b/packages/shared-components/src/avatar/AvatarWithDetails/__snapshots__/AvatarWithDetails.test.tsx.snap
@@ -0,0 +1,28 @@
+// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
+
+exports[`AvatarWithDetails renders a textual event 1`] = `
+
+
+
+
+
+ Room Name
+
+
+ Details about the avatar go here
+
+
+
+
+`;
diff --git a/packages/shared-components/src/avatar/AvatarWithDetails/index.tsx b/packages/shared-components/src/avatar/AvatarWithDetails/index.tsx
new file mode 100644
index 0000000000..a54f416d82
--- /dev/null
+++ b/packages/shared-components/src/avatar/AvatarWithDetails/index.tsx
@@ -0,0 +1,8 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+export { AvatarWithDetails } from "./AvatarWithDetails";
diff --git a/packages/shared-components/src/composer/Banner/Banner.module.css b/packages/shared-components/src/composer/Banner/Banner.module.css
new file mode 100644
index 0000000000..7bb48a9cc2
--- /dev/null
+++ b/packages/shared-components/src/composer/Banner/Banner.module.css
@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+:root {
+ --cpd-color-gradient-critical-linear: linear-gradient(
+ 180deg,
+ var(--cpd-color-alpha-red-500) 0%,
+ var(--cpd-color-alpha-red-400) 20%,
+ var(--cpd-color-alpha-red-300) 40%,
+ var(--cpd-color-alpha-red-200) 60%,
+ var(--cpd-color-alpha-red-100) 80%,
+ var(--cpd-color-transparent) 100%
+ );
+}
+
+.banner {
+ container-type: inline-size;
+ container-name: banner;
+ display: flex;
+ align-items: center;
+ justify-content: start;
+ gap: var(--cpd-space-3x);
+ padding: var(--cpd-space-4x);
+
+ border-top: 1px solid var(--cpd-color-gray-400);
+}
+
+.banner[data-type="success"] {
+ background: var(--cpd-color-gradient-subtle-linear);
+ border-color: var(--cpd-color-green-900);
+}
+
+.banner[data-type="critical"] {
+ background: var(--cpd-color-gradient-critical-linear);
+ border-color: var(--cpd-color-border-critical-primary);
+}
+
+.banner[data-type="info"] {
+ background: var(--cpd-color-gradient-info-linear);
+ border-color: var(--cpd-color-blue-900);
+}
+
+.banner[data-type="info"] :is(svg) {
+ color: var(--cpd-color-blue-900);
+}
+
+.banner[data-type="success"] :is(.content, svg) {
+ color: var(--cpd-color-green-900);
+}
+
+.banner[data-type="critical"] :is(.content, svg) {
+ color: var(--cpd-color-red-900);
+}
+
+.banner p {
+ margin: 0;
+}
+
+.icon {
+ /* lock icon dimensions */
+ min-width: 32px;
+ min-height: 32px;
+ max-width: 32px;
+ max-height: 32px;
+
+ margin: 4px;
+
+ /* centre svg icons, as they are not full width */
+ flex: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.icon img {
+ border-radius: 50%;
+}
+
+.actions {
+ margin-left: auto;
+
+ flex: 0;
+ display: flex;
+ flex-direction: row;
+ gap: var(--cpd-space-1x);
+ align-self: center;
+
+ white-space: nowrap;
+}
diff --git a/packages/shared-components/src/composer/Banner/Banner.stories.tsx b/packages/shared-components/src/composer/Banner/Banner.stories.tsx
new file mode 100644
index 0000000000..e1e3e110fb
--- /dev/null
+++ b/packages/shared-components/src/composer/Banner/Banner.stories.tsx
@@ -0,0 +1,85 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+import React from "react";
+import { fn } from "storybook/test";
+import { type Meta, type StoryObj } from "@storybook/react-vite";
+import { Button } from "@vector-im/compound-web";
+
+import { Banner } from "./Banner";
+
+const meta = {
+ title: "room/Banner",
+ component: Banner,
+ tags: ["autodocs"],
+ args: {
+ children:
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed quis massa facilisis, venenatis risus
+ consectetur, sagittis libero. Aenean et scelerisque justo. Nunc luctus, mi sed facilisis suscipit, magna
+ ante pharetra sem, eu rutrum purus quam quis arcu. Sed eleifend arcu vitae magna sodales, sit amet
+ fermentum urna dictum. Mauris vel velit pulvinar enim mollis tincidunt. Vivamus egestas rhoncus
+ sagittis. Curabitur auctor vehicula massa, et cursus lacus laoreet a. Maecenas et sollicitudin lectus,
+ in ligula.
+
+ ),
+ },
+};
diff --git a/packages/shared-components/src/composer/Banner/Banner.test.tsx b/packages/shared-components/src/composer/Banner/Banner.test.tsx
new file mode 100644
index 0000000000..0f33fb452b
--- /dev/null
+++ b/packages/shared-components/src/composer/Banner/Banner.test.tsx
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+import React from "react";
+import { render } from "jest-matrix-react";
+import { composeStories } from "@storybook/react-vite";
+
+import * as stories from "./Banner.stories.tsx";
+
+const { Default, Info, Success, WithAction, WithAvatarImage, Critical } = composeStories(stories);
+
+describe("AvatarWithDetails", () => {
+ it("renders a default banner", () => {
+ const { container } = render();
+ expect(container).toMatchSnapshot();
+ });
+ it("renders a info banner", () => {
+ const { container } = render();
+ expect(container).toMatchSnapshot();
+ });
+ it("renders a success banner", () => {
+ const { container } = render();
+ expect(container).toMatchSnapshot();
+ });
+ it("renders a critical banner", () => {
+ const { container } = render();
+ expect(container).toMatchSnapshot();
+ });
+ it("renders a banner with an action", () => {
+ const { container } = render();
+ expect(container).toMatchSnapshot();
+ });
+ it("renders a banner with an avatar iamge", () => {
+ const { container } = render();
+ expect(container).toMatchSnapshot();
+ });
+});
diff --git a/packages/shared-components/src/composer/Banner/Banner.tsx b/packages/shared-components/src/composer/Banner/Banner.tsx
new file mode 100644
index 0000000000..7781442ed9
--- /dev/null
+++ b/packages/shared-components/src/composer/Banner/Banner.tsx
@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) 2025 Element Creations Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+import classNames from "classnames";
+import React, {
+ type MouseEventHandler,
+ type ReactElement,
+ type ReactNode,
+ type PropsWithChildren,
+ useMemo,
+} from "react";
+import { Button } from "@vector-im/compound-web";
+import CheckCircleIcon from "@vector-im/compound-design-tokens/assets/web/icons/check-circle";
+import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error-solid";
+import InfoIcon from "@vector-im/compound-design-tokens/assets/web/icons/info";
+
+import styles from "./Banner.module.css";
+import { _t } from "../../utils/i18n";
+
+interface BannerProps {
+ /**
+ * The type of the status banner.
+ */
+ type?: "success" | "info" | "critical";
+
+ /**
+ * The banner avatar.
+ */
+ avatar?: React.ReactNode;
+
+ className?: string;
+
+ /**
+ * Actions presented to the user in the right-hand side of the banner alongside the dismiss button.
+ */
+ actions?: ReactNode;
+ /**
+ * Called when the user presses the "dismiss" button.
+ */
+ onClose?: MouseEventHandler;
+}
+
+/**
+ * A banner component used for displaying user-facing information above the message composer.
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ */
+export function Banner({
+ type,
+ children,
+ avatar,
+ className,
+ actions,
+ onClose,
+ ...props
+}: PropsWithChildren): ReactElement {
+ const classes = classNames(styles.banner, className);
+
+ const icon = useMemo(() => {
+ switch (type) {
+ case "critical":
+ return ;
+ case "info":
+ return ;
+ case "success":
+ return ;
+ default:
+ return ;
+ }
+ }, [type, props]);
+
+ return (
+
+
{avatar ?? icon}
+
{children}
+
+ {actions}
+ {onClose && (
+
+ )}
+
+
+ );
+}
diff --git a/packages/shared-components/src/composer/Banner/__snapshots__/Banner.test.tsx.snap b/packages/shared-components/src/composer/Banner/__snapshots__/Banner.test.tsx.snap
new file mode 100644
index 0000000000..ebb8df0a3d
--- /dev/null
+++ b/packages/shared-components/src/composer/Banner/__snapshots__/Banner.test.tsx.snap
@@ -0,0 +1,299 @@
+// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
+
+exports[`AvatarWithDetails renders a banner with an action 1`] = `
+
+
+
+
+
+
+
+ Alice's (
+
+ @alice:example.com
+
+ ) identity was reset.
+
+ Learn more
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`AvatarWithDetails renders a banner with an avatar iamge 1`] = `
+