diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index bf8ea6e6ef..ae2cf6294d 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -17,6 +17,12 @@
/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
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/workflows/build.yml b/.github/workflows/build.yml
index 7d3cfcf72f..ee6d9526ac 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:
@@ -45,7 +44,7 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
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,9 +63,7 @@ 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
diff --git a/.github/workflows/build_develop.yml b/.github/workflows/build_develop.yml
index 5a7bf7630d..924d1bdd1d 100644
--- a/.github/workflows/build_develop.yml
+++ b/.github/workflows/build_develop.yml
@@ -28,7 +28,7 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
cache: "yarn"
node-version: "lts/*"
diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml
index fe38d06326..6840c48bc7 100644
--- a/.github/workflows/docker.yaml
+++ b/.github/workflows/docker.yaml
@@ -37,14 +37,14 @@ jobs:
install: true
- name: Login to Docker Hub
- uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # 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@184bdaa0721073962dff0199f1fb9940f07167d1 # v3
+ uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
if: github.event_name != 'pull_request'
with:
registry: ghcr.io
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 456fa4761a..62a582249f 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -33,7 +33,7 @@ jobs:
repository: matrix-org/matrix-js-sdk
path: matrix-js-sdk
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
cache: "yarn"
cache-dependency-path: element-web/yarn.lock
@@ -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.yaml b/.github/workflows/end-to-end-tests.yaml
index 8e790d0fde..5064c25fa3 100644
--- a/.github/workflows/end-to-end-tests.yaml
+++ b/.github/workflows/end-to-end-tests.yaml
@@ -54,21 +54,16 @@ jobs:
with:
repository: element-hq/element-web
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
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,9 +71,7 @@ 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
@@ -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);
@@ -140,7 +133,7 @@ jobs:
name: webapp
path: webapp
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
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@0400d5f644dc74513175e3cd8d07132dd4860809 # v4
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
id: playwright-cache
with:
path: ~/.cache/ms-playwright
@@ -207,7 +200,7 @@ jobs:
persist-credentials: false
repository: element-hq/element-web
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
if: inputs.skip != true
with:
cache: "yarn"
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/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/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/shared-component-visual-tests.yaml b/.github/workflows/shared-component-visual-tests.yaml
index 180d6bcbab..5d5af153b6 100644
--- a/.github/workflows/shared-component-visual-tests.yaml
+++ b/.github/workflows/shared-component-visual-tests.yaml
@@ -26,7 +26,7 @@ jobs:
persist-credentials: false
repository: element-hq/element-web
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
cache: "yarn"
node-version: "lts/*"
@@ -39,7 +39,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@0400d5f644dc74513175e3cd8d07132dd4860809 # v4
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
id: playwright-cache
with:
path: ~/.cache/ms-playwright
diff --git a/.github/workflows/static_analysis.yaml b/.github/workflows/static_analysis.yaml
index c4aa773070..db75583821 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
@@ -25,7 +24,7 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
cache: "yarn"
node-version: "lts/*"
@@ -70,7 +69,7 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
cache: "yarn"
node-version: "lts/*"
@@ -88,7 +87,7 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
cache: "yarn"
node-version: "lts/*"
@@ -106,7 +105,7 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
cache: "yarn"
node-version: "lts/*"
@@ -124,7 +123,7 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
cache: "yarn"
node-version: "lts/*"
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index a6f3b3ae70..47465f8dae 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -44,7 +44,7 @@ jobs:
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@a0853c24544627f65ddf259abe73b1d18a591444 # v5
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@0400d5f644dc74513175e3cd8d07132dd4860809 # v4
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: /tmp/jest_cache
key: ${{ hashFiles('**/yarn.lock') }}
@@ -104,7 +104,7 @@ jobs:
- name: Skip SonarCloud in merge queue
if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true'
- uses: guibranco/github-status-action-v2@741ea90ba6c3ca76fe0d43ba11a90cda97d5e685
+ uses: guibranco/github-status-action-v2@5530c593759f489bba08272e96986ffc571c1ea1
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: success
diff --git a/.github/workflows/triage-labelled.yml b/.github/workflows/triage-labelled.yml
index 3ddc1b65ae..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({
diff --git a/.github/workflows/triage-stale.yml b/.github/workflows/triage-stale.yml
index 51120c336e..768dad22b1 100644
--- a/.github/workflows/triage-stale.yml
+++ b/.github/workflows/triage-stale.yml
@@ -12,7 +12,7 @@ jobs:
issues: write
pull-requests: write
steps:
- - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9
+ - uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10
with:
operations-per-run: 100
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 0a764699e7..eda2137cdd 100644
--- a/.github/workflows/update-jitsi.yml
+++ b/.github/workflows/update-jitsi.yml
@@ -11,7 +11,7 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
cache: "yarn"
node-version: "lts/*"
diff --git a/.github/workflows/update-topics.yaml b/.github/workflows/update-topics.yaml
index 7bf751384d..85b9632b45 100644
--- a/.github/workflows/update-topics.yaml
+++ b/.github/workflows/update-topics.yaml
@@ -22,7 +22,7 @@ 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 }}
diff --git a/.storybook/main.ts b/.storybook/main.ts
index fc4daac88a..fd1338404f 100644
--- a/.storybook/main.ts
+++ b/.storybook/main.ts
@@ -13,7 +13,7 @@ import { mergeConfig } from "vite";
const config: StorybookConfig = {
stories: ["../src/shared-components/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
staticDirs: ["../webapp"],
- addons: ["@storybook/addon-docs", "@storybook/addon-designs"],
+ addons: ["@storybook/addon-docs", "@storybook/addon-designs", "@storybook/addon-a11y"],
framework: "@storybook/react-vite",
core: {
disableTelemetry: true,
diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx
index b7f05badf3..e83526aa03 100644
--- a/.storybook/preview.tsx
+++ b/.storybook/preview.tsx
@@ -1,12 +1,11 @@
-import type { ArgTypes, Preview, Decorator } from "@storybook/react-vite";
-import { addons } from "storybook/preview-api";
+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 { FORCE_RE_RENDER } from "storybook/internal/core-events";
import { setLanguage } from "../src/shared-components/utils/i18n";
import { TooltipProvider } from "@vector-im/compound-web";
+import { StoryContext } from "storybook/internal/csf";
export const globalTypes = {
theme: {
@@ -59,29 +58,9 @@ const withThemeProvider: Decorator = (Story, context) => {
);
};
-const LanguageSwitcher: React.FC<{
- language: string;
-}> = ({ language }) => {
- useLayoutEffect(() => {
- const changeLanguage = async (language: string) => {
- await setLanguage(language);
- // Force the component to re-render to apply the new language
- addons.getChannel().emit(FORCE_RE_RENDER);
- };
- changeLanguage(language);
- }, [language]);
-
- return null;
-};
-
-export const withLanguageProvider: Decorator = (Story, context) => {
- return (
- <>
-
-
- >
- );
-};
+async function languageLoader(context: StoryContext): Promise {
+ await setLanguage(context.globals.language);
+}
const withTooltipProvider: Decorator = (Story) => {
return (
@@ -93,14 +72,22 @@ const withTooltipProvider: Decorator = (Story) => {
const preview: Preview = {
tags: ["autodocs"],
- decorators: [withThemeProvider, withLanguageProvider, withTooltipProvider],
+ decorators: [withThemeProvider, withTooltipProvider],
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/CHANGELOG.md b/CHANGELOG.md
index 01fa572689..ac74c02a7f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,95 @@
+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.
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 a4599cc7f6..da11e0a74e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,7 +1,7 @@
-# syntax=docker.io/docker/dockerfile:1.17-labs@sha256:9187104f31e3a002a8a6a3209ea1f937fb7486c093cbbde1e14b0fa0d7e4f1b5
+# syntax=docker.io/docker/dockerfile:1.18-labs@sha256:79cdc14e1c220efb546ad14a8ebc816e3277cd72d27195ced5bebdd226dd1025
# Builder
-FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:9e34ba52e1f3c31ed9bd4d0bcf784f5909db17cda61c220e29c8d7a8ebfb402e AS builder
+FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:5e638ea282ab9f0224949e8cfc7bb4621710e5d21b19fc3cf6e8884fcb5839f0 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:ea6c4b8b568824ea94cd1fabd47e1c4e7c0c04744f344a3793f7e9c8ac3a3636
+FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:13d1e0acc26b7ce8a40d155473759387e04d1433e73726d4bb49c67bdb197fe5
# 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/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..065b647c24 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/src/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/src/shared-components/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/src/shared-components/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 2bc36e206f..0c5a2b2dca 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -585,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/jest.config.ts b/jest.config.ts
index 3403ad6a0c..d4e79561da 100644
--- a/jest.config.ts
+++ b/jest.config.ts
@@ -41,7 +41,9 @@ const config: Config = {
"recorderWorkletFactory": "/__mocks__/empty.js",
"^fetch-mock$": "/node_modules/fetch-mock",
},
- transformIgnorePatterns: ["/node_modules/(?!(mime|matrix-js-sdk)).+$"],
+ transformIgnorePatterns: [
+ "/node_modules/(?!(mime|matrix-js-sdk|uuid|p-retry|is-network-error|react-merge-refs)).+$",
+ ],
collectCoverageFrom: [
"/src/**/*.{js,ts,tsx}",
// getSessionLock is piped into a different JS context via stringification, and the coverage functionality is
diff --git a/knip.ts b/knip.ts
index e129c11481..87aba7ff69 100644
--- a/knip.ts
+++ b/knip.ts
@@ -2,7 +2,6 @@ import { KnipConfig } from "knip";
export default {
entry: [
- "src/vector/index.ts",
"src/serviceworker/index.ts",
"src/workers/*.worker.ts",
"src/utils/exportUtils/exportJS.js",
@@ -12,8 +11,6 @@ export default {
"res/decoder-ring/**",
"res/jitsi_external_api.min.js",
"docs/**",
- // Used by jest
- "__mocks__/maplibre-gl.js",
],
project: ["**/*.{js,ts,jsx,tsx}"],
ignore: [
@@ -42,10 +39,18 @@ export default {
"util",
// Embedded into webapp
"@element-hq/element-call-embedded",
+
+ // 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",
],
ignoreBinaries: [
// Used in scripts & workflows
"jq",
+ "wait-on",
],
ignoreExportsUsedInFile: true,
} satisfies KnipConfig;
diff --git a/package.json b/package.json
index aa21689838..e29faadb92 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "element-web",
- "version": "1.11.109",
+ "version": "1.12.0",
"description": "Element: the future of secure communication",
"author": "New Vector Ltd.",
"repository": {
@@ -75,11 +75,11 @@
"resolutions": {
"**/pretty-format/react-is": "19.1.1",
"@playwright/test": "1.54.2",
- "@types/react": "19.1.10",
- "@types/react-dom": "19.1.7",
+ "@types/react": "19.1.14",
+ "@types/react-dom": "19.1.9",
"oidc-client-ts": "3.3.0",
"jwt-decode": "4.0.0",
- "caniuse-lite": "1.0.30001724",
+ "caniuse-lite": "1.0.30001745",
"testcontainers": "^11.0.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
"wrap-ansi": "npm:wrap-ansi@^7.0.0"
@@ -98,7 +98,7 @@
"@types/png-chunks-extract": "^1.0.2",
"@vector-im/compound-design-tokens": "^6.0.0",
"@vector-im/compound-web": "^8.1.2",
- "@vector-im/matrix-wysiwyg": "2.39.0",
+ "@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",
@@ -116,7 +116,7 @@
"emojibase-regex": "15.3.2",
"escape-html": "^1.0.3",
"file-saver": "^2.0.5",
- "filesize": "11.0.2",
+ "filesize": "11.0.13",
"github-markdown-css": "^5.5.1",
"glob-to-regexp": "^0.4.1",
"highlight.js": "^11.3.1",
@@ -142,7 +142,7 @@
"opus-recorder": "^8.0.3",
"pako": "^2.0.3",
"png-chunks-extract": "^1.0.0",
- "posthog-js": "1.260.1",
+ "posthog-js": "1.268.6",
"qrcode": "1.5.4",
"re-resizable": "6.11.2",
"react": "^19.0.0",
@@ -150,6 +150,7 @@
"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-virtuoso": "^4.14.0",
@@ -158,8 +159,8 @@
"sanitize-html": "2.17.0",
"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": {
@@ -184,13 +185,14 @@
"@babel/preset-typescript": "^7.12.7",
"@babel/runtime": "^7.12.5",
"@casualbot/jest-sonar-reporter": "2.2.7",
- "@element-hq/element-call-embedded": "0.14.1",
+ "@element-hq/element-call-embedded": "0.16.0",
"@element-hq/element-web-playwright-common": "^1.4.6",
"@peculiar/webcrypto": "^1.4.3",
"@playwright/test": "^1.50.1",
"@principalstudio/html-webpack-inject-preload": "^1.2.7",
"@rrweb/types": "^2.0.0-alpha.18",
"@sentry/webpack-plugin": "^4.0.0",
+ "@storybook/addon-a11y": "^9.0.18",
"@storybook/addon-designs": "^10.0.1",
"@storybook/addon-docs": "^9.0.12",
"@storybook/icons": "^1.4.0",
@@ -203,6 +205,7 @@
"@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",
@@ -221,15 +224,15 @@
"@types/node-fetch": "^2.6.2",
"@types/pako": "^2.0.0",
"@types/qrcode": "^1.3.5",
- "@types/react": "19.1.10",
+ "@types/react": "19.1.14",
"@types/react-beautiful-dnd": "^13.0.0",
- "@types/react-dom": "19.1.7",
+ "@types/react-dom": "19.1.9",
"@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",
diff --git a/patches/@matrix-org+react-sdk-module-api+2.5.0.patch b/patches/@matrix-org+react-sdk-module-api+2.5.0.patch
index 5b17244da0..89cbca457f 100644
--- a/patches/@matrix-org+react-sdk-module-api+2.5.0.patch
+++ b/patches/@matrix-org+react-sdk-module-api+2.5.0.patch
@@ -11,3 +11,42 @@ index 917a7fc..a2710c6 100644
didOkOrSubmit: boolean;
model: M;
}>;
+diff --git a/node_modules/@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions.js b/node_modules/@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions.js
+index 5d422ed..b823add 100644
+--- a/node_modules/@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions.js
++++ b/node_modules/@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions.js
+@@ -124,34 +124,28 @@ var DefaultCryptoSetupExtensions = /*#__PURE__*/function (_CryptoSetupExtension)
+ (0, _createClass2["default"])(DefaultCryptoSetupExtensions, [{
+ key: "examineLoginResponse",
+ value: function examineLoginResponse(response, credentials) {
+- console.log("Default empty examineLoginResponse() => void");
+ }
+ }, {
+ key: "persistCredentials",
+ value: function persistCredentials(credentials) {
+- console.log("Default empty persistCredentials() => void");
+ }
+ }, {
+ key: "getSecretStorageKey",
+ value: function getSecretStorageKey() {
+- console.log("Default empty getSecretStorageKey() => null");
+ return null;
+ }
+ }, {
+ key: "createSecretStorageKey",
+ value: function createSecretStorageKey() {
+- console.log("Default empty createSecretStorageKey() => null");
+ return null;
+ }
+ }, {
+ key: "catchAccessSecretStorageError",
+ value: function catchAccessSecretStorageError(e) {
+- console.log("Default catchAccessSecretStorageError() => void");
+ }
+ }, {
+ key: "setupEncryptionNeeded",
+ value: function setupEncryptionNeeded(args) {
+- console.log("Default setupEncryptionNeeded() => false");
+ return false;
+ }
+ }, {
diff --git a/playwright/e2e/accessibility/keyboard-navigation.spec.ts b/playwright/e2e/accessibility/keyboard-navigation.spec.ts
index e22664c898..6635a01d57 100644
--- a/playwright/e2e/accessibility/keyboard-navigation.spec.ts
+++ b/playwright/e2e/accessibility/keyboard-navigation.spec.ts
@@ -29,7 +29,7 @@ test.describe("Landmark navigation tests", () => {
// Pressing Control+F6 again will focus room search
await page.keyboard.press("ControlOrMeta+F6");
- await expect(page.locator(".mx_RoomSearch")).toBeFocused();
+ await expect(page.locator(".mx_RoomListSearch_search")).toBeFocused();
// Pressing Control+F6 again will focus the message composer
await page.keyboard.press("ControlOrMeta+F6");
@@ -44,7 +44,7 @@ test.describe("Landmark navigation tests", () => {
await expect(page.locator(".mx_HomePage")).toBeFocused();
await page.keyboard.press("ControlOrMeta+Shift+F6");
- await expect(page.locator(".mx_RoomSearch")).toBeFocused();
+ await expect(page.locator(".mx_RoomListSearch_search")).toBeFocused();
await page.keyboard.press("ControlOrMeta+Shift+F6");
await expect(page.locator(".mx_SpaceButton_active")).toBeFocused();
@@ -75,11 +75,11 @@ test.describe("Landmark navigation tests", () => {
// Pressing Control+F6 again will focus room search
await page.keyboard.press("ControlOrMeta+F6");
- await expect(page.locator(".mx_RoomSearch")).toBeFocused();
+ await expect(page.locator(".mx_RoomListSearch_search")).toBeFocused();
// Pressing Control+F6 again will focus the room tile in the room list
await page.keyboard.press("ControlOrMeta+F6");
- await expect(page.locator(".mx_RoomTile_selected")).toBeFocused();
+ await expect(page.locator(".mx_RoomListItemView_selected")).toBeFocused();
// Pressing Control+F6 again will focus the message composer
await page.keyboard.press("ControlOrMeta+F6");
@@ -94,10 +94,10 @@ test.describe("Landmark navigation tests", () => {
await expect(page.locator(".mx_BasicMessageComposer_input")).toBeFocused();
await page.keyboard.press("ControlOrMeta+Shift+F6");
- await expect(page.locator(".mx_RoomTile_selected")).toBeFocused();
+ await expect(page.locator(".mx_RoomListItemView_selected")).toBeFocused();
await page.keyboard.press("ControlOrMeta+Shift+F6");
- await expect(page.locator(".mx_RoomSearch")).toBeFocused();
+ await expect(page.locator(".mx_RoomListSearch_search")).toBeFocused();
await page.keyboard.press("ControlOrMeta+Shift+F6");
await expect(page.locator(".mx_SpaceButton_active")).toBeFocused();
@@ -131,11 +131,11 @@ test.describe("Landmark navigation tests", () => {
// Pressing Control+F6 again will focus room search
await page.keyboard.press("ControlOrMeta+F6");
- await expect(page.locator(".mx_RoomSearch")).toBeFocused();
+ await expect(page.locator(".mx_RoomListSearch_search")).toBeFocused();
// Pressing Control+F6 again will focus the room tile in the room list
await page.keyboard.press("ControlOrMeta+F6");
- await expect(page.locator(".mx_RoomTile")).toBeFocused();
+ await expect(page.locator(".mx_RoomListItemView")).toBeFocused();
// Pressing Control+F6 again will focus the home section
await page.keyboard.press("ControlOrMeta+F6");
@@ -150,10 +150,10 @@ test.describe("Landmark navigation tests", () => {
await expect(page.locator(".mx_HomePage")).toBeFocused();
await page.keyboard.press("ControlOrMeta+Shift+F6");
- await expect(page.locator(".mx_RoomTile")).toBeFocused();
+ await expect(page.locator(".mx_RoomListItemView")).toBeFocused();
await page.keyboard.press("ControlOrMeta+Shift+F6");
- await expect(page.locator(".mx_RoomSearch")).toBeFocused();
+ await expect(page.locator(".mx_RoomListSearch_search")).toBeFocused();
await page.keyboard.press("ControlOrMeta+Shift+F6");
await expect(page.locator(".mx_SpaceButton_active")).toBeFocused();
diff --git a/playwright/e2e/composer/CIDER.spec.ts b/playwright/e2e/composer/CIDER.spec.ts
index 8214c8058b..fcfbd1aad3 100644
--- a/playwright/e2e/composer/CIDER.spec.ts
+++ b/playwright/e2e/composer/CIDER.spec.ts
@@ -14,6 +14,9 @@ const CtrlOrMeta = process.platform === "darwin" ? "Meta" : "Control";
test.describe("Composer", () => {
test.use({
displayName: "Janet",
+ botCreateOpts: {
+ displayName: "Bob",
+ },
});
test.use({
@@ -94,5 +97,25 @@ test.describe("Composer", () => {
).toBeVisible();
});
});
+
+ test("can send mention", { tag: "@screenshot" }, async ({ page, bot, app }) => {
+ // Set up a private room so we have another user to mention
+ await app.client.createRoom({
+ is_direct: true,
+ invite: [bot.credentials.userId],
+ });
+ await app.viewRoomByName("Bob");
+
+ const composer = page.getByRole("textbox", { name: "Send an unencrypted message…" });
+ await composer.pressSequentially("@bob");
+
+ // Note that we include the user ID here as the room tile is also an 'option' role
+ // with text 'Bob'
+ await page.getByRole("option", { name: `Bob ${bot.credentials.userId}` }).click();
+ await expect(composer.getByText("Bob")).toBeVisible();
+ await expect(composer).toMatchScreenshot("mention.png");
+ await composer.press("Enter");
+ await expect(page.locator(".mx_EventTile_body", { hasText: "Bob" })).toBeVisible();
+ });
});
});
diff --git a/playwright/e2e/create-room/create-room.spec.ts b/playwright/e2e/create-room/create-room.spec.ts
deleted file mode 100644
index 087a89e68d..0000000000
--- a/playwright/e2e/create-room/create-room.spec.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
-Copyright 2024 New Vector Ltd.
-Copyright 2022, 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 { test, expect } from "../../element-web-test";
-
-test.describe("Create Room", () => {
- test.use({ displayName: "Jim" });
-
- test("should allow us to create a public room with name, topic & address set", async ({ page, user, app }) => {
- const name = "Test room 1";
- const topic = "This room is dedicated to this test and this test only!";
-
- const dialog = await app.openCreateRoomDialog();
- // Fill name & topic
- await dialog.getByRole("textbox", { name: "Name" }).fill(name);
- await dialog.getByRole("textbox", { name: "Topic" }).fill(topic);
- // Change room to public
- await dialog.getByRole("button", { name: "Room visibility" }).click();
- await dialog.getByRole("option", { name: "Public room" }).click();
- // Fill room address
- await dialog.getByRole("textbox", { name: "Room address" }).fill("test-room-1");
- // Submit
- await dialog.getByRole("button", { name: "Create room" }).click();
-
- await expect(page).toHaveURL(new RegExp(`/#/room/#test-room-1:${user.homeServer}`));
- const header = page.locator(".mx_RoomHeader");
- await expect(header).toContainText(name);
- });
-});
diff --git a/playwright/e2e/crypto/backups-mas.spec.ts b/playwright/e2e/crypto/backups-mas.spec.ts
index fda82c220d..4cada7a49b 100644
--- a/playwright/e2e/crypto/backups-mas.spec.ts
+++ b/playwright/e2e/crypto/backups-mas.spec.ts
@@ -49,7 +49,7 @@ test.describe("Encryption state after registration", () => {
"Pa$sW0rD!",
);
- await page.getByRole("button", { name: "Add room" }).click();
+ await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
await page.getByRole("menuitem", { name: "New room" }).click();
await page.getByRole("textbox", { name: "Name" }).fill("test room");
await page.getByRole("button", { name: "Create room" }).click();
@@ -78,7 +78,7 @@ test.describe("Key backup reset from elsewhere", () => {
await page.getByRole("button", { name: "Continue" }).click();
await registerAccountMas(page, mailpitClient, testUsername, `${testUsername}@email.com`, testPassword);
- await page.getByRole("button", { name: "Add room" }).click();
+ await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
await page.getByRole("menuitem", { name: "New room" }).click();
await page.getByRole("textbox", { name: "Name" }).fill("test room");
await page.getByRole("button", { name: "Create room" }).click();
diff --git a/playwright/e2e/crypto/crypto.spec.ts b/playwright/e2e/crypto/crypto.spec.ts
index 668fd20021..9822f0522e 100644
--- a/playwright/e2e/crypto/crypto.spec.ts
+++ b/playwright/e2e/crypto/crypto.spec.ts
@@ -21,12 +21,11 @@ const checkDMRoom = async (page: Page) => {
};
const startDMWithBob = async (page: Page, bob: Bot) => {
- await page.locator(".mx_LegacyRoomList").getByRole("button", { name: "Start chat" }).click();
+ await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
+ await page.getByRole("menuitem", { name: "Start chat" }).click();
await page.getByTestId("invite-dialog-input").fill(bob.credentials.userId);
- await page.locator(".mx_InviteDialog_tile_nameStack_name").getByText("Bob").click();
- await expect(
- page.locator(".mx_InviteDialog_userTile_pill .mx_InviteDialog_userTile_name").getByText("Bob"),
- ).toBeVisible();
+ await page.getByRole("option", { name: bob.credentials.displayName }).click();
+ await expect(page.getByTestId("invite-dialog-input-wrapper").getByText("Bob")).toBeVisible();
await page.getByRole("button", { name: "Go" }).click();
};
@@ -154,8 +153,8 @@ test.describe("Cryptography", function () {
await app.client.bootstrapCrossSigning(aliceCredentials);
await startDMWithBob(page, bob);
// send first message
- await page.getByRole("textbox", { name: "Send an unencrypted message…" }).fill("Hey!");
- await page.getByRole("textbox", { name: "Send an unencrypted message…" }).press("Enter");
+ await page.getByRole("textbox", { name: "Send a message…" }).fill("Hey!");
+ await page.getByRole("textbox", { name: "Send a message…" }).press("Enter");
await checkDMRoom(page);
const bobRoomId = await bobJoin(page, bob);
// We no longer show the grey badge in the composer, check that it is not there.
diff --git a/playwright/e2e/crypto/decryption-failure-messages.spec.ts b/playwright/e2e/crypto/decryption-failure-messages.spec.ts
index 529251b223..306e073c00 100644
--- a/playwright/e2e/crypto/decryption-failure-messages.spec.ts
+++ b/playwright/e2e/crypto/decryption-failure-messages.spec.ts
@@ -143,10 +143,7 @@ test.describe("Cryptography", function () {
);
// Alice accepts the invite
- await expect(
- page.getByRole("group", { name: "Invites" }).locator(".mx_RoomSublist_tiles").getByRole("treeitem"),
- ).toHaveCount(1);
- await page.getByRole("treeitem", { name: "Test room" }).click();
+ await page.getByRole("option", { name: "Test room" }).click();
await page.locator(".mx_RoomView").getByRole("button", { name: "Accept" }).click();
// Bob sends an encrypted event and an undecryptable event
@@ -280,10 +277,7 @@ test.describe("Cryptography", function () {
);
// Alice accepts the invite
- await expect(
- page.getByRole("group", { name: "Invites" }).locator(".mx_RoomSublist_tiles").getByRole("treeitem"),
- ).toHaveCount(1);
- await page.getByRole("treeitem", { name: "Test room" }).click();
+ await page.getByRole("option", { name: "Test room" }).click();
await page.locator(".mx_RoomView").getByRole("button", { name: "Accept" }).click();
// wait until we're joined and see the timeline
diff --git a/playwright/e2e/crypto/dehydration.spec.ts b/playwright/e2e/crypto/dehydration.spec.ts
index 379fc36cf9..4483593f1c 100644
--- a/playwright/e2e/crypto/dehydration.spec.ts
+++ b/playwright/e2e/crypto/dehydration.spec.ts
@@ -38,7 +38,7 @@ test.describe("Dehydration", () => {
// Reset the identity key
const settings = await app.settings.openUserSettings("Encryption");
await settings.getByRole("button", { name: "Verify this device" }).click();
- await page.getByRole("button", { name: "Proceed with reset" }).click();
+ await page.getByRole("button", { name: "Can't confirm?" }).click();
await page.getByRole("button", { name: "Continue" }).click();
// Set up recovery
@@ -106,7 +106,7 @@ test.describe("Dehydration", () => {
await logIntoElement(page, credentials);
// Oh no, we forgot our recovery key - reset our identity
- await page.locator(".mx_AuthPage").getByRole("button", { name: "Reset all" }).click();
+ await page.locator(".mx_AuthPage").getByRole("button", { name: "Can't confirm" }).click();
await expect(
page.getByRole("heading", { name: "Are you sure you want to reset your identity?" }),
).toBeVisible();
diff --git a/playwright/e2e/crypto/device-verification.spec.ts b/playwright/e2e/crypto/device-verification.spec.ts
index ab36c37a76..2300b382b8 100644
--- a/playwright/e2e/crypto/device-verification.spec.ts
+++ b/playwright/e2e/crypto/device-verification.spec.ts
@@ -36,13 +36,13 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
expectedBackupVersion = res.expectedBackupVersion;
});
- // Click the "Verify with another device" button, and have the bot client auto-accept it.
+ // Click the "Use another device" button, and have the bot client auto-accept it.
async function initiateAliceVerificationRequest(page: Page): Promise> {
// alice bot waits for verification request
const promiseVerificationRequest = waitForVerificationRequest(aliceBotClient);
- // Click on "Verify with another device"
- await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with another device" }).click();
+ // Click on "Use another device"
+ await page.locator(".mx_AuthPage").getByRole("button", { name: "Use another device" }).click();
// alice bot responds yes to verification request from alice
return promiseVerificationRequest;
@@ -146,8 +146,8 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
);
// Confirm that the bot user scanned successfully
- await expect(infoDialog.getByText("Almost there! Is your other device showing the same shield?")).toBeVisible();
- await infoDialog.getByRole("button", { name: "Yes" }).click();
+ await expect(infoDialog.getByText("Confirm that you see a green shield on your other device")).toBeVisible();
+ await infoDialog.getByRole("button", { name: "Yes, I see a green shield" }).click();
await infoDialog.getByRole("button", { name: "Got it" }).click();
// wait for the bot to see we have finished
@@ -201,9 +201,33 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
await enterRecoveryKeyAndCheckVerified(page, app, recoveryKey);
});
+ test("After cancelling verify with another device, I can try again #29882", async ({ page, app, credentials }) => {
+ // Regression test for https://github.com/element-hq/element-web/issues/29882
+
+ // Log in without verifying
+ await logIntoElement(page, credentials);
+ const authPage = page.locator(".mx_AuthPage");
+ await authPage.getByRole("button", { name: "Skip verification for now" }).click();
+ await authPage.getByRole("button", { name: "I'll verify later" }).click();
+ await page.waitForSelector(".mx_MatrixChat");
+
+ // Start to verify with "Use another device" but cancel
+ const settings = await app.settings.openUserSettings("Encryption");
+ await settings.getByRole("button", { name: "Verify this device" }).click();
+ await page.getByRole("button", { name: "Use another device" }).click();
+ await page.locator("#mx_Dialog_Container").getByRole("button", { name: "Close dialog" }).click();
+
+ // Start again
+ await settings.getByRole("button", { name: "Verify this device" }).click();
+
+ // We should be offered to use another device again.
+ // (In the bug, we were immediately told that verification has been cancelled.)
+ await expect(page.getByRole("button", { name: "Use another device" })).toBeVisible();
+ });
+
/** Helper for the three tests above which verify by recovery key */
async function enterRecoveryKeyAndCheckVerified(page: Page, app: ElementAppPage, recoveryKey: string) {
- await page.getByRole("button", { name: "Verify with Recovery Key or Phrase" }).click();
+ await page.getByRole("button", { name: "Use recovery key" }).click();
// Enter the recovery key
const dialog = page.locator(".mx_Dialog");
@@ -246,7 +270,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
// it should contain the device ID of the requesting device
await expect(toast.getByText(`${aliceBotClient.credentials.deviceId} from `)).toBeVisible();
// Accept
- await toast.getByRole("button", { name: "Verify Session" }).click();
+ await toast.getByRole("button", { name: "Start verification" }).click();
/* Click 'Start' to start SAS verification */
await page.getByRole("button", { name: "Start" }).click();
@@ -261,10 +285,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
/* And we're all done! */
const infoDialog = page.locator(".mx_InfoDialog");
await infoDialog.getByRole("button", { name: "They match" }).click();
- // We don't assert the full string as the device name is unset on Synapse but set to the user ID on Dendrite
- await expect(infoDialog.getByText(`You've successfully verified`)).toContainText(
- `(${aliceBotClient.credentials.deviceId})`,
- );
+ await expect(infoDialog.getByText("Device verified")).toBeVisible();
await infoDialog.getByRole("button", { name: "Got it" }).click();
});
});
diff --git a/playwright/e2e/crypto/event-shields.spec.ts b/playwright/e2e/crypto/event-shields.spec.ts
index 326ee2521f..722a30a0d2 100644
--- a/playwright/e2e/crypto/event-shields.spec.ts
+++ b/playwright/e2e/crypto/event-shields.spec.ts
@@ -14,7 +14,7 @@ import {
createSecondBotDevice,
createSharedRoomWithUser,
enableKeyBackup,
- logIntoElement,
+ logIntoElementAndVerify,
logOutOfElement,
verify,
waitForDevices,
@@ -195,7 +195,7 @@ test.describe("Cryptography", function () {
window.localStorage.clear();
});
await page.reload();
- await logIntoElement(page, aliceCredentials, securityKey);
+ await logIntoElementAndVerify(page, aliceCredentials, securityKey);
/* go back to the test room and find Bob's message again */
await app.viewRoomById(testRoomId);
diff --git a/playwright/e2e/crypto/toasts.spec.ts b/playwright/e2e/crypto/toasts.spec.ts
index 7b838edaac..7c455e5658 100644
--- a/playwright/e2e/crypto/toasts.spec.ts
+++ b/playwright/e2e/crypto/toasts.spec.ts
@@ -8,7 +8,7 @@
import { type GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
import { test, expect } from "../../element-web-test";
-import { createBot, deleteCachedSecrets, disableKeyBackup, logIntoElement } from "./utils";
+import { createBot, deleteCachedSecrets, disableKeyBackup, logIntoElementAndVerify } from "./utils";
import { type Bot } from "../../pages/bot";
test.describe("Key storage out of sync toast", () => {
@@ -18,12 +18,12 @@ test.describe("Key storage out of sync toast", () => {
const res = await createBot(page, homeserver, credentials);
recoveryKey = res.recoveryKey;
- await logIntoElement(page, credentials, recoveryKey.encodedPrivateKey);
+ await logIntoElementAndVerify(page, credentials, recoveryKey.encodedPrivateKey);
await deleteCachedSecrets(page);
// We won't be prompted for crypto setup unless we have an e2e room, so make one
- await page.getByRole("button", { name: "Add room" }).click();
+ await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
await page.getByRole("menuitem", { name: "New room" }).click();
await page.getByRole("textbox", { name: "Name" }).fill("Test room");
await page.getByRole("button", { name: "Create room" }).click();
@@ -65,10 +65,10 @@ test.describe("'Turn on key storage' toast", () => {
const recoveryKey = res.recoveryKey;
botClient = res.botClient;
- await logIntoElement(page, credentials, recoveryKey.encodedPrivateKey);
+ await logIntoElementAndVerify(page, credentials, recoveryKey.encodedPrivateKey);
// We won't be prompted for crypto setup unless we have an e2e room, so make one
- await page.getByRole("button", { name: "Add room" }).click();
+ await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
await page.getByRole("menuitem", { name: "New room" }).click();
await page.getByRole("textbox", { name: "Name" }).fill("Test room");
await page.getByRole("button", { name: "Create room" }).click();
@@ -126,7 +126,7 @@ test.describe("'Turn on key storage' toast", () => {
await toast.getByRole("button", { name: "Continue" }).click();
// Then we see the Encryption settings dialog with an option to turn on key storage
- await expect(page.getByRole("checkbox", { name: "Allow key storage" })).toBeVisible();
+ await expect(page.getByRole("switch", { name: "Allow key storage" })).toBeVisible();
// And when we close that
await page.getByRole("button", { name: "Close dialog" }).click();
@@ -153,7 +153,7 @@ test.describe("'Turn on key storage' toast", () => {
await page.getByRole("button", { name: "Go to Settings" }).click();
// Then we see Encryption settings again
- await expect(page.getByRole("checkbox", { name: "Allow key storage" })).toBeVisible();
+ await expect(page.getByRole("switch", { name: "Allow key storage" })).toBeVisible();
// And when we close that, see the toast, click Dismiss, and Yes, Dismiss
await page.getByRole("button", { name: "Close dialog" }).click();
diff --git a/playwright/e2e/crypto/utils.ts b/playwright/e2e/crypto/utils.ts
index 0521df236e..cf395c31dd 100644
--- a/playwright/e2e/crypto/utils.ts
+++ b/playwright/e2e/crypto/utils.ts
@@ -206,32 +206,42 @@ export async function checkDeviceIsConnectedKeyBackup(
/**
* Fill in the login form in element with the given creds.
- *
- * If a `securityKey` is given, verifies the new device using the key.
*/
-export async function logIntoElement(page: Page, credentials: Credentials, securityKey?: string) {
+export async function logIntoElement(page: Page, credentials: Credentials) {
await page.goto("/#/login");
await page.getByRole("textbox", { name: "Username" }).fill(credentials.userId);
await page.getByPlaceholder("Password").fill(credentials.password);
await page.getByRole("button", { name: "Sign in" }).click();
+}
- // if a securityKey was given, verify the new device
- if (securityKey !== undefined) {
- await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Recovery Key" }).click();
+/**
+ * Fill in the login form in Element with the given creds, and then complete the `CompleteSecurity` step, using the
+ * given recovery key. (Normally this will verify the new device using the secrets from 4S.)
+ *
+ * Afterwards, waits for the application to redirect to the home page.
+ */
+export async function logIntoElementAndVerify(page: Page, credentials: Credentials, recoveryKey: string) {
+ await logIntoElement(page, credentials);
- const useSecurityKey = page.locator(".mx_Dialog").getByRole("button", { name: "use your Recovery Key" });
- // If the user has set a recovery *passphrase*, they'll be prompted for that first and have to click
- // through to enter the recovery key which is what we have here. If they haven't, they'll be prompted
- // for a recovery key straight away. We click the button if it's there so this works in both cases.
- if (await useSecurityKey.isVisible()) {
- await useSecurityKey.click();
- }
- // Fill in the recovery key
- await page.locator(".mx_Dialog").getByTitle("Recovery key").fill(securityKey);
- await page.getByRole("button", { name: "Continue", disabled: false }).click();
- await page.getByRole("button", { name: "Done" }).click();
+ await page.locator(".mx_AuthPage").getByRole("button", { name: "Use recovery key" }).click();
+
+ const useSecurityKey = page.locator(".mx_Dialog").getByRole("button", { name: "Use recovery key" });
+ // If the user has set a recovery *passphrase*, they'll be prompted for that first and have to click
+ // through to enter the recovery key which is what we have here. If they haven't, they'll be prompted
+ // for a recovery key straight away. We click the button if it's there so this works in both cases.
+ if (await useSecurityKey.isVisible()) {
+ await useSecurityKey.click();
}
+
+ // Fill in the recovery key
+ await page.locator(".mx_Dialog").getByTitle("Recovery key").fill(recoveryKey);
+ await page.getByRole("button", { name: "Continue", disabled: false }).click();
+ await page.getByRole("button", { name: "Done" }).click();
+
+ // The application should now redirect to `/#/home`. Wait for that to happen, otherwise if a test immediately does
+ // a `viewRoomById` or similar, it could race.
+ await page.waitForURL("/#/home");
}
/**
@@ -262,7 +272,7 @@ export async function logOutOfElement(page: Page, discardKeys: boolean = false)
export async function verifySession(app: ElementAppPage, securityKey: string) {
const settings = await app.settings.openUserSettings("Encryption");
await settings.getByRole("button", { name: "Verify this device" }).click();
- await app.page.getByRole("button", { name: "Verify with Recovery Key" }).click();
+ await app.page.getByRole("button", { name: "Use recovery key" }).click();
await app.page.locator(".mx_Dialog").getByTitle("Recovery key").fill(securityKey);
await app.page.getByRole("button", { name: "Continue", disabled: false }).click();
await app.page.getByRole("button", { name: "Done" }).click();
@@ -300,9 +310,9 @@ export async function doTwoWaySasVerification(page: Page, verifier: JSHandle {
const encryptionTab = await app.settings.openUserSettings("Encryption");
- const keyStorageToggle = encryptionTab.getByRole("checkbox", { name: "Allow key storage" });
+ const keyStorageToggle = encryptionTab.getByRole("switch", { name: "Allow key storage" });
if (!(await keyStorageToggle.isChecked())) {
- await encryptionTab.getByRole("checkbox", { name: "Allow key storage" }).click();
+ await encryptionTab.getByRole("switch", { name: "Allow key storage" }).click();
}
await encryptionTab.getByRole("button", { name: "Set up recovery" }).click();
@@ -323,11 +333,11 @@ export async function enableKeyBackup(app: ElementAppPage): Promise {
export async function disableKeyBackup(app: ElementAppPage): Promise {
const encryptionTab = await app.settings.openUserSettings("Encryption");
- const keyStorageToggle = encryptionTab.getByRole("checkbox", { name: "Allow key storage" });
+ const keyStorageToggle = encryptionTab.getByRole("switch", { name: "Allow key storage" });
if (await keyStorageToggle.isChecked()) {
- await encryptionTab.getByRole("checkbox", { name: "Allow key storage" }).click();
+ await encryptionTab.getByRole("switch", { name: "Allow key storage" }).click();
await encryptionTab.getByRole("button", { name: "Delete key storage" }).click();
- await encryptionTab.getByRole("checkbox", { name: "Allow key storage" }).isVisible();
+ await encryptionTab.getByRole("switch", { name: "Allow key storage" }).isVisible();
// Wait for the update to account data to stick
await new Promise((resolve) => setTimeout(resolve, 2000));
@@ -428,8 +438,8 @@ export async function sendMessageInCurrentRoom(page: Page, message: string): Pro
* @param isEncrypted - Whether the room should be encrypted
*/
export async function createRoom(page: Page, roomName: string, isEncrypted: boolean): Promise {
- await page.getByRole("button", { name: "Add room" }).click();
- await page.locator(".mx_IconizedContextMenu").getByRole("menuitem", { name: "New room" }).click();
+ await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
+ await page.getByRole("menuitem", { name: "New room" }).click();
const dialog = page.locator(".mx_Dialog");
diff --git a/playwright/e2e/devtools/devtools.spec.ts b/playwright/e2e/devtools/devtools.spec.ts
new file mode 100644
index 0000000000..051736e38b
--- /dev/null
+++ b/playwright/e2e/devtools/devtools.spec.ts
@@ -0,0 +1,33 @@
+/*
+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 { test, expect } from "../../element-web-test";
+
+test.describe("Devtools", () => {
+ test.use({
+ displayName: "Alice",
+ });
+
+ test("should render the devtools", { tag: "@screenshot" }, async ({ page, homeserver, user, app, axe }) => {
+ await app.client.createRoom({ name: "Test Room" });
+ await app.viewRoomByName("Test Room");
+
+ const composer = app.getComposer().locator("[contenteditable]");
+ await composer.fill("/devtools");
+ await composer.press("Enter");
+ const dialog = page.locator(".mx_Dialog");
+ await dialog.getByLabel("Developer mode").check();
+
+ axe.disableRules("color-contrast"); // XXX: Inheriting colour contrast issues from room view.
+ await expect(axe).toHaveNoViolations();
+ await expect(dialog).toMatchScreenshot("devtools-dialog.png", {
+ css: `.mx_CopyableText {
+ display: none;
+ }`,
+ });
+ });
+});
diff --git a/playwright/e2e/devtools/upgraderoom.spec.ts b/playwright/e2e/devtools/upgraderoom.spec.ts
new file mode 100644
index 0000000000..69de03b6ee
--- /dev/null
+++ b/playwright/e2e/devtools/upgraderoom.spec.ts
@@ -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 { SettingLevel } from "../../../src/settings/SettingLevel";
+import { test, expect } from "../../element-web-test";
+
+test.describe("Room upgrade dialog", () => {
+ test.use({
+ displayName: "Alice",
+ });
+
+ test(
+ "should render the room upgrade dialog",
+ { tag: "@screenshot" },
+ async ({ page, homeserver, user, app, axe }) => {
+ // Enable developer mode
+ await app.settings.setValue("developerMode", null, SettingLevel.ACCOUNT, true);
+
+ await app.client.createRoom({ name: "Test Room" });
+ await app.viewRoomByName("Test Room");
+
+ const composer = app.getComposer().locator("[contenteditable]");
+ // Pick a room version that is likely to be supported by all our target homeservers.
+ await composer.fill("/upgraderoom 5");
+ await composer.press("Enter");
+ const dialog = page.locator(".mx_Dialog");
+ await dialog.getByLabel("Automatically invite members from this room to the new one").check();
+
+ axe.disableRules("color-contrast"); // XXX: Inheriting colour contrast issues from room view.
+ await expect(axe).toHaveNoViolations();
+ await expect(dialog).toMatchScreenshot("upgrade-room.png");
+ },
+ );
+});
diff --git a/playwright/e2e/invite/decline-and-block-invite-dialog.spec.ts b/playwright/e2e/invite/decline-and-block-invite-dialog.spec.ts
new file mode 100644
index 0000000000..1dbacaac24
--- /dev/null
+++ b/playwright/e2e/invite/decline-and-block-invite-dialog.spec.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 { test, expect } from "../../element-web-test";
+
+test.describe("Decline and block invite dialog", function () {
+ test.use({
+ displayName: "Hanako",
+ });
+
+ test(
+ "should show decline and block dialog for a room",
+ { tag: "@screenshot" },
+ async ({ page, app, user, bot, axe }) => {
+ await bot.createRoom({ name: "Test Room", invite: [user.userId] });
+ await app.viewRoomByName("Test Room");
+ await page.getByRole("button", { name: "Decline and block" }).click();
+
+ axe.disableRules("color-contrast"); // XXX: Inheriting colour contrast issues from room view.
+ await expect(axe).toHaveNoViolations();
+ await expect(page.locator(".mx_Dialog")).toMatchScreenshot("decline-and-block-invite-empty.png");
+ },
+ );
+});
diff --git a/playwright/e2e/invite/invite-dialog.spec.ts b/playwright/e2e/invite/invite-dialog.spec.ts
index 8d64e6e047..cdf877f95e 100644
--- a/playwright/e2e/invite/invite-dialog.spec.ts
+++ b/playwright/e2e/invite/invite-dialog.spec.ts
@@ -50,15 +50,11 @@ test.describe("Invite dialog", function () {
await expect(other.locator(".mx_InviteDialog_identityServer")).toBeVisible();
// Assert that the bot id is rendered properly
- await expect(
- other.locator(".mx_InviteDialog_tile_nameStack_userId").getByText(bot.credentials.userId),
- ).toBeVisible();
+ await expect(other.getByRole("option", { name: botName }).getByText(bot.credentials.userId)).toBeVisible();
- await other.locator(".mx_InviteDialog_tile_nameStack_name").getByText(botName).click();
+ await other.getByRole("option", { name: botName }).click();
- await expect(
- other.locator(".mx_InviteDialog_userTile_pill .mx_InviteDialog_userTile_name").getByText(botName),
- ).toBeVisible();
+ await expect(other.getByTestId("invite-dialog-input-wrapper").getByText(botName)).toBeVisible();
// Take a snapshot of the invite dialog with a user pill
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("invite-dialog-room-with-user-pill.png");
@@ -77,7 +73,8 @@ test.describe("Invite dialog", function () {
"should support inviting a user to Direct Messages",
{ tag: "@screenshot" },
async ({ page, app, user, bot }) => {
- await page.locator(".mx_LegacyRoomList").getByRole("button", { name: "Start chat" }).click();
+ await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
+ await page.getByRole("menuitem", { name: "Start chat" }).click();
const other = page.locator(".mx_InviteDialog_other");
// Assert that the header is rendered
@@ -93,14 +90,10 @@ test.describe("Invite dialog", function () {
await other.getByTestId("invite-dialog-input").fill(bot.credentials.userId);
- await expect(
- other.locator(".mx_InviteDialog_tile_nameStack").getByText(bot.credentials.userId),
- ).toBeVisible();
- await other.locator(".mx_InviteDialog_tile_nameStack").getByText(botName).click();
+ await expect(other.getByRole("option", { name: botName }).getByText(bot.credentials.userId)).toBeVisible();
+ await other.getByRole("option", { name: botName }).click();
- await expect(
- other.locator(".mx_InviteDialog_userTile_pill .mx_InviteDialog_userTile_name").getByText(botName),
- ).toBeVisible();
+ await expect(other.getByTestId("invite-dialog-input-wrapper").getByText(botName)).toBeVisible();
// Take a snapshot of the invite dialog with a user pill
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("invite-dialog-dm-with-user-pill.png");
diff --git a/playwright/e2e/knock/knock-into-room.spec.ts b/playwright/e2e/knock/knock-into-room.spec.ts
index be6619697d..26440a6e1e 100644
--- a/playwright/e2e/knock/knock-into-room.spec.ts
+++ b/playwright/e2e/knock/knock-into-room.spec.ts
@@ -59,7 +59,7 @@ test.describe("Knock Into Room", () => {
// Knocked room should appear in Rooms
await expect(
- page.getByRole("group", { name: "Rooms" }).getByRole("treeitem", { name: "Cybersecurity" }),
+ page.getByTestId("room-list").getByRole("option", { name: "Open room Cybersecurity" }),
).toBeVisible();
// bot waits for knock request from Alice
@@ -77,7 +77,7 @@ test.describe("Knock Into Room", () => {
await bot.inviteUser(room.roomId, user.userId);
await expect(
- page.getByRole("group", { name: "Invites" }).getByRole("treeitem", { name: "Cybersecurity" }),
+ page.getByTestId("room-list").getByRole("option", { name: "Open room Cybersecurity" }),
).toBeVisible();
// Alice have to accept invitation in order to join the room.
@@ -85,7 +85,7 @@ test.describe("Knock Into Room", () => {
await page.locator(".mx_RoomView").getByRole("button", { name: "Accept" }).click();
await expect(
- page.getByRole("group", { name: "Rooms" }).getByRole("treeitem", { name: "Cybersecurity" }),
+ page.getByTestId("room-list").getByRole("option", { name: "Open room Cybersecurity" }),
).toBeVisible();
await expect(page.getByText("Alice joined the room")).toBeVisible();
@@ -136,7 +136,7 @@ test.describe("Knock Into Room", () => {
// Knocked room should appear in Rooms
await expect(
- page.getByRole("group", { name: "Rooms" }).getByRole("treeitem", { name: "Cybersecurity" }),
+ page.getByTestId("room-list").getByRole("option", { name: "Open room Cybersecurity" }),
).toBeVisible();
// bot waits for knock request from Alice
@@ -154,7 +154,7 @@ test.describe("Knock Into Room", () => {
await bot.inviteUser(room.roomId, user.userId);
await expect(
- page.getByRole("group", { name: "Invites" }).getByRole("treeitem", { name: "Cybersecurity" }),
+ page.getByTestId("room-list").getByRole("option", { name: "Open room Cybersecurity" }),
).toBeVisible();
// Alice have to accept invitation in order to join the room.
@@ -162,7 +162,7 @@ test.describe("Knock Into Room", () => {
await page.locator(".mx_RoomView").getByRole("button", { name: "Accept" }).click();
await expect(
- page.getByRole("group", { name: "Rooms" }).getByRole("treeitem", { name: "Cybersecurity" }),
+ page.getByTestId("room-list").getByRole("option", { name: "Open room Cybersecurity" }),
).toBeVisible();
await expect(page.getByText("Alice joined the room")).toBeVisible();
@@ -215,14 +215,14 @@ test.describe("Knock Into Room", () => {
await expect(roomPreviewBar.getByRole("heading", { name: "Request to join sent" })).toBeVisible();
// Knocked room should appear in Rooms
- page.getByRole("group", { name: "Rooms" }).getByRole("treeitem", { name: "Cybersecurity" });
+ page.getByTestId("room-list").getByRole("option", { name: "Open room Cybersecurity" });
await roomPreviewBar.getByRole("button", { name: "Cancel request" }).click();
await expect(roomPreviewBar.getByRole("heading", { name: "Ask to join Cybersecurity?" })).toBeVisible();
await expect(roomPreviewBar.getByRole("button", { name: "Request access" })).toBeVisible();
await expect(
- page.getByRole("group", { name: "Rooms" }).getByRole("treeitem", { name: "Cybersecurity" }),
+ page.getByTestId("room-list").getByRole("option", { name: "Open room Cybersecurity" }),
).not.toBeVisible();
});
@@ -244,7 +244,7 @@ test.describe("Knock Into Room", () => {
// Knocked room should appear in Rooms
await expect(
- page.getByRole("group", { name: "Rooms" }).getByRole("treeitem", { name: "Cybersecurity" }),
+ page.getByTestId("room-list").getByRole("option", { name: "Open room Cybersecurity" }),
).toBeVisible();
// bot waits for knock request from Alice
@@ -262,13 +262,10 @@ test.describe("Knock Into Room", () => {
await bot.kick(room.roomId, user.userId);
// Room should stay in Rooms and have red badge when knock is denied
- await expect(
- page.getByRole("group", { name: "Rooms" }).getByRole("treeitem", { name: "Cybersecurity", exact: true }),
- ).not.toBeVisible();
await expect(
page
- .getByRole("group", { name: "Rooms" })
- .getByRole("treeitem", { name: "Cybersecurity 1 unread mention." }),
+ .getByTestId("room-list")
+ .getByRole("option", { name: "Open room Cybersecurity with 1 unread mention." }),
).toBeVisible();
await expect(roomPreviewBar.getByRole("heading", { name: "You have been denied access" })).toBeVisible();
diff --git a/playwright/e2e/left-panel/left-panel.spec.ts b/playwright/e2e/left-panel/left-panel.spec.ts
index 3850df5cd0..cae6fd8934 100644
--- a/playwright/e2e/left-panel/left-panel.spec.ts
+++ b/playwright/e2e/left-panel/left-panel.spec.ts
@@ -17,7 +17,7 @@ test.describe("LeftPanel", () => {
// create rooms and check room names are correct
for (const name of ["Apple", "Pineapple", "Orange"]) {
await app.client.createRoom({ name });
- await expect(page.getByRole("treeitem", { name })).toBeVisible();
+ await expect(page.getByRole("option", { name: `Open room ${name}` })).toBeVisible();
}
});
});
diff --git a/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts b/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts
index 09dc010d3b..d1c0332cef 100644
--- a/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts
+++ b/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts
@@ -322,6 +322,13 @@ test.describe("Room list filters and sort", () => {
return page.getByTestId("empty-room-list");
}
+ test("should render the primary filters", { tag: "@screenshot" }, async ({ page, app, user }) => {
+ const primaryFilters = getPrimaryFilters(page);
+ await expect(primaryFilters).toMatchScreenshot("collapsed-primary-filters.png");
+ await getFilterExpandButton(page).click();
+ await expect(primaryFilters).toMatchScreenshot("expanded-primary-filters.png");
+ });
+
test(
"should render the default placeholder when there is no filter",
{ tag: "@screenshot" },
diff --git a/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts b/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts
index 2d62737b85..f1247b038b 100644
--- a/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts
+++ b/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts
@@ -41,7 +41,7 @@ test.describe("Room list", () => {
}
});
- test("should render the room list", { tag: "@screenshot" }, async ({ page, app, user }) => {
+ test("should render the room list", { tag: "@screenshot" }, async ({ page, app, user, axe }) => {
const roomListView = getRoomList(page);
await expect(roomListView.getByRole("option", { name: "Open room room29" })).toBeVisible();
await expect(roomListView).toMatchScreenshot("room-list.png");
@@ -54,6 +54,7 @@ test.describe("Room list", () => {
// scrollListToBottom seems to leave the mouse hovered over the list, move it away.
await page.getByRole("button", { name: "User menu" }).hover();
+ await expect(axe).toHaveNoViolations();
await expect(roomListView).toMatchScreenshot("room-list-scrolled.png");
});
@@ -193,6 +194,9 @@ test.describe("Room list", () => {
await roomListView.getByRole("option", { name: "Open room room20" }).click();
+ // Make sure the room with the unread is visible before we press the keyboard action to select it
+ await expect(roomListView.getByRole("option", { name: "1 notification" })).toBeVisible();
+
await page.keyboard.press("Alt+Shift+ArrowDown");
await expect(page.getByRole("heading", { name: "1 notification", level: 1 })).toBeVisible();
@@ -248,6 +252,26 @@ test.describe("Room list", () => {
// Focus should be back on the notification button
await expect(notificationButton).toBeFocused();
});
+
+ test("should navigate to the top and then bottom of the room list", async ({ page, app, user }) => {
+ const roomListView = getRoomList(page);
+
+ const topRoom = roomListView.getByRole("option", { name: "Open room room29" });
+
+ // open the room
+ await topRoom.click();
+ // put focus back on the room list item
+ await topRoom.click();
+ await expect(topRoom).toBeFocused();
+
+ await page.keyboard.press("End");
+ const bottomRoom = roomListView.getByRole("option", { name: "Open room room0" });
+ await expect(bottomRoom).toBeFocused();
+
+ await page.keyboard.press("Home");
+ const topRoomAgain = roomListView.getByRole("option", { name: "Open room room29" });
+ await expect(topRoomAgain).toBeFocused();
+ });
});
});
diff --git a/playwright/e2e/location/location.spec.ts b/playwright/e2e/location/location.spec.ts
index 52afd5e173..dda063364b 100644
--- a/playwright/e2e/location/location.spec.ts
+++ b/playwright/e2e/location/location.spec.ts
@@ -1,5 +1,5 @@
/*
-Copyright 2024 New Vector Ltd.
+Copyright 2024, 2025 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
@@ -57,4 +57,26 @@ test.describe("Location sharing", { tag: "@no-firefox" }, () => {
await expect(page.locator(".mx_Marker")).toBeVisible();
});
+
+ test(
+ "is prompted for and can consent to live location sharing",
+ { tag: "@screenshot" },
+ async ({ page, user, app, axe }) => {
+ await app.viewRoomById(await app.client.createRoom({}));
+
+ const composerOptions = await app.openMessageComposerOptions();
+ await composerOptions.getByRole("menuitem", { name: "Location", exact: true }).click();
+ const menu = page.locator(".mx_LocationShareMenu");
+
+ await menu.getByRole("button", { name: "My live location" }).click();
+ await menu.getByLabel("Enable live location sharing").check();
+
+ axe.disableRules([
+ "color-contrast", // XXX: Inheriting colour contrast issues from room view.
+ "region", // XXX: ContextMenu managed=false does not provide a role.
+ ]);
+ await expect(axe).toHaveNoViolations();
+ await expect(menu).toMatchScreenshot("location-live-share-dialog.png");
+ },
+ );
});
diff --git a/playwright/e2e/login/login-consent.spec.ts b/playwright/e2e/login/login-consent.spec.ts
index 23baf023fa..51486b1ce0 100644
--- a/playwright/e2e/login/login-consent.spec.ts
+++ b/playwright/e2e/login/login-consent.spec.ts
@@ -186,7 +186,7 @@ test.describe("Login", () => {
await page.goto("/");
await login(page, homeserver, credentials);
- await expect(page.getByRole("heading", { name: "Verify this device", level: 1 })).toBeVisible();
+ await expect(page.getByRole("heading", { name: "Confirm your identity", level: 2 })).toBeVisible();
await expect(page.getByRole("button", { name: "Skip verification for now" })).toBeVisible();
});
@@ -219,7 +219,7 @@ test.describe("Login", () => {
await page.goto("/");
await login(page, homeserver, credentials);
- await expect(page.getByRole("heading", { name: "Verify this device", level: 1 })).toBeVisible();
+ await expect(page.getByRole("heading", { name: "Confirm your identity", level: 2 })).toBeVisible();
await expect(page.getByRole("button", { name: "Skip verification for now" })).toBeVisible();
});
@@ -254,10 +254,10 @@ test.describe("Login", () => {
await page.goto("/");
await login(page, homeserver, credentials);
- const h1 = page.getByRole("heading", { name: "Verify this device", level: 1 });
- await expect(h1).toBeVisible();
+ const h2 = page.getByRole("heading", { name: "Confirm your identity", level: 2 });
+ await expect(h2).toBeVisible();
- await expect(h1.locator(".mx_CompleteSecurity_skip")).toHaveCount(0);
+ await expect(h2.locator(".mx_CompleteSecurity_skip")).toHaveCount(0);
});
test("Continues to show verification prompt after cancelling device verification", async ({
@@ -274,18 +274,18 @@ test.describe("Login", () => {
// Load the page and see that we are asked to verify
await page.goto("/#/welcome");
await login(page, homeserver, credentials);
- let h1 = page.getByRole("heading", { name: "Verify this device", level: 1 });
- await expect(h1).toBeVisible();
+ let h2 = page.getByRole("heading", { name: "Confirm your identity", level: 2 });
+ await expect(h2).toBeVisible();
- // Click "Verify with another device"
- await page.getByRole("button", { name: "Verify with another device" }).click();
+ // Click "Use another device"
+ await page.getByRole("button", { name: "Use another device" }).click();
// Cancel the new dialog
await page.getByRole("button", { name: "Close dialog" }).click();
// Check that we are still being asked to verify
- h1 = page.getByRole("heading", { name: "Verify this device", level: 1 });
- await expect(h1).toBeVisible();
+ h2 = page.getByRole("heading", { name: "Confirm your identity", level: 2 });
+ await expect(h2).toBeVisible();
});
});
@@ -303,18 +303,18 @@ test.describe("Login", () => {
await page.goto("/");
await login(page, homeserver, credentials);
- await expect(page.getByRole("heading", { name: "Verify this device", level: 1 })).toBeVisible();
+ await expect(page.getByRole("heading", { name: "Confirm your identity", level: 2 })).toBeVisible();
// Start the reset process
- await page.getByRole("button", { name: "Proceed with reset" }).click();
+ await page.getByRole("button", { name: "Can't confirm?" }).click();
// First try cancelling and restarting
await page.getByRole("button", { name: "Cancel" }).click();
- await page.getByRole("button", { name: "Proceed with reset" }).click();
+ await page.getByRole("button", { name: "Can't confirm?" }).click();
// Then click outside the dialog and restart
await page.getByRole("link", { name: "Powered by Matrix" }).click({ force: true });
- await page.getByRole("button", { name: "Proceed with reset" }).click();
+ await page.getByRole("button", { name: "Can't confirm?" }).click();
// Finally we actually continue
await page.getByRole("button", { name: "Continue" }).click();
diff --git a/playwright/e2e/oidc/oidc-native.spec.ts b/playwright/e2e/oidc/oidc-native.spec.ts
index acb23c0a81..2fedc0c451 100644
--- a/playwright/e2e/oidc/oidc-native.spec.ts
+++ b/playwright/e2e/oidc/oidc-native.spec.ts
@@ -129,8 +129,8 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
await page.getByRole("button", { name: "Continue" }).click();
await page.getByRole("button", { name: "Continue" }).click();
- // We should be in (we see an error because we have no recovery key).
- await expect(page.getByText("Unable to verify this device")).toBeVisible();
+ // We should be in
+ await expect(page.getByText("Confirm your identity")).toBeVisible();
});
test.describe("with force_verification on", () => {
@@ -162,7 +162,7 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
await page.getByRole("button", { name: "Continue" }).click();
// We should be being warned that we need to verify (but we can't)
- await expect(page.getByText("Unable to verify this device")).toBeVisible();
+ await expect(page.getByText("Confirm your identity")).toBeVisible();
// And there should be no way to close this prompt
await expect(page.getByRole("button", { name: "Skip verification for now" })).not.toBeVisible();
@@ -210,7 +210,7 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
await expect(page.getByRole("button", { name: "Skip verification for now" })).not.toBeVisible();
// When we start verifying with another device
- await page.getByRole("button", { name: "Verify with another device" }).click();
+ await page.getByRole("button", { name: "Use another device" }).click();
// And then cancel it
await page.getByRole("button", { name: "Close dialog" }).click();
@@ -227,8 +227,8 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
* Perform interactive emoji verification for a new device.
*/
async function verifyUsingOtherDevice(deviceToVerifyPage: Page, alreadyVerifiedDevicePage: Page) {
- await deviceToVerifyPage.getByRole("button", { name: "Verify with another device" }).click();
- await alreadyVerifiedDevicePage.getByRole("button", { name: "Verify session" }).click();
+ await deviceToVerifyPage.getByRole("button", { name: "Use another device" }).click();
+ await alreadyVerifiedDevicePage.getByRole("button", { name: "Start verification" }).click();
await alreadyVerifiedDevicePage.getByRole("button", { name: "Start" }).click();
await alreadyVerifiedDevicePage.getByRole("button", { name: "They match" }).click();
await deviceToVerifyPage.getByRole("button", { name: "They match" }).click();
diff --git a/playwright/e2e/permalinks/permalinks.spec.ts b/playwright/e2e/permalinks/permalinks.spec.ts
index e7657b1394..3f6fddbd0d 100644
--- a/playwright/e2e/permalinks/permalinks.spec.ts
+++ b/playwright/e2e/permalinks/permalinks.spec.ts
@@ -100,3 +100,51 @@ test.describe("permalinks", () => {
});
});
});
+
+test.describe("triple-click message selection", () => {
+ test.use({
+ displayName: "Alice",
+ });
+
+ test("should select entire message line when triple-clicking on message with pills", async ({
+ page,
+ app,
+ user,
+ bot,
+ }) => {
+ await bot.prepareClient();
+
+ const roomId = await app.client.createRoom({ name: "Test Room" });
+ await app.client.inviteUser(roomId, bot.credentials.userId);
+ await app.viewRoomByName("Test Room");
+
+ // Send a message with user and room pills
+ await app.client.sendMessage(
+ roomId,
+ `Testing triple-click message selection. ` +
+ `User: ${permalinkPrefix}${bot.credentials.userId}, ` +
+ `Room: ${permalinkPrefix}${roomId}, ` +
+ `Message: ${permalinkPrefix}${roomId}/$dummy-event, ` +
+ `and @room mention.`,
+ );
+
+ const timeline = page.locator(".mx_RoomView_timeline");
+ const messageTile = timeline.locator(".mx_EventTile").last();
+
+ // Triple-click on the message body to select its entire content
+ const messageBody = messageTile.locator(".mx_EventTile_body");
+ await messageBody.click({ clickCount: 3 });
+
+ // Get the expected text content of the message, including pills
+ const expectedText = await messageBody.innerText();
+
+ // Get the currently selected text from the page
+ const selectedText = await page.evaluate(() => {
+ const selection = window.getSelection();
+ return selection ? selection.toString().trim() : "";
+ });
+
+ // Verify that the selected text exactly matches the message content
+ expect(selectedText).toBe(expectedText);
+ });
+});
diff --git a/playwright/e2e/read-receipts/index.ts b/playwright/e2e/read-receipts/index.ts
index 067d3d16d2..f93d2cf03e 100644
--- a/playwright/e2e/read-receipts/index.ts
+++ b/playwright/e2e/read-receipts/index.ts
@@ -6,7 +6,7 @@ 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 type { JSHandle, Page } from "@playwright/test";
+import type { JSHandle, Locator, Page } from "@playwright/test";
import type { MatrixEvent, Room, IndexedDBStore, ReceiptType } from "matrix-js-sdk/src/matrix";
import { test as base, expect } from "../../element-web-test";
import { type Bot } from "../../pages/bot";
@@ -428,7 +428,7 @@ class Helpers {
}
getRoomListTile(label: string) {
- return this.page.getByRole("treeitem", { name: new RegExp("^" + label) });
+ return this.page.getByRole("option", { name: new RegExp("^Open room " + label) });
}
/**
@@ -446,8 +446,8 @@ class Helpers {
*/
async assertRead(room: RoomRef) {
const tile = this.getRoomListTile(room.name);
- await expect(tile.locator(".mx_NotificationBadge_dot")).not.toBeVisible();
- await expect(tile.locator(".mx_NotificationBadge_count")).not.toBeVisible();
+ await expect(tile.getByTestId("notification-decoration")).not.toBeVisible();
+ await expect(tile).not.toHaveAccessibleName(/with \d* unread message/);
}
/**
@@ -463,15 +463,18 @@ class Helpers {
/**
* Assert a given room is marked as unread (via the room list tile)
* @param room - the name of the room to check
- * @param count - the numeric count to assert, or if "." specified then a bold/dot (no count) state is asserted
+ * @param count - the numeric count to assert
*/
- async assertUnread(room: RoomRef, count: number | ".") {
+ async assertUnread(room: RoomRef, count: number) {
const tile = this.getRoomListTile(room.name);
- if (count === ".") {
- await expect(tile.locator(".mx_NotificationBadge_dot")).toBeVisible();
- } else {
- await expect(tile.locator(".mx_NotificationBadge_count")).toHaveText(count.toString());
- }
+ await expect(tile).toBeVisible();
+ await expect(tile).toHaveAccessibleName(/with \d* unread message/);
+ }
+
+ async unreadCountForRoomTile(tile: Locator): Promise {
+ const accessibleName = await tile.getAttribute("aria-label");
+ const match = accessibleName?.match(/(\d+)\s+unread message/);
+ return match ? parseInt(match[1], 10) : 0;
}
/**
@@ -487,7 +490,7 @@ class Helpers {
// .toBeLessThan doesn't have a retry mechanism, so we use .poll
await expect
.poll(async () => {
- return parseInt(await tile.locator(".mx_NotificationBadge_count").textContent(), 10);
+ return this.unreadCountForRoomTile(tile);
})
.toBeLessThan(lessThan);
}
@@ -505,7 +508,7 @@ class Helpers {
// .toBeGreaterThan doesn't have a retry mechanism, so we use .poll
await expect
.poll(async () => {
- return parseInt(await tile.locator(".mx_NotificationBadge_count").textContent(), 10);
+ return this.unreadCountForRoomTile(tile);
})
.toBeGreaterThan(greaterThan);
}
@@ -596,24 +599,15 @@ class Helpers {
await button.click();
}
- /**
- * Toggle the `Show rooms with unread messages first` option for the room list
- */
- async toggleRoomUnreadOrder() {
- await this.toggleRoomListMenu();
- await this.page.getByText("Show rooms with unread messages first").click();
- // Close contextual menu
- await this.page.locator(".mx_ContextualMenu_background").click();
- }
-
/**
* Assert that the room list is ordered as expected
* @param rooms
*/
async assertRoomListOrder(rooms: Array<{ name: string }>) {
- const roomList = this.page.locator(".mx_RoomTile_title");
+ const roomListContainer = this.page.getByTestId("room-list");
+ const roomTiles = roomListContainer.getByRole("option");
for (const [i, room] of rooms.entries()) {
- await expect(roomList.nth(i)).toHaveText(room.name);
+ await expect(roomTiles.nth(i)).toHaveAccessibleName(new RegExp(`${room.name}`));
}
}
}
diff --git a/playwright/e2e/read-receipts/read-receipts.spec.ts b/playwright/e2e/read-receipts/read-receipts.spec.ts
index 8ebce22b52..b1142bae59 100644
--- a/playwright/e2e/read-receipts/read-receipts.spec.ts
+++ b/playwright/e2e/read-receipts/read-receipts.spec.ts
@@ -112,7 +112,7 @@ test.describe("Read receipts", { tag: "@mergequeue" }, () => {
const main3 = await sendMessage(bot);
// (So the room starts off unread)
- await expect(page.getByLabel(`${otherRoomName} 3 unread messages.`)).toBeVisible();
+ await expect(page.getByLabel(`${otherRoomName} with 3 unread messages.`)).toBeVisible();
// When we send a threaded receipt for the last event in main
// And an unthreaded receipt for an earlier event
@@ -147,13 +147,13 @@ test.describe("Read receipts", { tag: "@mergequeue" }, () => {
await sendMessage(bot);
// (The room starts off unread)
- await expect(page.getByLabel(`${otherRoomName} 3 unread messages.`)).toBeVisible();
+ await expect(page.getByLabel(`${otherRoomName} with 3 unread messages.`)).toBeVisible();
// When we send a threaded receipt for the second-last event in main
await sendThreadedReadReceipt(app, main2);
// Then the room has only one unread
- await expect(page.getByLabel(`${otherRoomName} 1 unread message.`)).toBeVisible();
+ await expect(page.getByLabel(`${otherRoomName} with 1 unread message.`)).toBeVisible();
});
test("Considers room read if there is only a main thread and we have a main receipt", async ({
@@ -166,7 +166,7 @@ test.describe("Read receipts", { tag: "@mergequeue" }, () => {
await sendMessage(bot);
const main3 = await sendMessage(bot);
// (The room starts off unread)
- await expect(page.getByLabel(`${otherRoomName} 3 unread messages.`)).toBeVisible();
+ await expect(page.getByLabel(`${otherRoomName} with 3 unread messages.`)).toBeVisible();
// When we send a threaded receipt for the last event in main
await sendThreadedReadReceipt(app, main3);
@@ -186,7 +186,7 @@ test.describe("Read receipts", { tag: "@mergequeue" }, () => {
const thread1a = await botSendThreadMessage(bot, main1.event_id);
await botSendThreadMessage(bot, main1.event_id);
// 1 unread on the main thread, 2 in the new thread that aren't shown
- await expect(page.getByLabel(`${otherRoomName} 1 unread message.`)).toBeVisible();
+ await expect(page.getByLabel(`${otherRoomName} with 1 unread message.`)).toBeVisible();
// When we send receipts for main, and the second-last in the thread
await sendThreadedReadReceipt(app, main1);
@@ -203,7 +203,7 @@ test.describe("Read receipts", { tag: "@mergequeue" }, () => {
await botSendThreadMessage(bot, main1.event_id);
const thread1b = await botSendThreadMessage(bot, main1.event_id);
// 1 unread on the main thread, 2 in the new thread which don't show
- await expect(page.getByLabel(`${otherRoomName} 1 unread message.`)).toBeVisible();
+ await expect(page.getByLabel(`${otherRoomName} with 1 unread message.`)).toBeVisible();
// When we send receipts for main, and the last in the thread
await sendThreadedReadReceipt(app, main1);
@@ -226,7 +226,7 @@ test.describe("Read receipts", { tag: "@mergequeue" }, () => {
const thread1a = await botSendThreadMessage(bot, main1.event_id);
await botSendThreadMessage(bot, main1.event_id);
// 1 unread on the main thread, 2 in the new thread which don't count
- await expect(page.getByLabel(`${otherRoomName} 1 unread message.`)).toBeVisible();
+ await expect(page.getByLabel(`${otherRoomName} with 1 unread message.`)).toBeVisible();
// When we send an unthreaded receipt for the second-last in the thread
await sendUnthreadedReadReceipt(app, thread1a);
@@ -251,7 +251,7 @@ test.describe("Read receipts", { tag: "@mergequeue" }, () => {
const thread1b = await botSendThreadMessage(bot, main1.event_id);
await sendMessage(bot);
// 2 unreads on the main thread, 2 in the new thread which don't count
- await expect(page.getByLabel(`${otherRoomName} 2 unread messages.`)).toBeVisible();
+ await expect(page.getByLabel(`${otherRoomName} with 2 unread messages.`)).toBeVisible();
// When we send an unthreaded receipt for the last in the thread
await sendUnthreadedReadReceipt(app, thread1b);
@@ -259,7 +259,7 @@ test.describe("Read receipts", { tag: "@mergequeue" }, () => {
// Then the room has only one unread - the one in the
// main thread, because it is later than the unthreaded
// receipt.
- await expect(page.getByLabel(`${otherRoomName} 1 unread message.`)).toBeVisible();
+ await expect(page.getByLabel(`${otherRoomName} with 1 unread message.`)).toBeVisible();
});
/**
@@ -291,7 +291,9 @@ test.describe("Read receipts", { tag: "@mergequeue" }, () => {
const uriEncodedLastMessageId = encodeURIComponent(lastMessageId);
// wait until all messages have been received
- await expect(page.getByLabel(`${otherRoomName} ${sendMessageResponses.length} unread messages.`)).toBeVisible();
+ await expect(
+ page.getByLabel(`${otherRoomName} with ${sendMessageResponses.length} unread messages.`),
+ ).toBeVisible();
// switch to the room with the messages
await page.goto(`/#/room/${otherRoomId}`);
diff --git a/playwright/e2e/read-receipts/room-list-order.spec.ts b/playwright/e2e/read-receipts/room-list-order.spec.ts
index e940c45b87..405cdf22ed 100644
--- a/playwright/e2e/read-receipts/room-list-order.spec.ts
+++ b/playwright/e2e/read-receipts/room-list-order.spec.ts
@@ -12,7 +12,7 @@ import { test } from ".";
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.describe("Room list order", () => {
- test("Rooms with unread messages appear at the top of room list if 'unread first' is selected", async ({
+ test("Rooms with unread messages appear at the top of room list with default 'activity' ordering", async ({
roomAlpha: room1,
roomBeta: room2,
util,
@@ -22,15 +22,18 @@ test.describe("Read receipts", { tag: "@mergequeue" }, () => {
await util.goTo(room2);
// Display the unread first room
- await util.toggleRoomUnreadOrder();
await util.receiveMessages(room1, ["Msg1"]);
await page.reload();
+ // switch rooms so they can re-order in the list
+ await util.goTo(room1);
+
// Room 1 has an unread message and should be displayed first
+ // (as the default is to sort by activity)
await util.assertRoomListOrder([room1, room2]);
});
- test("Rooms with unread threads appear at the top of room list if 'unread first' is selected", async ({
+ test("Rooms with unread threads appear at the top of room list with default 'activity' order", async ({
roomAlpha: room1,
roomBeta: room2,
util,
@@ -42,7 +45,6 @@ test.describe("Read receipts", { tag: "@mergequeue" }, () => {
await util.assertRead(room1);
// Display the unread first room
- await util.toggleRoomUnreadOrder();
await util.receiveMessages(room1, [msg.threadedOff("Msg1", "Resp1")]);
await util.saveAndReload();
diff --git a/playwright/e2e/release-announcement/index.ts b/playwright/e2e/release-announcement/index.ts
index 2a2e704d74..e28fa6f989 100644
--- a/playwright/e2e/release-announcement/index.ts
+++ b/playwright/e2e/release-announcement/index.ts
@@ -30,9 +30,8 @@ export class Helpers {
/**
* Get the release announcement with the given name.
* @param name
- * @private
*/
- private getReleaseAnnouncement(name: string) {
+ public getReleaseAnnouncement(name: string) {
return this.page.getByRole("dialog", { name });
}
@@ -55,16 +54,6 @@ export class Helpers {
assertReleaseAnnouncementIsNotVisible(name: string) {
return expect(this.getReleaseAnnouncement(name)).not.toBeVisible();
}
-
- /**
- * Mark the release announcement with the given name as read.
- * If the release announcement is not visible, this will throw an error.
- * @param name
- */
- async markReleaseAnnouncementAsRead(name: string) {
- const dialog = this.getReleaseAnnouncement(name);
- await dialog.getByRole("button", { name: "Ok" }).click();
- }
}
export { expect };
diff --git a/playwright/e2e/release-announcement/releaseAnnouncement.spec.ts b/playwright/e2e/release-announcement/releaseAnnouncement.spec.ts
index 812b66b796..34c90ddf03 100644
--- a/playwright/e2e/release-announcement/releaseAnnouncement.spec.ts
+++ b/playwright/e2e/release-announcement/releaseAnnouncement.spec.ts
@@ -22,27 +22,36 @@ test.describe("Release announcement", () => {
await app.viewRoomById(roomId);
await use({ roomId });
},
+ labsFlags: ["feature_new_room_list"],
});
test(
- "should display the pinned messages release announcement",
+ "should display the new room list release announcement",
{ tag: "@screenshot" },
async ({ page, app, room, util }) => {
- await app.toggleRoomInfoPanel();
+ // dismiss the toast so the announcement appears
+ await page.getByRole("button", { name: "Dismiss" }).click();
- const name = "All new pinned messages";
+ const newSoundsName = "We’ve refreshed your sounds";
+ // The new sounds release announcement should be displayed
+ await util.assertReleaseAnnouncementIsVisible(newSoundsName);
+ // Hide the new sounds release announcement
+ const newSoundsDialog = util.getReleaseAnnouncement(newSoundsName);
+ await newSoundsDialog.getByRole("button", { name: "OK" }).click();
- // The release announcement should be displayed
- await util.assertReleaseAnnouncementIsVisible(name);
- // Hide the release announcement
- await util.markReleaseAnnouncementAsRead(name);
- await util.assertReleaseAnnouncementIsNotVisible(name);
+ const newRoomListName = "Chats has a new look!";
+ // The new room list release announcement should be displayed
+ await util.assertReleaseAnnouncementIsVisible(newRoomListName);
+ // Hide the new room list release announcement
+ const dialog = util.getReleaseAnnouncement(newRoomListName);
+ await dialog.getByRole("button", { name: "Next" }).click();
+
+ await util.assertReleaseAnnouncementIsNotVisible(newRoomListName);
await page.reload();
- await app.toggleRoomInfoPanel();
- await expect(page.getByRole("menuitem", { name: "Pinned messages" })).toBeVisible();
- // Check that once the release announcement has been marked as viewed, it does not appear again
- await util.assertReleaseAnnouncementIsNotVisible(name);
+ await expect(page.getByRole("button", { name: "Room options" })).toBeVisible();
+ // Check that once the release announcements has been marked as viewed, it does not appear again
+ await util.assertReleaseAnnouncementIsNotVisible(newRoomListName);
},
);
});
diff --git a/playwright/e2e/room/create-room.spec.ts b/playwright/e2e/room/create-room.spec.ts
new file mode 100644
index 0000000000..9424ee4e32
--- /dev/null
+++ b/playwright/e2e/room/create-room.spec.ts
@@ -0,0 +1,113 @@
+/*
+Copyright 2024 New Vector Ltd.
+Copyright 2022, 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 { SettingLevel } from "../../../src/settings/SettingLevel";
+import { UIFeature } from "../../../src/settings/UIFeature";
+import { test, expect } from "../../element-web-test";
+
+const name = "Test room";
+const topic = "A decently explanatory topic for a test room.";
+
+test.describe("Create Room", () => {
+ test.use({ displayName: "Jim" });
+
+ test(
+ "should create a public room with name, topic & address set",
+ { tag: "@screenshot" },
+ async ({ page, user, app, axe }) => {
+ const dialog = await app.openCreateRoomDialog();
+ // Fill name & topic
+ await dialog.getByRole("textbox", { name: "Name" }).fill(name);
+ await dialog.getByRole("textbox", { name: "Topic" }).fill(topic);
+ // Change room to public
+ await dialog.getByRole("button", { name: "Room visibility" }).click();
+ await dialog.getByRole("option", { name: "Public room" }).click();
+ // Fill room address
+ await dialog.getByRole("textbox", { name: "Room address" }).fill("test-create-room-standard");
+
+ axe.disableRules("color-contrast"); // XXX: Inheriting colour contrast issues from room view.
+ await expect(axe).toHaveNoViolations();
+ // Snapshot it
+ await expect(dialog).toMatchScreenshot("create-room.png");
+
+ // Submit
+ await dialog.getByRole("button", { name: "Create room" }).click();
+
+ await expect(page).toHaveURL(new RegExp(`/#/room/#test-create-room-standard:${user.homeServer}`));
+ const header = page.locator(".mx_RoomHeader");
+ await expect(header).toContainText(name);
+ },
+ );
+
+ test("should allow us to start a chat and show encryption state", async ({ page, user, app }) => {
+ await page.getByRole("button", { name: "Add", exact: true }).click();
+ await page.getByRole("menuitem", { name: "Start chat" }).click();
+
+ await page.getByTestId("invite-dialog-input").fill(user.userId);
+
+ await page.getByRole("button", { name: "Go" }).click();
+
+ await expect(page.getByText("Encryption enabled")).toBeVisible();
+ await expect(page.getByText("Send your first message to")).toBeVisible();
+
+ const composer = page.getByRole("region", { name: "Message composer" });
+ await expect(composer.getByRole("textbox", { name: "Send a message…" })).toBeVisible();
+ });
+
+ test("should create a video room", { tag: "@screenshot" }, async ({ page, user, app }) => {
+ await app.settings.setValue("feature_video_rooms", null, SettingLevel.DEVICE, true);
+
+ const dialog = await app.openCreateRoomDialog("New video room");
+ // Fill name & topic
+ await dialog.getByRole("textbox", { name: "Name" }).fill(name);
+ await dialog.getByRole("textbox", { name: "Topic" }).fill(topic);
+ // Change room to public
+ await dialog.getByRole("button", { name: "Room visibility" }).click();
+ await dialog.getByRole("option", { name: "Public room" }).click();
+ // Fill room address
+ await dialog.getByRole("textbox", { name: "Room address" }).fill("test-create-room-video");
+ // Snapshot it
+ await expect(dialog).toMatchScreenshot("create-video-room.png");
+
+ // Submit
+ await dialog.getByRole("button", { name: "Create video room" }).click();
+
+ await expect(page).toHaveURL(new RegExp(`/#/room/#test-create-room-video:${user.homeServer}`));
+ const header = page.locator(".mx_RoomHeader");
+ await expect(header).toContainText(name);
+ });
+
+ test.describe("Should hide public room option if not allowed", () => {
+ test.use({
+ config: {
+ setting_defaults: {
+ [UIFeature.AllowCreatingPublicRooms]: false,
+ },
+ },
+ });
+
+ test("should disallow creating public rooms", { tag: "@screenshot" }, async ({ page, user, app, axe }) => {
+ const dialog = await app.openCreateRoomDialog();
+ // Fill name & topic
+ await dialog.getByRole("textbox", { name: "Name" }).fill(name);
+ await dialog.getByRole("textbox", { name: "Topic" }).fill(topic);
+
+ axe.disableRules("color-contrast"); // XXX: Inheriting colour contrast issues from room view.
+ await expect(axe).toHaveNoViolations();
+ // Snapshot it
+ await expect(dialog).toMatchScreenshot("create-room-no-public.png");
+
+ // Submit
+ await dialog.getByRole("button", { name: "Create room" }).click();
+
+ await expect(page).toHaveURL(new RegExp(`/#/room/!.+`));
+ const header = page.locator(".mx_RoomHeader");
+ await expect(header).toContainText(name);
+ });
+ });
+});
diff --git a/playwright/e2e/room/room-header.spec.ts b/playwright/e2e/room/room-header.spec.ts
index 5f14bfc85f..2f681fe3d3 100644
--- a/playwright/e2e/room/room-header.spec.ts
+++ b/playwright/e2e/room/room-header.spec.ts
@@ -15,6 +15,11 @@ import { type ElementAppPage } from "../../pages/ElementAppPage";
test.describe("Room Header", () => {
test.use({
displayName: "Sakura",
+ config: {
+ features: {
+ feature_new_room_list: false,
+ },
+ },
});
test.describe("with feature_notifications enabled", () => {
@@ -23,7 +28,7 @@ test.describe("Room Header", () => {
});
test("should render default buttons properly", { tag: "@screenshot" }, async ({ page, app, user }) => {
await app.client.createRoom({ name: "Test Room" });
- await app.viewRoomByName("Test Room");
+ await app.viewRoomByNameOnOldRoomList("Test Room");
const header = page.locator(".mx_RoomHeader");
@@ -61,7 +66,7 @@ test.describe("Room Header", () => {
"officia deserunt mollit anim id est laborum.";
await app.client.createRoom({ name: LONG_ROOM_NAME });
- await app.viewRoomByName(LONG_ROOM_NAME);
+ await app.viewRoomByNameOnOldRoomList(LONG_ROOM_NAME);
const header = page.locator(".mx_RoomHeader");
// Wait until the room name is set
@@ -86,7 +91,7 @@ test.describe("Room Header", () => {
test("should render room header icon correctly", { tag: "@screenshot" }, async ({ page, app, user }) => {
await app.client.createRoom({ name: "Test Room", visibility: "public" as Visibility });
- await app.viewRoomByName("Test Room");
+ await app.viewRoomByNameOnOldRoomList("Test Room");
const header = page.locator(".mx_RoomHeader");
@@ -106,7 +111,7 @@ test.describe("Room Header", () => {
await page.getByRole("button", { name: "Create video room" }).click();
- await app.viewRoomByName("Test video room");
+ await app.viewRoomByNameOnOldRoomList("Test video room");
};
test.describe("and with feature_notifications enabled", () => {
diff --git a/playwright/e2e/room_options/marked_unread.spec.ts b/playwright/e2e/room_options/marked_unread.spec.ts
index 2817bbc921..911404e881 100644
--- a/playwright/e2e/room_options/marked_unread.spec.ts
+++ b/playwright/e2e/room_options/marked_unread.spec.ts
@@ -34,7 +34,7 @@ test.describe("Mark as Unread", () => {
await bot.sendMessage(roomId, "I am a robot. Beep.");
// Regular notification on new message
- await expect(page.getByLabel(TEST_ROOM_NAME + " 1 unread message.")).toBeVisible();
+ await expect(page.getByLabel(`Open room ${TEST_ROOM_NAME} with 1 unread message.`)).toBeVisible();
await expect(page).toHaveTitle("Element [1]");
await page.goto("/#/room/" + roomId);
@@ -48,9 +48,12 @@ test.describe("Mark as Unread", () => {
const roomTile = page.getByLabel(TEST_ROOM_NAME);
await roomTile.focus();
- await roomTile.getByRole("button", { name: "Room options" }).click();
+ await roomTile.getByRole("button", { name: "More Options" }).click();
await page.getByRole("menuitem", { name: "Mark as unread" }).click();
- await expect(page.getByLabel(TEST_ROOM_NAME + " Unread messages.")).toBeVisible();
+ // focus the user menu to avoid to have hover decoration
+ await page.getByRole("button", { name: "User menu" }).focus();
+
+ await expect(roomTile.getByTestId("notification-decoration")).toBeVisible();
});
});
diff --git a/playwright/e2e/settings/appearance-user-settings-tab/index.ts b/playwright/e2e/settings/appearance-user-settings-tab/index.ts
index 29e51fb0dd..e6eccac15a 100644
--- a/playwright/e2e/settings/appearance-user-settings-tab/index.ts
+++ b/playwright/e2e/settings/appearance-user-settings-tab/index.ts
@@ -85,7 +85,7 @@ class Helpers {
* Return the system theme toggle
*/
getMatchSystemThemeCheckbox() {
- return this.getThemePanel().getByRole("checkbox", { name: "Match system theme" });
+ return this.getThemePanel().getByRole("switch", { name: "Match system theme" });
}
/**
@@ -219,7 +219,7 @@ class Helpers {
* Return the compact layout checkbox
*/
getCompactLayoutCheckbox() {
- return this.getMessageLayoutPanel().getByRole("checkbox", { name: "Show compact text and messages" });
+ return this.getMessageLayoutPanel().getByRole("switch", { name: "Show compact text and messages" });
}
/**
diff --git a/playwright/e2e/settings/encryption-user-tab/encryption-tab.spec.ts b/playwright/e2e/settings/encryption-user-tab/encryption-tab.spec.ts
index ed1daecf35..291be4442a 100644
--- a/playwright/e2e/settings/encryption-user-tab/encryption-tab.spec.ts
+++ b/playwright/e2e/settings/encryption-user-tab/encryption-tab.spec.ts
@@ -117,7 +117,7 @@ test.describe("Encryption tab", () => {
await verifySession(app, recoveryKey.encodedPrivateKey);
await util.openEncryptionTab();
- await page.getByRole("checkbox", { name: "Allow key storage" }).click();
+ await page.getByRole("switch", { name: "Allow key storage" }).click();
await expect(
page.getByRole("heading", { name: "Are you sure you want to turn off key storage and delete it?" }),
@@ -136,7 +136,7 @@ test.describe("Encryption tab", () => {
await page.getByRole("button", { name: "Delete key storage" }).click();
- await expect(page.getByRole("checkbox", { name: "Allow key storage" })).not.toBeChecked();
+ await expect(page.getByRole("switch", { name: "Allow key storage" })).not.toBeChecked();
for (const prom of deleteRequestPromises) {
const request = await prom;
@@ -160,15 +160,15 @@ test.describe("Encryption tab", () => {
// We will reset our identity
await settings.getByRole("button", { name: "Verify this device" }).click();
- await page.getByRole("button", { name: "Proceed with reset" }).click();
+ await page.getByRole("button", { name: "Can't confirm?" }).click();
// First try cancelling and restarting
await page.getByRole("button", { name: "Cancel" }).click();
- await page.getByRole("button", { name: "Proceed with reset" }).click();
+ await page.getByRole("button", { name: "Can't confirm?" }).click();
// Then click outside the dialog and restart
await page.locator("li").filter({ hasText: "Encryption" }).click({ force: true });
- await page.getByRole("button", { name: "Proceed with reset" }).click();
+ await page.getByRole("button", { name: "Can't confirm?" }).click();
// Finally we actually continue
await page.getByRole("button", { name: "Continue" }).click();
diff --git a/playwright/e2e/settings/encryption-user-tab/index.ts b/playwright/e2e/settings/encryption-user-tab/index.ts
index a7351fd2b4..3d180047a1 100644
--- a/playwright/e2e/settings/encryption-user-tab/index.ts
+++ b/playwright/e2e/settings/encryption-user-tab/index.ts
@@ -43,7 +43,7 @@ class Helpers {
*/
async verifyDevice(recoveryKey: GeneratedSecretStorageKey) {
// Select the security phrase
- await this.page.getByRole("button", { name: "Verify with Recovery Key" }).click();
+ await this.page.getByRole("button", { name: "Use recovery key" }).click();
await this.enterRecoveryKey(recoveryKey);
await this.page.getByRole("button", { name: "Done" }).click();
}
@@ -104,7 +104,10 @@ class Helpers {
const clipboardContent = await this.app.getClipboard();
await dialog.getByRole("textbox").fill(clipboardContent);
- await dialog.getByRole("button", { name: confirmButtonLabel }).click();
+ const button = dialog.getByRole("button", { name: confirmButtonLabel });
+ await button.click();
+ // Button should disable immediately after clicking.
+ await expect(button).toBeDisabled();
await expect(this.getEncryptionRecoverySection()).toMatchScreenshot("default-recovery.png");
}
}
diff --git a/playwright/e2e/settings/notifications/notifications-settings-2-tab.spec.ts b/playwright/e2e/settings/notifications/notifications-settings-2-tab.spec.ts
new file mode 100644
index 0000000000..48f03ecf0d
--- /dev/null
+++ b/playwright/e2e/settings/notifications/notifications-settings-2-tab.spec.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 { test, expect } from "../../../element-web-test";
+import { SettingLevel } from "../../../../src/settings/SettingLevel";
+
+test.describe("Notifications 2 tab", () => {
+ test.use({
+ displayName: "Alice",
+ });
+
+ test("should display notification settings", { tag: "@screenshot" }, async ({ page, app, user, axe }) => {
+ await app.settings.setValue("feature_notification_settings2", null, SettingLevel.DEVICE, true);
+ await page.setViewportSize({ width: 1024, height: 2000 });
+ const settings = await app.settings.openUserSettings("Notifications");
+
+ axe.disableRules("color-contrast"); // XXX: Inheriting colour contrast issues from room view.
+ await expect(axe).toHaveNoViolations();
+ await expect(settings).toMatchScreenshot("standard-notifications-2-settings.png", {
+ // Mask the mxid.
+ mask: [settings.locator("#mx_NotificationSettings2_MentionCheckbox span")],
+ });
+ });
+});
diff --git a/playwright/e2e/settings/notifications/notifications-settings-tab.spec.ts b/playwright/e2e/settings/notifications/notifications-settings-tab.spec.ts
new file mode 100644
index 0000000000..a63e730788
--- /dev/null
+++ b/playwright/e2e/settings/notifications/notifications-settings-tab.spec.ts
@@ -0,0 +1,25 @@
+/*
+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 { test, expect } from "../../../element-web-test";
+
+test.describe("Notifications tab", () => {
+ test.use({
+ displayName: "Alice",
+ });
+
+ test("should display notification settings", { tag: "@screenshot" }, async ({ page, app, user, axe }) => {
+ await page.setViewportSize({ width: 1024, height: 1400 });
+ const settings = await app.settings.openUserSettings("Notifications");
+ await settings.getByLabel("Enable notifications for this account").check();
+ await settings.getByLabel("Enable notifications for this device").check();
+
+ axe.disableRules("color-contrast"); // XXX: Inheriting colour contrast issues from room view.
+ await expect(axe).toHaveNoViolations();
+ await expect(settings).toMatchScreenshot("standard-notification-settings.png");
+ });
+});
diff --git a/playwright/e2e/settings/roles-permissions-room-settings-tab.spec.ts b/playwright/e2e/settings/room-settings/roles-permissions-room-settings-tab.spec.ts
similarity index 97%
rename from playwright/e2e/settings/roles-permissions-room-settings-tab.spec.ts
rename to playwright/e2e/settings/room-settings/roles-permissions-room-settings-tab.spec.ts
index 06ee25e9cb..d287234af5 100644
--- a/playwright/e2e/settings/roles-permissions-room-settings-tab.spec.ts
+++ b/playwright/e2e/settings/room-settings/roles-permissions-room-settings-tab.spec.ts
@@ -8,7 +8,7 @@
import { type Locator } from "@playwright/test";
-import { test, expect } from "../../element-web-test";
+import { test, expect } from "../../../element-web-test";
test.describe("Roles & Permissions room settings tab", () => {
const roomName = "Test room";
diff --git a/playwright/e2e/settings/room-settings/room-security-tab.spec.ts b/playwright/e2e/settings/room-settings/room-security-tab.spec.ts
new file mode 100644
index 0000000000..605d38abc2
--- /dev/null
+++ b/playwright/e2e/settings/room-settings/room-security-tab.spec.ts
@@ -0,0 +1,121 @@
+/*
+ * 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 Locator } from "@playwright/test";
+
+import { test, expect } from "../../../element-web-test";
+
+test.describe("Roles & Permissions room settings tab", () => {
+ const roomName = "Test room";
+
+ test.use({
+ displayName: "Alice",
+ });
+
+ let settings: Locator;
+
+ test.beforeEach(async ({ user, app }) => {
+ await app.client.createRoom({
+ name: roomName,
+ power_level_content_override: {
+ events: {
+ // Set the join rules as lower than the history vis to test an edge case.
+ ["m.room.join_rules"]: 80,
+ ["m.room.history_visibility"]: 100,
+ },
+ },
+ });
+ await app.viewRoomByName(roomName);
+ settings = await app.settings.openRoomSettings("Security & Privacy");
+ });
+
+ test(
+ "should be able to toggle on encryption in a room",
+ { tag: "@screenshot" },
+ async ({ page, app, user, axe }) => {
+ await page.setViewportSize({ width: 1024, height: 1400 });
+ const encryptedToggle = settings.getByLabel("Encrypted");
+ await encryptedToggle.click();
+
+ // Accept the dialog.
+ await page.getByRole("button", { name: "Ok " }).click();
+
+ await expect(encryptedToggle).toBeChecked();
+ await expect(encryptedToggle).toBeDisabled();
+
+ await settings.getByLabel("Only send messages to verified users.").check();
+
+ axe.disableRules("color-contrast"); // XXX: Inheriting colour contrast issues from room view.
+ await expect(axe).toHaveNoViolations();
+ await expect(settings).toMatchScreenshot("room-security-settings.png");
+ },
+ );
+
+ test(
+ "should automatically adjust history visibility when a room is changed from public to private",
+ { tag: "@screenshot" },
+ async ({ page, app, user, axe }) => {
+ await page.setViewportSize({ width: 1024, height: 1400 });
+
+ const settingsGroupAccess = page.getByRole("group", { name: "Access" });
+ const settingsGroupHistory = page.getByRole("group", { name: "Who can read history?" });
+
+ await settingsGroupAccess.getByText("Public").click();
+ await settingsGroupHistory.getByText("Anyone").click();
+
+ // Test that we have the warning appear.
+ axe.disableRules("color-contrast"); // XXX: Inheriting colour contrast issues from room view.
+ await expect(axe).toHaveNoViolations();
+ await expect(settings).toMatchScreenshot("room-security-settings-world-readable.png");
+
+ await settingsGroupAccess.getByText("Private (invite only)").click();
+ // Element should have automatically set the room to "sharing" history visibility
+ await expect(
+ settingsGroupHistory.getByText("Members only (since the point in time of selecting this option)"),
+ ).toBeChecked();
+ },
+ );
+
+ test(
+ "should disallow changing from public to private if the user cannot alter history",
+ { tag: "@screenshot" },
+ async ({ page, app, user, bot }) => {
+ await page.setViewportSize({ width: 1024, height: 1400 });
+
+ const settingsGroupAccess = page.getByRole("group", { name: "Access" });
+ const settingsGroupHistory = page.getByRole("group", { name: "Who can read history?" });
+
+ await settingsGroupAccess.getByText("Public").click();
+ await settingsGroupHistory.getByText("Anyone").click();
+
+ // De-op ourselves
+ await app.settings.switchTab("Roles & Permissions");
+
+ // Wait for the permissions list to be visible
+ await expect(settings.getByRole("heading", { name: "Permissions" })).toBeVisible();
+
+ const ourComboBox = settings.getByRole("combobox", { name: user.userId });
+ await ourComboBox.selectOption("Custom level");
+ const ourPl = settings.getByRole("spinbutton", { name: user.userId });
+ await ourPl.fill("80");
+ await ourPl.blur(); // Shows a warning on
+
+ // Accept the de-op
+ await page.getByRole("button", { name: "Continue" }).click();
+ await settings.getByRole("button", { name: "Apply", disabled: false }).click();
+
+ await app.settings.switchTab("Security & Privacy");
+
+ await settingsGroupAccess.getByText("Private (invite only)").click();
+ // Element should have automatically set the room to "sharing" history visibility
+ const errorDialog = page.getByRole("heading", { name: "Cannot make room private" });
+ await expect(errorDialog).toBeVisible();
+ await errorDialog.getByLabel("OK");
+ await expect(settingsGroupHistory.getByText("Anyone")).toBeChecked();
+ },
+ );
+});
diff --git a/playwright/e2e/settings/room-settings/room-video-tab.spec.ts b/playwright/e2e/settings/room-settings/room-video-tab.spec.ts
new file mode 100644
index 0000000000..feff22a04e
--- /dev/null
+++ b/playwright/e2e/settings/room-settings/room-video-tab.spec.ts
@@ -0,0 +1,42 @@
+/*
+ * 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 Locator } from "@playwright/test";
+
+import { test, expect } from "../../../element-web-test";
+import { SettingLevel } from "../../../../src/settings/SettingLevel";
+
+test.describe("Voice & Video room settings tab", () => {
+ const roomName = "Test room";
+
+ test.use({
+ displayName: "Alice",
+ });
+
+ let settings: Locator;
+
+ test.beforeEach(async ({ user, app, page }) => {
+ // Execute client actions before setting, as the setting will force a reload.
+ await app.client.createRoom({ name: roomName });
+ await app.settings.setValue("feature_group_calls", null, SettingLevel.DEVICE, true);
+ await app.viewRoomByName(roomName);
+ settings = await app.settings.openRoomSettings("Voice & Video");
+ });
+
+ test(
+ "should be able to toggle on Element Call in the room",
+ { tag: "@screenshot" },
+ async ({ page, app, user, axe }) => {
+ await page.setViewportSize({ width: 1024, height: 1400 });
+ const callToggle = settings.getByLabel("Enable Element Call as an additional calling option in this room");
+ await callToggle.check();
+ axe.disableRules("color-contrast"); // XXX: Inheriting colour contrast issues from room view.
+ await expect(axe).toHaveNoViolations();
+ await expect(settings).toMatchScreenshot("room-video-settings.png");
+ },
+ );
+});
diff --git a/playwright/e2e/settings/security-user-settings-tab.spec.ts b/playwright/e2e/settings/security-user-settings-tab.spec.ts
index 25bf1a9dbe..377cae7495 100644
--- a/playwright/e2e/settings/security-user-settings-tab.spec.ts
+++ b/playwright/e2e/settings/security-user-settings-tab.spec.ts
@@ -41,6 +41,18 @@ test.describe("Security user settings tab", () => {
});
});
+ test("should render the security tab", { tag: "@screenshot" }, async ({ app, page, user }) => {
+ await page.setViewportSize({ width: 1024, height: 1400 });
+ const tab = await app.settings.openUserSettings("Security");
+ await expect(tab).toMatchScreenshot("security-settings-tab.png", {
+ mask: [
+ // Contains IM name.
+ tab.locator("#mx_SetIntegrationManager_BodyText"),
+ tab.locator("#mx_SetIntegrationManager_ManagerName"),
+ ],
+ });
+ });
+
test("should be able to set an ID server", async ({ app, context, user, page }) => {
const tab = await app.settings.openUserSettings("Security");
diff --git a/playwright/e2e/sliding-sync/sliding-sync.spec.ts b/playwright/e2e/sliding-sync/sliding-sync.spec.ts
index b540cd11d5..d7ef96dae9 100644
--- a/playwright/e2e/sliding-sync/sliding-sync.spec.ts
+++ b/playwright/e2e/sliding-sync/sliding-sync.spec.ts
@@ -6,7 +6,7 @@ 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 { type Page, type Request } from "@playwright/test";
+import { type Locator, type Page, type Request } from "@playwright/test";
import { test as base, expect } from "../../element-web-test";
import type { ElementAppPage } from "../../pages/ElementAppPage";
@@ -38,7 +38,7 @@ const test = base.extend<{
test.describe("Sliding Sync", () => {
const checkOrder = async (wantOrder: string[], page: Page) => {
- await expect(page.getByRole("group", { name: "Rooms" }).locator(".mx_RoomTile_title")).toHaveText(wantOrder);
+ await expect(page.getByTestId("room-list").locator(".mx_RoomListItemView_text")).toHaveText(wantOrder);
};
const bumpRoom = async (roomId: string, app: ElementAppPage) => {
@@ -50,6 +50,18 @@ test.describe("Sliding Sync", () => {
});
};
+ function getPrimaryFilters(page: Page): Locator {
+ return page.getByTestId("primary-filters");
+ }
+
+ function getRoomOptionsMenu(page: Page): Locator {
+ return page.getByRole("button", { name: "Room Options" });
+ }
+
+ function getFilterExpandButton(page: Page): Locator {
+ return getPrimaryFilters(page).getByRole("button", { name: "Expand filter list" });
+ }
+
test.use({
config: {
features: {
@@ -69,20 +81,15 @@ test.describe("Sliding Sync", () => {
// create rooms and check room names are correct
for (const fruit of ["Apple", "Pineapple", "Orange"]) {
await app.client.createRoom({ name: fruit });
- await expect(page.getByRole("treeitem", { name: fruit })).toBeVisible();
+ await expect(page.getByRole("option", { name: `Open room ${fruit}` })).toBeVisible();
}
-
+ const roomList = page.getByTestId("room-list");
// Check count, 3 fruits + 1 testRoom = 4
- await expect(page.locator(".mx_RoomSublist_tiles").getByRole("treeitem")).toHaveCount(4);
+ await expect(roomList.getByRole("option")).toHaveCount(4);
await checkOrder(["Orange", "Pineapple", "Apple", "Test Room"], page);
- const locator = page.getByRole("group", { name: "Rooms" }).locator(".mx_RoomSublist_headerContainer");
- await locator.hover();
- await locator.getByRole("button", { name: "List options" }).click();
-
- // force click as the radio button's size is zero
- await page.getByRole("menuitemradio", { name: "A-Z" }).dispatchEvent("click");
- await expect(page.locator(".mx_StyledRadioButton_checked").getByText("A-Z")).toBeVisible();
+ await getRoomOptionsMenu(page).click();
+ await page.getByRole("menuitemradio", { name: "A-Z" }).click();
await checkOrder(["Apple", "Orange", "Pineapple", "Test Room"], page);
});
@@ -93,11 +100,11 @@ test.describe("Sliding Sync", () => {
for (const fruit of ["Apple", "Pineapple", "Orange"]) {
const id = await app.client.createRoom({ name: fruit });
roomIds.push(id);
- await expect(page.getByRole("treeitem", { name: fruit })).toBeVisible();
+ await expect(page.getByRole("option", { name: `Open room ${fruit}` })).toBeVisible();
}
// Select the Test Room
- await page.getByRole("treeitem", { name: "Test Room" }).click();
+ await page.getByRole("option", { name: "Open room Test Room" }).click();
const [apple, pineapple, orange] = roomIds;
await checkOrder(["Orange", "Pineapple", "Apple", "Test Room"], page);
await bumpRoom(apple, app);
@@ -116,7 +123,7 @@ test.describe("Sliding Sync", () => {
for (const fruit of ["Apple", "Pineapple", "Orange"]) {
const id = await app.client.createRoom({ name: fruit });
roomIds.push(id);
- await expect(page.getByRole("treeitem", { name: fruit })).toBeVisible();
+ await expect(page.getByRole("option", { name: `Open room ${fruit}` })).toBeVisible();
}
// Given a list of Orange, Pineapple, Apple - if Pineapple is active and a message is sent in Apple, the list should
@@ -124,7 +131,7 @@ test.describe("Sliding Sync", () => {
// be Apple, Orange Pineapple - only when you click on a different room do things reshuffle.
// Select the Pineapple room
- await page.getByRole("treeitem", { name: "Pineapple" }).click();
+ await page.getByRole("option", { name: "Open room Pineapple" }).click();
await checkOrder(["Orange", "Pineapple", "Apple", "Test Room"], page);
// Move Apple
@@ -132,7 +139,7 @@ test.describe("Sliding Sync", () => {
await checkOrder(["Apple", "Pineapple", "Orange", "Test Room"], page);
// Select the Test Room
- await page.getByRole("treeitem", { name: "Test Room" }).click();
+ await page.getByRole("option", { name: "Open room Test Room" }).click();
// the rooms reshuffle to match reality
await checkOrder(["Apple", "Orange", "Pineapple", "Test Room"], page);
@@ -142,25 +149,20 @@ test.describe("Sliding Sync", () => {
// send a message in the test room: unread notification count should increment
await bob.sendMessage(testRoom.roomId, "Hello World");
- const treeItemLocator1 = page.getByRole("treeitem", { name: "Test Room 1 unread message." });
- await expect(treeItemLocator1.locator(".mx_NotificationBadge_count")).toHaveText("1");
- // await expect(page.locator(".mx_NotificationBadge")).not.toHaveClass("mx_NotificationBadge_highlighted");
- await expect(treeItemLocator1.locator(".mx_NotificationBadge")).not.toHaveClass(
- /mx_NotificationBadge_highlighted/,
- );
+ const itemLocator1 = page.getByRole("option", { name: "Open room David Langley with 1 unread message." });
+ await expect(itemLocator1.getByTestId("notification-decoration")).toHaveText("1");
// send an @mention: highlight count (red) should be 2.
await bob.sendMessage(testRoom.roomId, `Hello ${user.displayName}`);
- const treeItemLocator2 = page.getByRole("treeitem", {
- name: "Test Room 2 unread messages including mentions.",
+ const itemLocator2 = page.getByRole("treeitem", {
+ name: "Open room Test Room 2 unread messages including mentions.",
});
- await expect(treeItemLocator2.locator(".mx_NotificationBadge_count")).toHaveText("2");
- await expect(treeItemLocator2.locator(".mx_NotificationBadge")).toHaveClass(/mx_NotificationBadge_highlighted/);
+ await expect(itemLocator2.getByTestId("notification-decoration")).toHaveText("2");
// click on the room, the notif counts should disappear
- await page.getByRole("treeitem", { name: "Test Room 2 unread messages including mentions." }).click();
+ await page.getByRole("option", { name: "Open room Test Room 2 unread messages including mentions." }).click();
await expect(
- page.getByRole("treeitem", { name: "Test Room" }).locator("mx_NotificationBadge_count"),
+ page.getByRole("option", { name: "Open room Test Room" }).getByTestId("notification-decoration"),
).not.toBeAttached();
});
@@ -175,7 +177,9 @@ test.describe("Sliding Sync", () => {
// wait for this message to arrive, tell by the room list resorting
await checkOrder(["Test Room", "Dummy"], page);
- await expect(page.getByRole("treeitem", { name: "Test Room" }).locator(".mx_NotificationBadge")).toBeAttached();
+ await expect(
+ page.getByRole("option", { name: "Open room Test Room" }).getByTestId("notification-decoration"),
+ ).toBeAttached();
});
test("should update user settings promptly", async ({ page, app }) => {
@@ -193,7 +197,7 @@ test.describe("Sliding Sync", () => {
for (const fruit of ["Apple", "Pineapple", "Orange"]) {
const id = await app.client.createRoom({ name: fruit });
roomIds.push(id);
- await expect(page.getByRole("treeitem", { name: fruit })).toBeVisible();
+ await expect(page.getByRole("option", { name: `Open room ${fruit}` })).toBeVisible();
}
const [roomAId, roomPId] = roomIds;
@@ -206,7 +210,7 @@ test.describe("Sliding Sync", () => {
// Select the Test Room and wait for playwright to get the request
const [request] = await Promise.all([
page.waitForRequest(matchRoomSubRequest(roomAId)),
- page.getByRole("treeitem", { name: "Apple", exact: true }).click(),
+ page.getByRole("option", { name: "Open room Apple", exact: true }).click(),
]);
const roomSubscriptions = request.postDataJSON().room_subscriptions;
expect(roomSubscriptions, "room_subscriptions is object").toBeDefined();
@@ -214,7 +218,7 @@ test.describe("Sliding Sync", () => {
// Switch to another room and wait for playwright to get the request
await Promise.all([
page.waitForRequest(matchRoomSubRequest(roomPId)),
- page.getByRole("treeitem", { name: "Pineapple", exact: true }).click(),
+ page.getByRole("option", { name: "Open room Pineapple", exact: true }).click(),
]);
});
@@ -240,34 +244,29 @@ test.describe("Sliding Sync", () => {
{ roomNames, clientUserId },
);
- await expect(
- page.getByRole("group", { name: "Invites" }).locator(".mx_RoomSublist_tiles").getByRole("treeitem"),
- ).toHaveCount(3);
+ await getFilterExpandButton(page).click();
+ const primaryFilters = getPrimaryFilters(page);
+ await primaryFilters.getByRole("option", { name: "Invites" }).click();
+
+ await expect(page.getByTestId("room-list").getByRole("option")).toHaveCount(3);
// Select the room to join
- await page.getByRole("treeitem", { name: "Room to Join" }).click();
+ await page.getByRole("option", { name: "Open room Room to Join" }).click();
// Accept the invite
await page.locator(".mx_RoomView").getByRole("button", { name: "Accept" }).click();
- await checkOrder(["Room to Join", "Test Room"], page);
+ await checkOrder(["Room to Rescind", "Room to Reject"], page);
// Select the room to reject
- await page.getByRole("treeitem", { name: "Room to Reject" }).click();
+ await page.getByRole("option", { name: "Open room Room to Reject" }).click();
// Decline the invite
await page.locator(".mx_RoomView").getByRole("button", { name: "Decline", exact: true }).click();
- await expect(
- page.getByRole("group", { name: "Invites" }).locator(".mx_RoomSublist_tiles").getByRole("treeitem"),
- ).toHaveCount(2);
+ await expect(page.getByTestId("room-list").getByRole("option")).toHaveCount(1);
- // check the lists are correct
- await checkOrder(["Room to Join", "Test Room"], page);
-
- const titleLocator = page.getByRole("group", { name: "Invites" }).locator(".mx_RoomTile_title");
- await expect(titleLocator).toHaveCount(1);
- await expect(titleLocator).toHaveText("Room to Rescind");
+ await expect(page.getByRole("option", { name: "Open room Room to Rescind" })).toBeVisible();
// now rescind the invite
await bot.evaluate(
@@ -277,10 +276,14 @@ test.describe("Sliding Sync", () => {
{ roomRescind, clientUserId },
);
+ await page.getByRole("option", { name: "Open room Room to Rescind" }).click();
+
+ await page.locator(".mx_RoomView").getByRole("button", { name: "Forget this room", exact: true }).click();
+
+ await primaryFilters.getByRole("option", { name: "Invites" }).click();
+
// Wait for the rescind to take effect and check the joined list once more
- await expect(
- page.getByRole("group", { name: "Rooms" }).locator(".mx_RoomSublist_tiles").getByRole("treeitem"),
- ).toHaveCount(2);
+ await expect(page.getByTestId("room-list").getByRole("option")).toHaveCount(2);
await checkOrder(["Room to Join", "Test Room"], page);
});
@@ -293,19 +296,27 @@ test.describe("Sliding Sync", () => {
await app.client.evaluate(async (client, roomId) => {
await client.setRoomTag(roomId, "m.favourite", { order: 0.5 });
}, roomId);
- await expect(page.getByRole("group", { name: "Favourites" }).getByText("Favourite DM")).toBeVisible();
- await expect(page.getByRole("group", { name: "People" }).getByText("Favourite DM")).not.toBeAttached();
+
+ await getFilterExpandButton(page).click();
+ const primaryFilters = getPrimaryFilters(page);
+ await primaryFilters.getByRole("option", { name: "Favourites" }).click();
+
+ await expect(page.getByRole("option", { name: "Favourite DM" })).toBeVisible();
+
+ await primaryFilters.getByRole("option", { name: "People" }).click();
+
+ await expect(page.getByRole("option", { name: "Favourite DM" })).not.toBeAttached();
});
// Regression test for a bug in SS mode, but would be useful to have in non-SS mode too.
// This ensures we are setting RoomViewStore state correctly.
test("should clear the reply to field when swapping rooms", async ({ page, app, testRoom }) => {
await app.client.createRoom({ name: "Other Room" });
- await expect(page.getByRole("treeitem", { name: "Other Room" })).toBeVisible();
+ await expect(page.getByRole("option", { name: "Open room Other Room" })).toBeVisible();
await app.client.sendMessage(testRoom.roomId, "Hello world");
// select the room
- await page.getByRole("treeitem", { name: "Test Room" }).click();
+ await page.getByRole("option", { name: "Open room Test Room" }).click();
await expect(page.locator(".mx_ReplyPreview")).not.toBeAttached();
@@ -318,13 +329,13 @@ test.describe("Sliding Sync", () => {
await expect(page.locator(".mx_ReplyPreview")).toBeVisible();
// now click Other Room
- await page.getByRole("treeitem", { name: "Other Room" }).click();
+ await page.getByRole("option", { name: "Open room Other Room" }).click();
// ensure the reply-to disappears
await expect(page.locator(".mx_ReplyPreview")).not.toBeAttached();
// click back
- await page.getByRole("treeitem", { name: "Test Room" }).click();
+ await page.getByRole("option", { name: "Open room Test Room" }).click();
// ensure the reply-to reappears
await expect(page.locator(".mx_ReplyPreview")).toBeVisible();
@@ -338,7 +349,7 @@ test.describe("Sliding Sync", () => {
await app.client.sendMessage(testRoom.roomId, "Reply to me");
// select the room
- await page.getByRole("treeitem", { name: "Test Room" }).click();
+ await page.getByRole("option", { name: "Open room Test Room" }).click();
await expect(page.locator(".mx_ReplyPreview")).not.toBeAttached();
// click reply-to on the Reply to me message
diff --git a/playwright/e2e/spaces/spaces.spec.ts b/playwright/e2e/spaces/spaces.spec.ts
index b3f7ecb0c9..19e619e9c6 100644
--- a/playwright/e2e/spaces/spaces.spec.ts
+++ b/playwright/e2e/spaces/spaces.spec.ts
@@ -11,6 +11,7 @@ import { test, expect } from "../../element-web-test";
import type { Preset, ICreateRoomOpts } from "matrix-js-sdk/src/matrix";
import { type ElementAppPage } from "../../pages/ElementAppPage";
import { isDendrite } from "../../plugins/homeserver/dendrite";
+import { UIFeature } from "../../../src/settings/UIFeature";
async function openSpaceCreateMenu(page: Page): Promise {
await page.getByRole("button", { name: "Create a space" }).click();
@@ -95,9 +96,9 @@ test.describe("Spaces", () => {
await page.getByRole("button", { name: "Go to my first room" }).click();
// Assert rooms exist in the room list
- await expect(page.getByRole("treeitem", { name: "General" })).toBeVisible();
- await expect(page.getByRole("treeitem", { name: "Random" })).toBeVisible();
- await expect(page.getByRole("treeitem", { name: "Jokes" })).toBeVisible();
+ await expect(page.getByRole("option", { name: "General" })).toBeVisible();
+ await expect(page.getByRole("option", { name: "Random" })).toBeVisible();
+ await expect(page.getByRole("option", { name: "Jokes" })).toBeVisible();
},
);
@@ -126,10 +127,10 @@ test.describe("Spaces", () => {
await page.getByRole("button", { name: "Skip for now" }).click();
// Assert rooms exist in the room list
- const roomList = page.getByRole("tree", { name: "Rooms" });
- await expect(roomList.getByRole("treeitem", { name: "General", exact: true })).toBeVisible();
- await expect(roomList.getByRole("treeitem", { name: "Random", exact: true })).toBeVisible();
- await expect(roomList.getByRole("treeitem", { name: "Projects", exact: true })).toBeVisible();
+ const roomList = page.getByRole("listbox", { name: "Room list", exact: true });
+ await expect(roomList.getByRole("option", { name: "General" })).toBeVisible();
+ await expect(roomList.getByRole("option", { name: "Random" })).toBeVisible();
+ await expect(roomList.getByRole("option", { name: "Projects" })).toBeVisible();
// Assert rooms exist in the space explorer
await expect(
@@ -199,7 +200,7 @@ test.describe("Spaces", () => {
await page.getByRole("button", { name: "Skip for now" }).click();
- await page.getByRole("button", { name: "Add room" }).click();
+ await page.getByRole("main").getByRole("button", { name: "Add" }).click();
await page.getByRole("menuitem", { name: "Add existing room" }).click();
await page.getByRole("checkbox", { name: "Sample Room" }).click();
@@ -376,4 +377,68 @@ test.describe("Spaces", () => {
await app.viewSpaceByName("Root Space");
await expect(page.locator(".mx_SpaceRoomView")).toMatchScreenshot("space-room-view.png");
});
+
+ test("should render spaces visibility settings", { tag: "@screenshot" }, async ({ page, app, user, axe }) => {
+ await app.client.createSpace({
+ name: "My Space",
+ });
+ await app.viewSpaceByName("My space");
+ await page.getByLabel("Settings", { exact: true }).click();
+ await app.settings.switchTab("Visibility");
+
+ axe.disableRules("color-contrast"); // XXX: Inheriting colour contrast issues from room view.
+ await expect(axe).toHaveNoViolations();
+ await expect(page.locator("#mx_tabpanel_SPACE_VISIBILITY_TAB")).toMatchScreenshot(
+ "space-visibility-settings.png",
+ );
+ });
+
+ test.describe("Should hide public spaces option if not allowed", () => {
+ test.use({
+ config: {
+ setting_defaults: {
+ [UIFeature.AllowCreatingPublicSpaces]: false,
+ },
+ },
+ });
+
+ test("should disallow creating public rooms", { tag: "@screenshot" }, async ({ page, user, app }) => {
+ const menu = await openSpaceCreateMenu(page);
+ await menu
+ .locator('.mx_SpaceBasicSettings_avatarContainer input[type="file"]')
+ .setInputFiles("playwright/sample-files/riot.png");
+ await menu.getByRole("textbox", { name: "Name" }).fill("This is a private space");
+ await expect(menu.getByRole("textbox", { name: "Address" })).not.toBeVisible();
+ await menu
+ .getByRole("textbox", { name: "Description" })
+ .fill("This is a private space because we can't make public ones");
+ await menu.getByRole("button", { name: "Create" }).click();
+
+ await page.getByRole("button", { name: "Me and my teammates" }).click();
+
+ // Create the default General & Random rooms, as well as a custom "Projects" room
+ await expect(page.getByPlaceholder("General")).toBeVisible();
+ await expect(page.getByPlaceholder("Random")).toBeVisible();
+ await page.getByPlaceholder("Support").fill("Projects");
+ await page.getByRole("button", { name: "Continue" }).click();
+ await page.getByRole("button", { name: "Skip for now" }).click();
+
+ // Assert rooms exist in the room list
+ const roomList = page.getByRole("listbox", { name: "Room list", exact: true });
+ await expect(roomList.getByRole("option", { name: "General" })).toBeVisible();
+ await expect(roomList.getByRole("option", { name: "Random" })).toBeVisible();
+ await expect(roomList.getByRole("option", { name: "Projects" })).toBeVisible();
+
+ // Assert rooms exist in the space explorer
+ await expect(
+ page.locator(".mx_SpaceHierarchy_list .mx_SpaceHierarchy_roomTile", { hasText: "General" }),
+ ).toBeVisible();
+ await expect(
+ page.locator(".mx_SpaceHierarchy_list .mx_SpaceHierarchy_roomTile", { hasText: "Random" }),
+ ).toBeVisible();
+ await expect(
+ page.locator(".mx_SpaceHierarchy_list .mx_SpaceHierarchy_roomTile", { hasText: "Projects" }),
+ ).toBeVisible();
+ });
+ });
});
diff --git a/playwright/e2e/spaces/threads-activity-centre/index.ts b/playwright/e2e/spaces/threads-activity-centre/index.ts
index a050175c83..cf889f64cc 100644
--- a/playwright/e2e/spaces/threads-activity-centre/index.ts
+++ b/playwright/e2e/spaces/threads-activity-centre/index.ts
@@ -377,7 +377,7 @@ export class Helpers {
* Expand the space panel
*/
expandSpacePanel() {
- return this.page.getByRole("button", { name: "Expand" }).click();
+ return this.page.getByRole("navigation", { name: "Spaces" }).getByRole("button", { name: "Expand" }).click();
}
/**
diff --git a/playwright/e2e/spotlight/spotlight.spec.ts b/playwright/e2e/spotlight/spotlight.spec.ts
index 542b537c13..e07643c193 100644
--- a/playwright/e2e/spotlight/spotlight.spec.ts
+++ b/playwright/e2e/spotlight/spotlight.spec.ts
@@ -30,13 +30,13 @@ async function startDM(app: ElementAppPage, page: Page, name: string): Promise {
// Send first message to actually start DM
await expect(roomHeaderName(page)).toHaveText(bot2.credentials.displayName);
- const locator = page.getByRole("textbox", { name: "Send an unencrypted message…" });
+ const locator = page.getByRole("textbox", { name: "Send a message…" });
await locator.fill("Hey!");
await locator.press("Enter");
// Assert DM exists by checking for the first message and the room being in the room list
await expect(page.locator(".mx_EventTile_body").filter({ hasText: "Hey!" })).toBeAttached({ timeout: 3000 });
- await expect(page.getByRole("group", { name: "People" })).toContainText(bot2.credentials.displayName);
+ await expect(
+ page.getByTestId("room-list").getByRole("option", { name: `Open room ${bot2.credentials.displayName}` }),
+ ).toBeVisible();
// Invite BotBob into existing DM with ByteBot
const dmRooms = await app.client.evaluate((client, userId) => {
@@ -279,7 +281,9 @@ test.describe("Spotlight", () => {
const groupDmName = await app.client.evaluate((client, id) => client.getRoom(id).name, dmRooms[0]);
await app.client.inviteUser(dmRooms[0], bot1.credentials.userId);
await expect(roomHeaderName(page).first()).toContainText(groupDmName);
- await expect(page.getByRole("group", { name: "People" }).first()).toContainText(groupDmName);
+ await expect(
+ page.getByTestId("room-list").getByRole("option", { name: `Open room ${groupDmName}` }),
+ ).toBeVisible();
// Search for BotBob by id, should return group DM and user
spotlight = await app.openSpotlight();
diff --git a/playwright/e2e/timeline/media-preview-settings.spec.ts b/playwright/e2e/timeline/media-preview-settings.spec.ts
index e32a7dbc82..617aa08c09 100644
--- a/playwright/e2e/timeline/media-preview-settings.spec.ts
+++ b/playwright/e2e/timeline/media-preview-settings.spec.ts
@@ -50,9 +50,12 @@ test.describe("Media preview settings", () => {
}
`,
});
- await expect(
- page.getByRole("tree", { name: "Rooms" }).getByRole("treeitem", { name: "Test room" }),
- ).toMatchScreenshot("invite-room-tree-no-avatar.png");
+
+ const testRoomTile = page
+ .getByRole("listbox", { name: "Room list" })
+ .getByRole("option", { name: "Test room" });
+ await expect(testRoomTile).toBeVisible();
+ await expect(testRoomTile).toMatchScreenshot("invite-room-tree-no-avatar.png");
// And then go back to being visible
settings = await app.settings.openUserSettings("Preferences");
@@ -70,9 +73,7 @@ test.describe("Media preview settings", () => {
}
`,
});
- await expect(
- page.getByRole("tree", { name: "Rooms" }).getByRole("treeitem", { name: "Test room" }),
- ).toMatchScreenshot("invite-room-tree-with-avatar.png");
+ await expect(testRoomTile).toMatchScreenshot("invite-room-tree-with-avatar.png");
});
test("should be able to hide media in rooms globally", async ({ page, app, room, user }) => {
diff --git a/playwright/e2e/timeline/timeline.spec.ts b/playwright/e2e/timeline/timeline.spec.ts
index d169afd66f..55ae3b16dc 100644
--- a/playwright/e2e/timeline/timeline.spec.ts
+++ b/playwright/e2e/timeline/timeline.spec.ts
@@ -908,23 +908,37 @@ test.describe("Timeline", () => {
});
});
- test("should be able to hide an image", { tag: "@screenshot" }, async ({ page, app, room, context }) => {
- await app.viewRoomById(room.roomId);
- await sendImage(app.client, room.roomId, NEW_AVATAR);
- await app.timeline.scrollToBottom();
- const imgTile = page.locator(".mx_MImageBody").first();
- await expect(imgTile).toBeVisible();
- await imgTile.hover();
- await page.getByRole("button", { name: "Hide" }).click();
+ test(
+ "should be able to hide an image",
+ { tag: "@screenshot" },
+ async ({ page, app, homeserver, room, context }) => {
+ await app.viewRoomById(room.roomId);
- // Check that the image is now hidden.
- await expect(page.getByRole("button", { name: "Show image" })).toBeVisible();
- });
+ const bot = new Bot(page, homeserver, {});
+ await bot.prepareClient();
+ await app.client.inviteUser(room.roomId, bot.credentials.userId);
- test("should be able to hide a video", async ({ page, app, room, context }) => {
+ await sendImage(bot, room.roomId, NEW_AVATAR);
+ await app.timeline.scrollToBottom();
+ const imgTile = page.locator(".mx_MImageBody").first();
+ await expect(imgTile).toBeVisible();
+ await imgTile.hover();
+ await page.getByRole("button", { name: "Hide" }).click();
+
+ // Check that the image is now hidden.
+ await expect(page.getByRole("button", { name: "Show image" })).toBeVisible();
+ },
+ );
+
+ test("should be able to hide a video", async ({ page, app, homeserver, room, context }) => {
await app.viewRoomById(room.roomId);
- const upload = await app.client.uploadContent(VIDEO_FILE, { name: "bbb.webm", type: "video/webm" });
- await app.client.sendEvent(room.roomId, null, "m.room.message" as EventType, {
+
+ const bot = new Bot(page, homeserver, {});
+ await bot.prepareClient();
+ await app.client.inviteUser(room.roomId, bot.credentials.userId);
+
+ const upload = await bot.uploadContent(VIDEO_FILE, { name: "bbb.webm", type: "video/webm" });
+ await bot.sendEvent(room.roomId, null, "m.room.message" as EventType, {
msgtype: "m.video" as MsgType,
body: "bbb.webm",
url: upload.content_uri,
diff --git a/playwright/e2e/voip/element-call.spec.ts b/playwright/e2e/voip/element-call.spec.ts
new file mode 100644
index 0000000000..1911843046
--- /dev/null
+++ b/playwright/e2e/voip/element-call.spec.ts
@@ -0,0 +1,339 @@
+/*
+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 { EventType, Preset } from "matrix-js-sdk/src/matrix";
+import { SettingLevel } from "../../../src/settings/SettingLevel";
+import { test, expect } from "../../element-web-test";
+import type { Credentials } from "../../plugins/homeserver";
+import type { Bot } from "../../pages/bot";
+
+function assertCommonCallParameters(
+ url: URLSearchParams,
+ hash: URLSearchParams,
+ user: Credentials,
+ room: { roomId: string },
+): void {
+ expect(url.has("widgetId")).toEqual(true);
+ expect(url.has("parentUrl")).toEqual(true);
+
+ expect(hash.get("perParticipantE2EE")).toEqual("false");
+ expect(hash.get("userId")).toEqual(user.userId);
+ expect(hash.get("deviceId")).toEqual(user.deviceId);
+ expect(hash.get("roomId")).toEqual(room.roomId);
+ expect(hash.get("preload")).toEqual("false");
+}
+
+async function sendRTCState(bot: Bot, roomId: string, notification?: "ring" | "notification") {
+ const resp = await bot.sendStateEvent(
+ roomId,
+ "org.matrix.msc3401.call.member",
+ {
+ application: "m.call",
+ call_id: "",
+ device_id: "OiDFxsZrjz",
+ expires: 180000000,
+ foci_preferred: [
+ {
+ livekit_alias: roomId,
+ livekit_service_url: "https://example.org",
+ type: "livekit",
+ },
+ ],
+ focus_active: {
+ focus_selection: "oldest_membership",
+ type: "livekit",
+ },
+ scope: "m.room",
+ },
+ `_@${bot.credentials.userId}_OiDFxsZrjz_m.call`,
+ );
+ if (!notification) {
+ return;
+ }
+ await bot.sendEvent(roomId, null, "org.matrix.msc4075.rtc.notification", {
+ "lifetime": 30000,
+ "m.mentions": {
+ room: true,
+ user_ids: [],
+ },
+ "m.relates_to": {
+ event_id: resp.event_id,
+ rel_type: "org.matrix.msc4075.rtc.notification.parent",
+ },
+ "notification_type": notification,
+ "sender_ts": 1758611895996,
+ });
+}
+
+test.describe("Element Call", () => {
+ test.use({
+ config: {
+ element_call: {
+ use_exclusively: false,
+ },
+ features: {
+ feature_group_calls: true,
+ },
+ },
+ displayName: "Alice",
+ botCreateOpts: {
+ autoAcceptInvites: true,
+ displayName: "Bob",
+ },
+ });
+
+ test.beforeEach(async ({ page, user, app }) => {
+ // Mock a widget page. It doesn't need to actually be Element Call.
+ await page.route("/widget.html", async (route) => {
+ await route.fulfill({
+ status: 200,
+ body: "
Hello world
",
+ });
+ });
+ await app.settings.setValue(
+ "Developer.elementCallUrl",
+ null,
+ SettingLevel.DEVICE,
+ new URL("/widget.html#", page.url()).toString(),
+ );
+ });
+
+ test.describe("Group Chat", () => {
+ test.use({
+ room: async ({ page, app, user, bot }, use) => {
+ const roomId = await app.client.createRoom({ name: "TestRoom", invite: [bot.credentials.userId] });
+ await use({ roomId });
+ },
+ });
+ test("should be able to start a video call", async ({ page, user, room, app }) => {
+ await app.viewRoomById(room.roomId);
+ await expect(page.getByText("Bob joined the room")).toBeVisible();
+
+ await page.getByRole("button", { name: "Video call" }).click();
+ await page.getByRole("menuitem", { name: "Element Call" }).click();
+
+ const frameUrlStr = await page.locator("iframe").getAttribute("src");
+ await expect(frameUrlStr).toBeDefined();
+ // Ensure we set the correct parameters for ECall.
+ const url = new URL(frameUrlStr);
+ const hash = new URLSearchParams(url.hash.slice(1));
+ assertCommonCallParameters(url.searchParams, hash, user, room);
+ expect(hash.get("intent")).toEqual("start_call");
+ expect(hash.get("skipLobby")).toEqual(null);
+ });
+
+ test("should be able to skip lobby by holding down shift", async ({ page, user, bot, room, app }) => {
+ await app.viewRoomById(room.roomId);
+ await expect(page.getByText("Bob joined the room")).toBeVisible();
+
+ await page.getByRole("button", { name: "Video call" }).click();
+ await page.keyboard.down("Shift");
+ await page.getByRole("menuitem", { name: "Element Call" }).click();
+ await page.keyboard.up("Shift");
+
+ const frameUrlStr = await page.locator("iframe").getAttribute("src");
+ await expect(frameUrlStr).toBeDefined();
+ const url = new URL(frameUrlStr);
+ const hash = new URLSearchParams(url.hash.slice(1));
+ assertCommonCallParameters(url.searchParams, hash, user, room);
+ expect(hash.get("intent")).toEqual("start_call");
+ expect(hash.get("skipLobby")).toEqual("true");
+ });
+
+ test("should be able to join a call in progress", async ({ page, user, bot, room, app }) => {
+ await app.viewRoomById(room.roomId);
+ // Allow bob to create a call
+ await app.client.setPowerLevel(room.roomId, bot.credentials.userId, 50);
+ await expect(page.getByText("Bob joined the room")).toBeVisible();
+ // Fake a start of a call
+ await sendRTCState(bot, room.roomId);
+ const button = page.getByTestId("join-call-button");
+ await expect(button).toBeInViewport({ timeout: 5000 });
+ // And test joining
+ await button.click();
+ const frameUrlStr = await page.locator("iframe").getAttribute("src");
+ console.log(frameUrlStr);
+ await expect(frameUrlStr).toBeDefined();
+ const url = new URL(frameUrlStr);
+ const hash = new URLSearchParams(url.hash.slice(1));
+ assertCommonCallParameters(url.searchParams, hash, user, room);
+
+ expect(hash.get("intent")).toEqual("join_existing");
+ expect(hash.get("skipLobby")).toEqual(null);
+ });
+
+ [true, false].forEach((skipLobbyToggle) => {
+ test(
+ `should be able to join a call via incoming call toast (skipLobby=${skipLobbyToggle})`,
+ { tag: ["@screenshot"] },
+ async ({ page, user, bot, room, app }) => {
+ await app.viewRoomById(room.roomId);
+ // Allow bob to create a call
+ await app.client.setPowerLevel(room.roomId, bot.credentials.userId, 50);
+ await expect(page.getByText("Bob joined the room")).toBeVisible();
+ // Fake a start of a call
+ await sendRTCState(bot, room.roomId, "notification");
+ const toast = page.locator(".mx_Toast_toast");
+ const button = toast.getByRole("button", { name: "Join" });
+ if (skipLobbyToggle) {
+ await toast.getByRole("switch").check();
+ await expect(toast).toMatchScreenshot("incoming-call-group-video-toast-checked.png");
+ } else {
+ await toast.getByRole("switch").uncheck();
+ await expect(toast).toMatchScreenshot("incoming-call-group-video-toast-unchecked.png");
+ }
+
+ // And test joining
+ await button.click();
+ const frameUrlStr = await page.locator("iframe").getAttribute("src");
+ console.log(frameUrlStr);
+ await expect(frameUrlStr).toBeDefined();
+ const url = new URL(frameUrlStr);
+ const hash = new URLSearchParams(url.hash.slice(1));
+ assertCommonCallParameters(url.searchParams, hash, user, room);
+
+ expect(hash.get("intent")).toEqual("join_existing");
+ expect(hash.get("skipLobby")).toEqual(skipLobbyToggle.toString());
+ },
+ );
+ });
+ });
+
+ test.describe("DMs", () => {
+ test.use({
+ room: async ({ page, app, user, bot }, use) => {
+ const roomId = await app.client.createRoom({
+ preset: "trusted_private_chat" as Preset.TrustedPrivateChat,
+ invite: [bot.credentials.userId],
+ });
+ await app.client.setAccountData("m.direct" as EventType.Direct, {
+ [bot.credentials.userId]: [roomId],
+ });
+ await use({ roomId });
+ },
+ });
+
+ test("should be able to start a video call", async ({ page, user, room, app }) => {
+ await app.viewRoomById(room.roomId);
+ await expect(page.getByText("Bob joined the room")).toBeVisible();
+
+ await page.getByRole("button", { name: "Video call" }).click();
+ await page.getByRole("menuitem", { name: "Element Call" }).click();
+ const frameUrlStr = await page.locator("iframe").getAttribute("src");
+
+ await expect(frameUrlStr).toBeDefined();
+ const url = new URL(frameUrlStr);
+ const hash = new URLSearchParams(url.hash.slice(1));
+ assertCommonCallParameters(url.searchParams, hash, user, room);
+ expect(hash.get("intent")).toEqual("start_call_dm");
+ expect(hash.get("skipLobby")).toEqual(null);
+ });
+
+ test("should be able to skip lobby by holding down shift", async ({ page, user, room, app }) => {
+ await app.viewRoomById(room.roomId);
+ await expect(page.getByText("Bob joined the room")).toBeVisible();
+
+ await page.getByRole("button", { name: "Video call" }).click();
+ await page.keyboard.down("Shift");
+ await page.getByRole("menuitem", { name: "Element Call" }).click();
+ await page.keyboard.up("Shift");
+ const frameUrlStr = await page.locator("iframe").getAttribute("src");
+
+ await expect(frameUrlStr).toBeDefined();
+ const url = new URL(frameUrlStr);
+ const hash = new URLSearchParams(url.hash.slice(1));
+ assertCommonCallParameters(url.searchParams, hash, user, room);
+ expect(hash.get("intent")).toEqual("start_call_dm");
+ expect(hash.get("skipLobby")).toEqual("true");
+ });
+
+ test("should be able to join a call in progress", async ({ page, user, bot, room, app }) => {
+ await app.viewRoomById(room.roomId);
+ // Allow bob to create a call
+ await expect(page.getByText("Bob joined the room")).toBeVisible();
+ // Fake a start of a call
+ await sendRTCState(bot, room.roomId);
+ const button = page.getByTestId("join-call-button");
+ await expect(button).toBeInViewport({ timeout: 5000 });
+ // And test joining
+ await button.click();
+ const frameUrlStr = await page.locator("iframe").getAttribute("src");
+ console.log(frameUrlStr);
+ await expect(frameUrlStr).toBeDefined();
+ const url = new URL(frameUrlStr);
+ const hash = new URLSearchParams(url.hash.slice(1));
+ assertCommonCallParameters(url.searchParams, hash, user, room);
+
+ expect(hash.get("intent")).toEqual("join_existing_dm");
+ expect(hash.get("skipLobby")).toEqual(null);
+ });
+
+ [true, false].forEach((skipLobbyToggle) => {
+ test(
+ `should be able to join a call via incoming call toast (skipLobby=${skipLobbyToggle})`,
+ { tag: ["@screenshot"] },
+ async ({ page, user, bot, room, app }) => {
+ await app.viewRoomById(room.roomId);
+ // Allow bob to create a call
+ await expect(page.getByText("Bob joined the room")).toBeVisible();
+ // Fake a start of a call
+ await sendRTCState(bot, room.roomId, "ring");
+ const toast = page.locator(".mx_Toast_toast");
+ const button = toast.getByRole("button", { name: "Join" });
+ if (skipLobbyToggle) {
+ await toast.getByRole("switch").check();
+ await expect(toast).toMatchScreenshot("incoming-call-dm-video-toast-checked.png");
+ } else {
+ await toast.getByRole("switch").uncheck();
+ await expect(toast).toMatchScreenshot("incoming-call-dm-video-toast-unchecked.png");
+ }
+
+ // And test joining
+ await button.click();
+ const frameUrlStr = await page.locator("iframe").getAttribute("src");
+ console.log(frameUrlStr);
+ await expect(frameUrlStr).toBeDefined();
+ const url = new URL(frameUrlStr);
+ const hash = new URLSearchParams(url.hash.slice(1));
+ assertCommonCallParameters(url.searchParams, hash, user, room);
+
+ expect(hash.get("intent")).toEqual("join_existing_dm");
+ expect(hash.get("skipLobby")).toEqual(skipLobbyToggle.toString());
+ },
+ );
+ });
+ });
+
+ test.describe("Video Rooms", () => {
+ test.use({
+ config: {
+ features: {
+ feature_video_rooms: true,
+ feature_element_call_video_rooms: true,
+ },
+ },
+ });
+ test("should be able to create and join a video room", async ({ page, user }) => {
+ await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
+ await page.getByRole("menuitem", { name: "New video room" }).click();
+ await page.getByRole("textbox", { name: "Name" }).fill("Test room");
+ await page.getByRole("button", { name: "Create video room" }).click();
+ await expect(page).toHaveURL(new RegExp(`/#/room/`));
+ const roomId = new URL(page.url()).hash.slice("#/room/".length);
+
+ const frameUrlStr = await page.locator("iframe").getAttribute("src");
+ await expect(frameUrlStr).toBeDefined();
+ // Ensure we set the correct parameters for ECall.
+ const url = new URL(frameUrlStr);
+ const hash = new URLSearchParams(url.hash.slice(1));
+ assertCommonCallParameters(url.searchParams, hash, user, { roomId });
+ expect(hash.get("intent")).toEqual("join_existing");
+ expect(hash.get("skipLobby")).toEqual("false");
+ expect(hash.get("returnToLobby")).toEqual("true");
+ });
+ });
+});
diff --git a/playwright/e2e/voip/pstn.spec.ts b/playwright/e2e/voip/pstn.spec.ts
index 9a35d9b9c3..0275b88067 100644
--- a/playwright/e2e/voip/pstn.spec.ts
+++ b/playwright/e2e/voip/pstn.spec.ts
@@ -24,7 +24,7 @@ test.describe("PSTN", () => {
await toasts.rejectToast("Notifications");
await toasts.assertNoToasts();
- await expect(page.locator(".mx_LeftPanel_filterContainer")).toMatchScreenshot("dialpad-trigger.png");
+ await expect(page.locator(".mx_RoomListSearch")).toMatchScreenshot("dialpad-trigger.png");
await page.getByLabel("Open dial pad").click();
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("dialpad.png");
});
diff --git a/playwright/e2e/widgets/permissions-dialog.spec.ts b/playwright/e2e/widgets/permissions-dialog.spec.ts
new file mode 100644
index 0000000000..bdaec30cd5
--- /dev/null
+++ b/playwright/e2e/widgets/permissions-dialog.spec.ts
@@ -0,0 +1,98 @@
+/*
+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 { test, expect } from "../../element-web-test";
+
+const DEMO_WIDGET_ID = "demo-widget-id";
+const DEMO_WIDGET_NAME = "Demo Widget";
+const DEMO_WIDGET_TYPE = "demo";
+const ROOM_NAME = "Demo";
+
+const DEMO_WIDGET_HTML = `
+
+
+ Demo Widget
+
+
+
+`;
+
+test.describe("Widger permissions dialog", () => {
+ test.use({
+ displayName: "Mike",
+ });
+
+ let demoWidgetUrl: string;
+ test.beforeEach(async ({ webserver }) => {
+ demoWidgetUrl = webserver.start(DEMO_WIDGET_HTML);
+ });
+
+ test(
+ "should be updated if user is re-invited into the room with updated state event",
+ { tag: "@screenshot" },
+ async ({ page, app, user, axe }) => {
+ const roomId = await app.client.createRoom({
+ name: ROOM_NAME,
+ });
+
+ // setup widget via state event
+ await app.client.sendStateEvent(
+ roomId,
+ "im.vector.modular.widgets",
+ {
+ id: DEMO_WIDGET_ID,
+ creatorUserId: "somebody",
+ type: DEMO_WIDGET_TYPE,
+ name: DEMO_WIDGET_NAME,
+ url: demoWidgetUrl,
+ },
+ DEMO_WIDGET_ID,
+ );
+
+ // set initial layout
+ await app.client.sendStateEvent(
+ roomId,
+ "io.element.widgets.layout",
+ {
+ widgets: {
+ [DEMO_WIDGET_ID]: {
+ container: "top",
+ index: 1,
+ width: 100,
+ height: 0,
+ },
+ },
+ },
+ "",
+ );
+
+ // open the room
+ await app.viewRoomByName(ROOM_NAME);
+
+ axe.disableRules("color-contrast"); // XXX: Inheriting colour contrast issues from room view.
+ await expect(axe).toHaveNoViolations();
+ await expect(page.locator(".mx_WidgetCapabilitiesPromptDialog")).toMatchScreenshot(
+ "widget-capabilites-prompt.png",
+ );
+ },
+ );
+});
diff --git a/playwright/pages/ElementAppPage.ts b/playwright/pages/ElementAppPage.ts
index abddc9c644..816583fd49 100644
--- a/playwright/pages/ElementAppPage.ts
+++ b/playwright/pages/ElementAppPage.ts
@@ -51,9 +51,10 @@ export class ElementAppPage {
/**
* Open room creation dialog.
*/
- public async openCreateRoomDialog(): Promise {
- await this.page.getByRole("button", { name: "Add room", exact: true }).click();
- await this.page.getByRole("menuitem", { name: "New room", exact: true }).click();
+
+ public async openCreateRoomDialog(roomKindname: "New room" | "New video room" = "New room"): Promise {
+ await this.page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
+ await this.page.getByRole("menuitem", { name: roomKindname }).click();
return this.page.locator(".mx_CreateRoomDialog");
}
@@ -70,12 +71,23 @@ export class ElementAppPage {
/**
* Opens the given room by name. The room must be visible in the
+ * room list and the room may contain unread messages.
+ *
+ * @param name The exact room name to find and click on/open.
+ */
+ public async viewRoomByName(name: string): Promise {
+ // We get the room list by test-id which is a listbox and matching title=name
+ return this.page.getByTestId("room-list").locator(`[title="${name}"]`).first().click();
+ }
+
+ /**
+ * Opens the given room on the old room list by name. The room must be visible in the
* room list, but the room list may be folded horizontally, and the
* room may contain unread messages.
*
* @param name The exact room name to find and click on/open.
*/
- public async viewRoomByName(name: string): Promise {
+ public async viewRoomByNameOnOldRoomList(name: string): Promise {
// We look for the room inside the room list, which is a tree called Rooms.
//
// There are 3 cases:
diff --git a/playwright/pages/client.ts b/playwright/pages/client.ts
index 8296e9111e..86cb581397 100644
--- a/playwright/pages/client.ts
+++ b/playwright/pages/client.ts
@@ -469,6 +469,27 @@ export class Client {
);
}
+ /**
+ * Set a power level to one or multiple users.
+ * Will apply changes atop of current power level event.
+ * @param roomId - the room to update power levels in
+ * @param userId - the ID of the user or users to update power levels of
+ * @param powerLevel - the numeric power level to update given users to
+ */
+ public async setPowerLevel(
+ roomId: string,
+ userId: string | string[],
+ powerLevel: number,
+ ): Promise {
+ const client = await this.prepareClient();
+ return client.evaluate(
+ async (client, { roomId, userId, powerLevel }) => {
+ return client.setPowerLevel(roomId, userId, powerLevel);
+ },
+ { roomId, userId, powerLevel },
+ );
+ }
+
/**
* Leaves the given room.
* @param roomId ID of the room to leave
diff --git a/playwright/pages/settings.ts b/playwright/pages/settings.ts
index a08ca65f34..0ea8c50348 100644
--- a/playwright/pages/settings.ts
+++ b/playwright/pages/settings.ts
@@ -43,7 +43,7 @@ export class Settings {
* @param {*} value The new value of the setting, may be null.
* @return {Promise} Resolves when the setting has been changed.
*/
- public async setValue(settingName: string, roomId: string, level: SettingLevel, value: any): Promise {
+ public async setValue(settingName: string, roomId: string | null, level: SettingLevel, value: any): Promise {
return this.page.evaluate<
Promise,
{
diff --git a/playwright/shared-component-snapshots/avatar-avatarwithdetails--default-linux.png b/playwright/shared-component-snapshots/avatar-avatarwithdetails--default-linux.png
new file mode 100644
index 0000000000..ae339219e5
Binary files /dev/null and b/playwright/shared-component-snapshots/avatar-avatarwithdetails--default-linux.png differ
diff --git a/playwright/shared-component-snapshots/pillinput-pill--default-linux.png b/playwright/shared-component-snapshots/pillinput-pill--default-linux.png
new file mode 100644
index 0000000000..cd8eb65ba1
Binary files /dev/null and b/playwright/shared-component-snapshots/pillinput-pill--default-linux.png differ
diff --git a/playwright/shared-component-snapshots/pillinput-pill--without-close-button-linux.png b/playwright/shared-component-snapshots/pillinput-pill--without-close-button-linux.png
new file mode 100644
index 0000000000..451de895dc
Binary files /dev/null and b/playwright/shared-component-snapshots/pillinput-pill--without-close-button-linux.png differ
diff --git a/playwright/shared-component-snapshots/pillinput-pillinput--default-linux.png b/playwright/shared-component-snapshots/pillinput-pillinput--default-linux.png
new file mode 100644
index 0000000000..93bf6317a9
Binary files /dev/null and b/playwright/shared-component-snapshots/pillinput-pillinput--default-linux.png differ
diff --git a/playwright/shared-component-snapshots/pillinput-pillinput--no-child-linux.png b/playwright/shared-component-snapshots/pillinput-pillinput--no-child-linux.png
new file mode 100644
index 0000000000..a303e726b5
Binary files /dev/null and b/playwright/shared-component-snapshots/pillinput-pillinput--no-child-linux.png differ
diff --git a/playwright/shared-component-snapshots/richlist-richitem--default-linux.png b/playwright/shared-component-snapshots/richlist-richitem--default-linux.png
new file mode 100644
index 0000000000..9a5ad9eb0f
Binary files /dev/null and b/playwright/shared-component-snapshots/richlist-richitem--default-linux.png differ
diff --git a/playwright/shared-component-snapshots/richlist-richitem--hover-linux.png b/playwright/shared-component-snapshots/richlist-richitem--hover-linux.png
new file mode 100644
index 0000000000..9a5ad9eb0f
Binary files /dev/null and b/playwright/shared-component-snapshots/richlist-richitem--hover-linux.png differ
diff --git a/playwright/shared-component-snapshots/richlist-richitem--selected-linux.png b/playwright/shared-component-snapshots/richlist-richitem--selected-linux.png
new file mode 100644
index 0000000000..f9c92b066f
Binary files /dev/null and b/playwright/shared-component-snapshots/richlist-richitem--selected-linux.png differ
diff --git a/playwright/shared-component-snapshots/richlist-richitem--separator-linux.png b/playwright/shared-component-snapshots/richlist-richitem--separator-linux.png
new file mode 100644
index 0000000000..a405f35902
Binary files /dev/null and b/playwright/shared-component-snapshots/richlist-richitem--separator-linux.png differ
diff --git a/playwright/shared-component-snapshots/richlist-richitem--without-timestamp-linux.png b/playwright/shared-component-snapshots/richlist-richitem--without-timestamp-linux.png
new file mode 100644
index 0000000000..de5ecda7c3
Binary files /dev/null and b/playwright/shared-component-snapshots/richlist-richitem--without-timestamp-linux.png differ
diff --git a/playwright/shared-component-snapshots/richlist-richlist--default-linux.png b/playwright/shared-component-snapshots/richlist-richlist--default-linux.png
new file mode 100644
index 0000000000..7919f356df
Binary files /dev/null and b/playwright/shared-component-snapshots/richlist-richlist--default-linux.png differ
diff --git a/playwright/shared-component-snapshots/richlist-richlist--empty-linux.png b/playwright/shared-component-snapshots/richlist-richlist--empty-linux.png
new file mode 100644
index 0000000000..f655ecab96
Binary files /dev/null and b/playwright/shared-component-snapshots/richlist-richlist--empty-linux.png differ
diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--bubble-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--bubble-layout-linux.png
index 17bc4bfc78..03d2084fdb 100644
Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--bubble-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--bubble-layout-linux.png differ
diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--group-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--group-layout-linux.png
index 30e0de96ce..d1edd45eb8 100644
Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--group-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--group-layout-linux.png differ
diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--irc-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--irc-layout-linux.png
index f2fd0de114..ad25839e87 100644
Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--irc-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--irc-layout-linux.png differ
diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--bubble-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--bubble-layout-linux.png
index ab9b63960d..933c790e3d 100644
Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--bubble-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--bubble-layout-linux.png differ
diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--group-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--group-layout-linux.png
index 7e6137c4eb..bba9a07cf1 100644
Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--group-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--group-layout-linux.png differ
diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--irc-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--irc-layout-linux.png
index 519a46371d..585d8cb5ee 100644
Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--irc-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--irc-layout-linux.png differ
diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--bubble-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--bubble-layout-linux.png
index 0a299b63d1..7cd3f0c871 100644
Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--bubble-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--bubble-layout-linux.png differ
diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--group-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--group-layout-linux.png
index 8307809872..195c907273 100644
Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--group-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--group-layout-linux.png differ
diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--irc-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--irc-layout-linux.png
index f57a1e27a6..fccee422d7 100644
Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--irc-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--irc-layout-linux.png differ
diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--bubble-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--bubble-layout-linux.png
index 8a6dec9273..d54c186e2d 100644
Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--bubble-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--bubble-layout-linux.png differ
diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--group-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--group-layout-linux.png
index b6a2ed0bfc..92ad8df33f 100644
Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--group-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--group-layout-linux.png differ
diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--irc-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--irc-layout-linux.png
index c59ef184c4..c2972f34db 100644
Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--irc-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--irc-layout-linux.png differ
diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-bubble-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-bubble-layout-linux.png
index 9d86393932..4e77b7d094 100644
Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-bubble-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-bubble-layout-linux.png differ
diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-bubble-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-bubble-layout-linux.png
index 470366cb9b..d5f4e7a7ca 100644
Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-bubble-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-bubble-layout-linux.png differ
diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-group-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-group-layout-linux.png
index 2dac381c78..e9fca0d76b 100644
Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-group-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-group-layout-linux.png differ
diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-irc-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-irc-layout-linux.png
index b09c0ceed4..f9fe2e8c94 100644
Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-irc-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-irc-layout-linux.png differ
diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-group-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-group-layout-linux.png
index 16f51d0262..52357bc9e4 100644
Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-group-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-group-layout-linux.png differ
diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-irc-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-irc-layout-linux.png
index 1952c0681d..11e9499298 100644
Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-irc-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-irc-layout-linux.png differ
diff --git a/playwright/snapshots/composer/CIDER.spec.ts/mention-linux.png b/playwright/snapshots/composer/CIDER.spec.ts/mention-linux.png
new file mode 100644
index 0000000000..8098757aba
Binary files /dev/null and b/playwright/snapshots/composer/CIDER.spec.ts/mention-linux.png differ
diff --git a/playwright/snapshots/crypto/toasts.spec.ts/key-storage-out-of-sync-toast-linux.png b/playwright/snapshots/crypto/toasts.spec.ts/key-storage-out-of-sync-toast-linux.png
index 8cf3cb7d69..9579ed87a5 100644
Binary files a/playwright/snapshots/crypto/toasts.spec.ts/key-storage-out-of-sync-toast-linux.png and b/playwright/snapshots/crypto/toasts.spec.ts/key-storage-out-of-sync-toast-linux.png differ
diff --git a/playwright/snapshots/devtools/devtools.spec.ts/devtools-dialog-linux.png b/playwright/snapshots/devtools/devtools.spec.ts/devtools-dialog-linux.png
new file mode 100644
index 0000000000..ad239ddc04
Binary files /dev/null and b/playwright/snapshots/devtools/devtools.spec.ts/devtools-dialog-linux.png differ
diff --git a/playwright/snapshots/devtools/upgraderoom.spec.ts/upgrade-room-linux.png b/playwright/snapshots/devtools/upgraderoom.spec.ts/upgrade-room-linux.png
new file mode 100644
index 0000000000..94f0c9a599
Binary files /dev/null and b/playwright/snapshots/devtools/upgraderoom.spec.ts/upgrade-room-linux.png differ
diff --git a/playwright/snapshots/invite/decline-and-block-invite-dialog.spec.ts/decline-and-block-invite-empty-linux.png b/playwright/snapshots/invite/decline-and-block-invite-dialog.spec.ts/decline-and-block-invite-empty-linux.png
new file mode 100644
index 0000000000..4726c446d3
Binary files /dev/null and b/playwright/snapshots/invite/decline-and-block-invite-dialog.spec.ts/decline-and-block-invite-empty-linux.png differ
diff --git a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-with-user-pill-linux.png b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-with-user-pill-linux.png
index 1a66050e5f..86154ad35d 100644
Binary files a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-with-user-pill-linux.png and b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-with-user-pill-linux.png differ
diff --git a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-without-user-linux.png b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-without-user-linux.png
index f463282be7..e63333c1e9 100644
Binary files a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-without-user-linux.png and b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-without-user-linux.png differ
diff --git a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png
index 043e35b6be..0c0695ad64 100644
Binary files a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png and b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png differ
diff --git a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-without-user-linux.png b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-without-user-linux.png
index 910040e20f..cee338835f 100644
Binary files a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-without-user-linux.png and b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-without-user-linux.png differ
diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/Favourite-empty-room-list-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/Favourite-empty-room-list-linux.png
index 8cf4a3c97b..d30b38b38f 100644
Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/Favourite-empty-room-list-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/Favourite-empty-room-list-linux.png differ
diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/collapsed-primary-filters-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/collapsed-primary-filters-linux.png
new file mode 100644
index 0000000000..1d5a8ae87a
Binary files /dev/null and b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/collapsed-primary-filters-linux.png differ
diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/expanded-primary-filters-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/expanded-primary-filters-linux.png
new file mode 100644
index 0000000000..3bafa521b7
Binary files /dev/null and b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/expanded-primary-filters-linux.png differ
diff --git a/playwright/snapshots/location/location.spec.ts/location-live-share-dialog-linux.png b/playwright/snapshots/location/location.spec.ts/location-live-share-dialog-linux.png
new file mode 100644
index 0000000000..a2dce30b15
Binary files /dev/null and b/playwright/snapshots/location/location.spec.ts/location-live-share-dialog-linux.png differ
diff --git a/playwright/snapshots/messages/messages.spec.ts/basic-message-ltr-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/basic-message-ltr-ltrdisplayname-linux.png
index 9831051768..9a29c3994c 100644
Binary files a/playwright/snapshots/messages/messages.spec.ts/basic-message-ltr-ltrdisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/basic-message-ltr-ltrdisplayname-linux.png differ
diff --git a/playwright/snapshots/messages/messages.spec.ts/basic-message-ltr-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/basic-message-ltr-rtldisplayname-linux.png
index c1f4477d66..3fbe91e49a 100644
Binary files a/playwright/snapshots/messages/messages.spec.ts/basic-message-ltr-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/basic-message-ltr-rtldisplayname-linux.png differ
diff --git a/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-ltrdisplayname-linux.png
index 59b9fc41e8..680f5e2f28 100644
Binary files a/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-ltrdisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-ltrdisplayname-linux.png differ
diff --git a/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-rtldisplayname-linux.png
index 07f007545c..8d1721a0af 100644
Binary files a/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-rtldisplayname-linux.png differ
diff --git a/playwright/snapshots/messages/messages.spec.ts/edited-message-ltr-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/edited-message-ltr-ltrdisplayname-linux.png
index d8b02a028b..d93f7250f3 100644
Binary files a/playwright/snapshots/messages/messages.spec.ts/edited-message-ltr-ltrdisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/edited-message-ltr-ltrdisplayname-linux.png differ
diff --git a/playwright/snapshots/messages/messages.spec.ts/edited-message-ltr-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/edited-message-ltr-rtldisplayname-linux.png
index e694dde7c6..cc50f3bdb0 100644
Binary files a/playwright/snapshots/messages/messages.spec.ts/edited-message-ltr-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/edited-message-ltr-rtldisplayname-linux.png differ
diff --git a/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-ltrdisplayname-linux.png
index 8c2e2d153b..7be9d08d55 100644
Binary files a/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-ltrdisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-ltrdisplayname-linux.png differ
diff --git a/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-rtldisplayname-linux.png
index 4bb04ba279..22ba802f89 100644
Binary files a/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-rtldisplayname-linux.png differ
diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-ltr-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-ltr-ltrdisplayname-linux.png
index 5ab0a7e88f..12294041af 100644
Binary files a/playwright/snapshots/messages/messages.spec.ts/emote-ltr-ltrdisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/emote-ltr-ltrdisplayname-linux.png differ
diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-ltr-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-ltr-rtldisplayname-linux.png
index c711b21a58..22ae3e6c99 100644
Binary files a/playwright/snapshots/messages/messages.spec.ts/emote-ltr-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/emote-ltr-rtldisplayname-linux.png differ
diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-rich-ltr-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-rich-ltr-ltrdisplayname-linux.png
index c3501583f8..ea9840bf73 100644
Binary files a/playwright/snapshots/messages/messages.spec.ts/emote-rich-ltr-ltrdisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/emote-rich-ltr-ltrdisplayname-linux.png differ
diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-rich-ltr-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-rich-ltr-rtldisplayname-linux.png
index b92c4691d3..9d66f951f2 100644
Binary files a/playwright/snapshots/messages/messages.spec.ts/emote-rich-ltr-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/emote-rich-ltr-rtldisplayname-linux.png differ
diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-ltrdisplayname-linux.png
index c727b98745..d40c1e2c7a 100644
Binary files a/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-ltrdisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-ltrdisplayname-linux.png differ
diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-rtldisplayname-linux.png
index bd1baed9aa..d07506957f 100644
Binary files a/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-rtldisplayname-linux.png differ
diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-rtl-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-rtl-ltrdisplayname-linux.png
index b55925218d..88ced478ef 100644
Binary files a/playwright/snapshots/messages/messages.spec.ts/emote-rtl-ltrdisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/emote-rtl-ltrdisplayname-linux.png differ
diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-rtl-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-rtl-rtldisplayname-linux.png
index cb18ce68c7..ad9de193f1 100644
Binary files a/playwright/snapshots/messages/messages.spec.ts/emote-rtl-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/emote-rtl-rtldisplayname-linux.png differ
diff --git a/playwright/snapshots/messages/messages.spec.ts/reply-message-ltr-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/reply-message-ltr-ltrdisplayname-linux.png
index a4de383fba..3619ac320d 100644
Binary files a/playwright/snapshots/messages/messages.spec.ts/reply-message-ltr-ltrdisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/reply-message-ltr-ltrdisplayname-linux.png differ
diff --git a/playwright/snapshots/messages/messages.spec.ts/reply-message-ltr-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/reply-message-ltr-rtldisplayname-linux.png
index e3d3e68c64..decfcb190d 100644
Binary files a/playwright/snapshots/messages/messages.spec.ts/reply-message-ltr-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/reply-message-ltr-rtldisplayname-linux.png differ
diff --git a/playwright/snapshots/messages/messages.spec.ts/reply-message-trl-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/reply-message-trl-ltrdisplayname-linux.png
index d6964445b1..fe644d923d 100644
Binary files a/playwright/snapshots/messages/messages.spec.ts/reply-message-trl-ltrdisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/reply-message-trl-ltrdisplayname-linux.png differ
diff --git a/playwright/snapshots/messages/messages.spec.ts/reply-message-trl-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/reply-message-trl-rtldisplayname-linux.png
index 8830c22d92..e6b787f147 100644
Binary files a/playwright/snapshots/messages/messages.spec.ts/reply-message-trl-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/reply-message-trl-rtldisplayname-linux.png differ
diff --git a/playwright/snapshots/modules/custom-component.spec.ts/custom-component-crash-handle-filter-linux.png b/playwright/snapshots/modules/custom-component.spec.ts/custom-component-crash-handle-filter-linux.png
index b144ca6a5e..f2819d7479 100644
Binary files a/playwright/snapshots/modules/custom-component.spec.ts/custom-component-crash-handle-filter-linux.png and b/playwright/snapshots/modules/custom-component.spec.ts/custom-component-crash-handle-filter-linux.png differ
diff --git a/playwright/snapshots/modules/custom-component.spec.ts/custom-component-crash-handle-renderer-linux.png b/playwright/snapshots/modules/custom-component.spec.ts/custom-component-crash-handle-renderer-linux.png
index b3fa5e0f57..7876849092 100644
Binary files a/playwright/snapshots/modules/custom-component.spec.ts/custom-component-crash-handle-renderer-linux.png and b/playwright/snapshots/modules/custom-component.spec.ts/custom-component-crash-handle-renderer-linux.png differ
diff --git a/playwright/snapshots/modules/custom-component.spec.ts/custom-component-tile-fall-through-linux.png b/playwright/snapshots/modules/custom-component.spec.ts/custom-component-tile-fall-through-linux.png
index 0fe98072a0..2d49d287f5 100644
Binary files a/playwright/snapshots/modules/custom-component.spec.ts/custom-component-tile-fall-through-linux.png and b/playwright/snapshots/modules/custom-component.spec.ts/custom-component-tile-fall-through-linux.png differ
diff --git a/playwright/snapshots/modules/custom-component.spec.ts/custom-component-tile-linux.png b/playwright/snapshots/modules/custom-component.spec.ts/custom-component-tile-linux.png
index 7c5d6b66e6..56d2cea124 100644
Binary files a/playwright/snapshots/modules/custom-component.spec.ts/custom-component-tile-linux.png and b/playwright/snapshots/modules/custom-component.spec.ts/custom-component-tile-linux.png differ
diff --git a/playwright/snapshots/modules/custom-component.spec.ts/custom-component-tile-original-linux.png b/playwright/snapshots/modules/custom-component.spec.ts/custom-component-tile-original-linux.png
index 9a00a3b04b..809199feb5 100644
Binary files a/playwright/snapshots/modules/custom-component.spec.ts/custom-component-tile-original-linux.png and b/playwright/snapshots/modules/custom-component.spec.ts/custom-component-tile-original-linux.png differ
diff --git a/playwright/snapshots/permalinks/permalinks.spec.ts/permalink-rendering-linux.png b/playwright/snapshots/permalinks/permalinks.spec.ts/permalink-rendering-linux.png
index b386eaa564..15eb9e0d1f 100644
Binary files a/playwright/snapshots/permalinks/permalinks.spec.ts/permalink-rendering-linux.png and b/playwright/snapshots/permalinks/permalinks.spec.ts/permalink-rendering-linux.png differ
diff --git a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-Msg1-linux.png b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-Msg1-linux.png
index f39caf654a..fc82f24778 100644
Binary files a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-Msg1-linux.png and b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-Msg1-linux.png differ
diff --git a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-1-Msg1-linux.png b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-1-Msg1-linux.png
index ac13a2152d..e240565e11 100644
Binary files a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-1-Msg1-linux.png and b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-1-Msg1-linux.png differ
diff --git a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-2-Msg1-linux.png b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-2-Msg1-linux.png
index 20917ae16f..a22ae313ae 100644
Binary files a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-2-Msg1-linux.png and b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-2-Msg1-linux.png differ
diff --git a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-2-Msg2-linux.png b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-2-Msg2-linux.png
index aaf8c720eb..4f635ff5fd 100644
Binary files a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-2-Msg2-linux.png and b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-2-Msg2-linux.png differ
diff --git a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg1-linux.png b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg1-linux.png
index 7ef356c589..0c544e58cb 100644
Binary files a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg1-linux.png and b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg1-linux.png differ
diff --git a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg2-linux.png b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg2-linux.png
index a62d8182b1..f9df40b66d 100644
Binary files a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg2-linux.png and b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg2-linux.png differ
diff --git a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg3-linux.png b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg3-linux.png
index fa548aaadc..969df22f36 100644
Binary files a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg3-linux.png and b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg3-linux.png differ
diff --git a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg4-linux.png b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg4-linux.png
index bc2062f98d..8af50bb4e5 100644
Binary files a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg4-linux.png and b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg4-linux.png differ
diff --git a/playwright/snapshots/polls/polls.spec.ts/Polls-Timeline-tile-no-votes-linux.png b/playwright/snapshots/polls/polls.spec.ts/Polls-Timeline-tile-no-votes-linux.png
index 7b7f296be1..db3ae8eccb 100644
Binary files a/playwright/snapshots/polls/polls.spec.ts/Polls-Timeline-tile-no-votes-linux.png and b/playwright/snapshots/polls/polls.spec.ts/Polls-Timeline-tile-no-votes-linux.png differ
diff --git a/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-All-new-pinned-messages-linux.png b/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-All-new-pinned-messages-linux.png
deleted file mode 100644
index d33c711add..0000000000
Binary files a/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-All-new-pinned-messages-linux.png and /dev/null differ
diff --git a/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-Chats-has-a-new-look--linux.png b/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-Chats-has-a-new-look--linux.png
new file mode 100644
index 0000000000..54deb9e4a6
Binary files /dev/null and b/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-Chats-has-a-new-look--linux.png differ
diff --git a/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-We’ve-refreshed-your-sounds-linux.png b/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-We’ve-refreshed-your-sounds-linux.png
new file mode 100644
index 0000000000..0f4c80ac9e
Binary files /dev/null and b/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-We’ve-refreshed-your-sounds-linux.png differ
diff --git a/playwright/snapshots/room/create-room.spec.ts/create-room-linux.png b/playwright/snapshots/room/create-room.spec.ts/create-room-linux.png
new file mode 100644
index 0000000000..a16373c8dc
Binary files /dev/null and b/playwright/snapshots/room/create-room.spec.ts/create-room-linux.png differ
diff --git a/playwright/snapshots/room/create-room.spec.ts/create-room-no-public-linux.png b/playwright/snapshots/room/create-room.spec.ts/create-room-no-public-linux.png
new file mode 100644
index 0000000000..84494f46bf
Binary files /dev/null and b/playwright/snapshots/room/create-room.spec.ts/create-room-no-public-linux.png differ
diff --git a/playwright/snapshots/room/create-room.spec.ts/create-video-room-linux.png b/playwright/snapshots/room/create-room.spec.ts/create-video-room-linux.png
new file mode 100644
index 0000000000..628ae1159c
Binary files /dev/null and b/playwright/snapshots/room/create-room.spec.ts/create-video-room-linux.png differ
diff --git a/playwright/snapshots/room/invites.spec.ts/Invites-room-view-linux.png b/playwright/snapshots/room/invites.spec.ts/Invites-room-view-linux.png
index 86d0e37b93..60487a7768 100644
Binary files a/playwright/snapshots/room/invites.spec.ts/Invites-room-view-linux.png and b/playwright/snapshots/room/invites.spec.ts/Invites-room-view-linux.png differ
diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png
index 65258303c9..b377cf296f 100644
Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png differ
diff --git a/playwright/snapshots/settings/encryption-user-tab/advanced.spec.ts/encryption-details-linux.png b/playwright/snapshots/settings/encryption-user-tab/advanced.spec.ts/encryption-details-linux.png
index e18821b774..438691fd44 100644
Binary files a/playwright/snapshots/settings/encryption-user-tab/advanced.spec.ts/encryption-details-linux.png and b/playwright/snapshots/settings/encryption-user-tab/advanced.spec.ts/encryption-details-linux.png differ
diff --git a/playwright/snapshots/settings/notifications/notifications-settings-2-tab.spec.ts/standard-notifications-2-settings-linux.png b/playwright/snapshots/settings/notifications/notifications-settings-2-tab.spec.ts/standard-notifications-2-settings-linux.png
new file mode 100644
index 0000000000..b3936c2a7b
Binary files /dev/null and b/playwright/snapshots/settings/notifications/notifications-settings-2-tab.spec.ts/standard-notifications-2-settings-linux.png differ
diff --git a/playwright/snapshots/settings/notifications/notifications-settings-tab.spec.ts/standard-notification-settings-linux.png b/playwright/snapshots/settings/notifications/notifications-settings-tab.spec.ts/standard-notification-settings-linux.png
new file mode 100644
index 0000000000..6182c73b75
Binary files /dev/null and b/playwright/snapshots/settings/notifications/notifications-settings-tab.spec.ts/standard-notification-settings-linux.png differ
diff --git a/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png b/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png
index dca96a1f4f..01ebc9d415 100644
Binary files a/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png and b/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png differ
diff --git a/playwright/snapshots/settings/quick-settings-menu.spec.ts/quick-settings-linux.png b/playwright/snapshots/settings/quick-settings-menu.spec.ts/quick-settings-linux.png
index c6605b5b64..2992e35d60 100644
Binary files a/playwright/snapshots/settings/quick-settings-menu.spec.ts/quick-settings-linux.png and b/playwright/snapshots/settings/quick-settings-menu.spec.ts/quick-settings-linux.png differ
diff --git a/playwright/snapshots/settings/room-settings/room-security-tab.spec.ts/room-security-settings-linux.png b/playwright/snapshots/settings/room-settings/room-security-tab.spec.ts/room-security-settings-linux.png
new file mode 100644
index 0000000000..7d03f4b6ef
Binary files /dev/null and b/playwright/snapshots/settings/room-settings/room-security-tab.spec.ts/room-security-settings-linux.png differ
diff --git a/playwright/snapshots/settings/room-settings/room-security-tab.spec.ts/room-security-settings-world-readable-linux.png b/playwright/snapshots/settings/room-settings/room-security-tab.spec.ts/room-security-settings-world-readable-linux.png
new file mode 100644
index 0000000000..ff18bcf702
Binary files /dev/null and b/playwright/snapshots/settings/room-settings/room-security-tab.spec.ts/room-security-settings-world-readable-linux.png differ
diff --git a/playwright/snapshots/settings/room-settings/room-video-tab.spec.ts/room-video-settings-linux.png b/playwright/snapshots/settings/room-settings/room-video-tab.spec.ts/room-video-settings-linux.png
new file mode 100644
index 0000000000..2e46c528a3
Binary files /dev/null and b/playwright/snapshots/settings/room-settings/room-video-tab.spec.ts/room-video-settings-linux.png differ
diff --git a/playwright/snapshots/settings/security-user-settings-tab.spec.ts/security-settings-tab-linux.png b/playwright/snapshots/settings/security-user-settings-tab.spec.ts/security-settings-tab-linux.png
new file mode 100644
index 0000000000..080bdd9ce1
Binary files /dev/null and b/playwright/snapshots/settings/security-user-settings-tab.spec.ts/security-settings-tab-linux.png differ
diff --git a/playwright/snapshots/spaces/spaces.spec.ts/invite-teammates-dialog-linux.png b/playwright/snapshots/spaces/spaces.spec.ts/invite-teammates-dialog-linux.png
index b69496e5fe..93d8044a3d 100644
Binary files a/playwright/snapshots/spaces/spaces.spec.ts/invite-teammates-dialog-linux.png and b/playwright/snapshots/spaces/spaces.spec.ts/invite-teammates-dialog-linux.png differ
diff --git a/playwright/snapshots/spaces/spaces.spec.ts/space-panel-collapsed-linux.png b/playwright/snapshots/spaces/spaces.spec.ts/space-panel-collapsed-linux.png
index 312ca2580c..4ca008798f 100644
Binary files a/playwright/snapshots/spaces/spaces.spec.ts/space-panel-collapsed-linux.png and b/playwright/snapshots/spaces/spaces.spec.ts/space-panel-collapsed-linux.png differ
diff --git a/playwright/snapshots/spaces/spaces.spec.ts/space-panel-expanded-linux.png b/playwright/snapshots/spaces/spaces.spec.ts/space-panel-expanded-linux.png
index f0606ce47a..59d3f87bb2 100644
Binary files a/playwright/snapshots/spaces/spaces.spec.ts/space-panel-expanded-linux.png and b/playwright/snapshots/spaces/spaces.spec.ts/space-panel-expanded-linux.png differ
diff --git a/playwright/snapshots/spaces/spaces.spec.ts/space-room-view-linux.png b/playwright/snapshots/spaces/spaces.spec.ts/space-room-view-linux.png
index 8e627ec9f2..f313bde9c4 100644
Binary files a/playwright/snapshots/spaces/spaces.spec.ts/space-room-view-linux.png and b/playwright/snapshots/spaces/spaces.spec.ts/space-room-view-linux.png differ
diff --git a/playwright/snapshots/spaces/spaces.spec.ts/space-visibility-settings-linux.png b/playwright/snapshots/spaces/spaces.spec.ts/space-visibility-settings-linux.png
new file mode 100644
index 0000000000..80f051c2d5
Binary files /dev/null and b/playwright/snapshots/spaces/spaces.spec.ts/space-visibility-settings-linux.png differ
diff --git a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-button-expanded-linux.png b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-button-expanded-linux.png
index 2ad6315d9e..30c7b662c5 100644
Binary files a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-button-expanded-linux.png and b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-button-expanded-linux.png differ
diff --git a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-expanded-linux.png b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-expanded-linux.png
index 2ad6315d9e..30c7b662c5 100644
Binary files a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-expanded-linux.png and b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-expanded-linux.png differ
diff --git a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-linux.png b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-linux.png
index 591d22c3c4..5884f6f56f 100644
Binary files a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-linux.png and b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-linux.png differ
diff --git a/playwright/snapshots/timeline/media-preview-settings.spec.ts/invite-no-avatar-linux.png b/playwright/snapshots/timeline/media-preview-settings.spec.ts/invite-no-avatar-linux.png
index 377282a2c8..6840739135 100644
Binary files a/playwright/snapshots/timeline/media-preview-settings.spec.ts/invite-no-avatar-linux.png and b/playwright/snapshots/timeline/media-preview-settings.spec.ts/invite-no-avatar-linux.png differ
diff --git a/playwright/snapshots/timeline/media-preview-settings.spec.ts/invite-room-tree-no-avatar-linux.png b/playwright/snapshots/timeline/media-preview-settings.spec.ts/invite-room-tree-no-avatar-linux.png
index 29d129f73f..f3478d05aa 100644
Binary files a/playwright/snapshots/timeline/media-preview-settings.spec.ts/invite-room-tree-no-avatar-linux.png and b/playwright/snapshots/timeline/media-preview-settings.spec.ts/invite-room-tree-no-avatar-linux.png differ
diff --git a/playwright/snapshots/timeline/media-preview-settings.spec.ts/invite-room-tree-with-avatar-linux.png b/playwright/snapshots/timeline/media-preview-settings.spec.ts/invite-room-tree-with-avatar-linux.png
index 452f08d3e2..7555a90440 100644
Binary files a/playwright/snapshots/timeline/media-preview-settings.spec.ts/invite-room-tree-with-avatar-linux.png and b/playwright/snapshots/timeline/media-preview-settings.spec.ts/invite-room-tree-with-avatar-linux.png differ
diff --git a/playwright/snapshots/timeline/media-preview-settings.spec.ts/invite-with-avatar-linux.png b/playwright/snapshots/timeline/media-preview-settings.spec.ts/invite-with-avatar-linux.png
index 93459507d0..7a7d2025c2 100644
Binary files a/playwright/snapshots/timeline/media-preview-settings.spec.ts/invite-with-avatar-linux.png and b/playwright/snapshots/timeline/media-preview-settings.spec.ts/invite-with-avatar-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/code-block-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/code-block-linux.png
index 2f62d0dec6..01cca9c0d3 100644
Binary files a/playwright/snapshots/timeline/timeline.spec.ts/code-block-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/code-block-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-and-messages-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-and-messages-irc-layout-linux.png
index 45c1c5f4d9..2ab0a03e8e 100644
Binary files a/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-and-messages-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-and-messages-irc-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-bubble-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-bubble-layout-linux.png
index 945e5d074b..7540e34ebd 100644
Binary files a/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-bubble-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-bubble-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/configured-room-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/configured-room-irc-layout-linux.png
index 41c0c6010c..601985cafd 100644
Binary files a/playwright/snapshots/timeline/timeline.spec.ts/configured-room-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/configured-room-irc-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/edited-code-block-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/edited-code-block-linux.png
index 46f5cf1a7a..15eaa51233 100644
Binary files a/playwright/snapshots/timeline/timeline.spec.ts/edited-code-block-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/edited-code-block-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-line-inline-start-margin-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-line-inline-start-margin-irc-layout-linux.png
index cd2b806409..56eb5ff115 100644
Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-line-inline-start-margin-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-line-inline-start-margin-irc-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-bubble-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-bubble-layout-linux.png
index 167f9b6855..b3df4da31f 100644
Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-bubble-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-bubble-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-compact-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-compact-modern-layout-linux.png
index 0682bf760f..9a3d74e5a8 100644
Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-compact-modern-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-compact-modern-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-layout-linux.png
index b9899fb177..829a6bb47d 100644
Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-modern-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-modern-linux.png
index 67a207c687..3f274802f4 100644
Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-modern-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-modern-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-bubble-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-bubble-layout-linux.png
index 7cb6c886e3..67e6c66903 100644
Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-bubble-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-bubble-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-compact-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-compact-modern-layout-linux.png
index 548d2b0f92..795e68ec5b 100644
Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-compact-modern-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-compact-modern-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-irc-layout-linux.png
index 360a316068..b57939e6b3 100644
Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-irc-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-modern-layout-linux.png
index fd2a51857c..1bd5ee24d8 100644
Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-modern-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-modern-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-and-messages-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-and-messages-irc-layout-linux.png
index d5a1b0cbcc..335173ab0f 100644
Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-and-messages-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-and-messages-irc-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-bubble-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-bubble-layout-linux.png
index 5e4e7dadf0..45ba93f347 100644
Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-bubble-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-bubble-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-emote-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-emote-irc-layout-linux.png
index c0b5f579f4..bc99948639 100644
Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-emote-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-emote-irc-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-irc-layout-linux.png
index cd2b806409..728ec48a90 100644
Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-irc-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-modern-layout-linux.png
index bf8a5ee3fb..23c87f9660 100644
Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-modern-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-modern-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-redaction-placeholder-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-redaction-placeholder-linux.png
index d41a9f6935..41d3fcca28 100644
Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-redaction-placeholder-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-redaction-placeholder-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-padding-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-padding-modern-layout-linux.png
index cdeb0a6c0b..0360a11893 100644
Binary files a/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-padding-modern-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-padding-modern-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-zero-padding-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-zero-padding-irc-layout-linux.png
index 32a680f065..c2901d9f2e 100644
Binary files a/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-zero-padding-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-zero-padding-irc-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/highlighted-search-results-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/highlighted-search-results-linux.png
index a99cfb68cf..d8e061597f 100644
Binary files a/playwright/snapshots/timeline/timeline.spec.ts/highlighted-search-results-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/highlighted-search-results-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/hovered-hidden-event-line-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/hovered-hidden-event-line-irc-layout-linux.png
index 5fb0c283b5..2eb3f8725d 100644
Binary files a/playwright/snapshots/timeline/timeline.spec.ts/hovered-hidden-event-line-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/hovered-hidden-event-line-irc-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/image-in-timeline-default-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/image-in-timeline-default-layout-linux.png
index b1236c9ea0..eccba9a912 100644
Binary files a/playwright/snapshots/timeline/timeline.spec.ts/image-in-timeline-default-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/image-in-timeline-default-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-bubble-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-bubble-layout-linux.png
index 5d44a1f655..c63913bbc9 100644
Binary files a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-bubble-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-bubble-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-irc-layout-linux.png
index baa75ffaba..5a38e0593f 100644
Binary files a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-irc-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-modern-layout-linux.png
index 4b54392a21..479e80d6e6 100644
Binary files a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-modern-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-modern-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/search-aux-panel-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/search-aux-panel-linux.png
index d46e898b9c..6009b554ca 100644
Binary files a/playwright/snapshots/timeline/timeline.spec.ts/search-aux-panel-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/search-aux-panel-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/search-results-with-TextualEvent-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/search-results-with-TextualEvent-linux.png
index 5aa6bcea19..e742edfed9 100644
Binary files a/playwright/snapshots/timeline/timeline.spec.ts/search-results-with-TextualEvent-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/search-results-with-TextualEvent-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/spoiler-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/spoiler-linux.png
index 529d254dc4..e63e52dc8e 100644
Binary files a/playwright/snapshots/timeline/timeline.spec.ts/spoiler-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/spoiler-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/spoiler-uncovered-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/spoiler-uncovered-linux.png
index 41d5bc8bee..842476a93b 100644
Binary files a/playwright/snapshots/timeline/timeline.spec.ts/spoiler-uncovered-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/spoiler-uncovered-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/url-preview-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/url-preview-linux.png
index 56220d88d1..5bbaf080d1 100644
Binary files a/playwright/snapshots/timeline/timeline.spec.ts/url-preview-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/url-preview-linux.png differ
diff --git a/playwright/snapshots/voip/element-call.spec.ts/incoming-call-dm-video-toast-checked-linux.png b/playwright/snapshots/voip/element-call.spec.ts/incoming-call-dm-video-toast-checked-linux.png
new file mode 100644
index 0000000000..f309c37f3a
Binary files /dev/null and b/playwright/snapshots/voip/element-call.spec.ts/incoming-call-dm-video-toast-checked-linux.png differ
diff --git a/playwright/snapshots/voip/element-call.spec.ts/incoming-call-dm-video-toast-unchecked-linux.png b/playwright/snapshots/voip/element-call.spec.ts/incoming-call-dm-video-toast-unchecked-linux.png
new file mode 100644
index 0000000000..6d75547fcd
Binary files /dev/null and b/playwright/snapshots/voip/element-call.spec.ts/incoming-call-dm-video-toast-unchecked-linux.png differ
diff --git a/playwright/snapshots/voip/element-call.spec.ts/incoming-call-group-video-toast-checked-linux.png b/playwright/snapshots/voip/element-call.spec.ts/incoming-call-group-video-toast-checked-linux.png
new file mode 100644
index 0000000000..aa181714ae
Binary files /dev/null and b/playwright/snapshots/voip/element-call.spec.ts/incoming-call-group-video-toast-checked-linux.png differ
diff --git a/playwright/snapshots/voip/element-call.spec.ts/incoming-call-group-video-toast-unchecked-linux.png b/playwright/snapshots/voip/element-call.spec.ts/incoming-call-group-video-toast-unchecked-linux.png
new file mode 100644
index 0000000000..9893a76cb7
Binary files /dev/null and b/playwright/snapshots/voip/element-call.spec.ts/incoming-call-group-video-toast-unchecked-linux.png differ
diff --git a/playwright/snapshots/voip/pstn.spec.ts/dialpad-trigger-linux.png b/playwright/snapshots/voip/pstn.spec.ts/dialpad-trigger-linux.png
index 17bea8979d..d1c0cad107 100644
Binary files a/playwright/snapshots/voip/pstn.spec.ts/dialpad-trigger-linux.png and b/playwright/snapshots/voip/pstn.spec.ts/dialpad-trigger-linux.png differ
diff --git a/playwright/snapshots/widgets/layout.spec.ts/apps-drawer-linux.png b/playwright/snapshots/widgets/layout.spec.ts/apps-drawer-linux.png
index 6331d373b3..50c0e1ed2d 100644
Binary files a/playwright/snapshots/widgets/layout.spec.ts/apps-drawer-linux.png and b/playwright/snapshots/widgets/layout.spec.ts/apps-drawer-linux.png differ
diff --git a/playwright/snapshots/widgets/permissions-dialog.spec.ts/widget-capabilites-prompt-linux.png b/playwright/snapshots/widgets/permissions-dialog.spec.ts/widget-capabilites-prompt-linux.png
new file mode 100644
index 0000000000..ec021ebd75
Binary files /dev/null and b/playwright/snapshots/widgets/permissions-dialog.spec.ts/widget-capabilites-prompt-linux.png differ
diff --git a/playwright/testcontainers/mas.ts b/playwright/testcontainers/mas.ts
index 1063b91f7d..0b28b261ea 100644
--- a/playwright/testcontainers/mas.ts
+++ b/playwright/testcontainers/mas.ts
@@ -10,7 +10,7 @@ import {
type StartedPostgreSqlContainer,
} from "@element-hq/element-web-playwright-common/lib/testcontainers";
-const TAG = "main@sha256:e1004d0213a985064766c99df344b0ac799869aff503be5aba1a720478258873";
+const TAG = "main@sha256:b72c84c99eef6dd2da363038f017d409556a136a92b40f700fd4ba66692c3652";
/**
* MatrixAuthenticationServiceContainer which freezes the docker digest to
diff --git a/playwright/testcontainers/synapse.ts b/playwright/testcontainers/synapse.ts
index ed3d5a273d..0cdd86fbc7 100644
--- a/playwright/testcontainers/synapse.ts
+++ b/playwright/testcontainers/synapse.ts
@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
import { SynapseContainer as BaseSynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers";
-const TAG = "develop@sha256:78b32934c4a7a616ada0a0af6e6ba3a102f97ff747fc0d2d1e3bd930e16e529e";
+const TAG = "develop@sha256:902e510b38351b61a354b447e04708c139b9982e4633df674bfa20e57f3a459a";
/**
* SynapseContainer which freezes the docker digest to stabilise tests,
diff --git a/res/css/_common.pcss b/res/css/_common.pcss
index 3eed8c93c6..908da2bdda 100644
--- a/res/css/_common.pcss
+++ b/res/css/_common.pcss
@@ -602,6 +602,8 @@ legend {
.mx_AccessibleButton,
.mx_IdentityServerPicker button,
.mx_AccessSecretStorageDialog button,
+ .mx_InviteDialog_section button,
+ .mx_InviteDialog_editor button,
[class|="maplibregl"]
),
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton, .mx_AccessibleButton),
@@ -643,7 +645,9 @@ legend {
.mx_ThemeChoicePanel_CustomTheme button,
.mx_UnpinAllDialog button,
.mx_ShareDialog button,
- .mx_EncryptionUserSettingsTab button
+ .mx_EncryptionUserSettingsTab button,
+ .mx_InviteDialog_section button,
+ .mx_InviteDialog_editor button
):focus,
.mx_Dialog input[type="submit"]:focus,
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton, .mx_AccessibleButton):focus,
diff --git a/res/css/structures/auth/_SetupEncryptionBody.pcss b/res/css/structures/auth/_SetupEncryptionBody.pcss
index ac6384313c..204e1f5c59 100644
--- a/res/css/structures/auth/_SetupEncryptionBody.pcss
+++ b/res/css/structures/auth/_SetupEncryptionBody.pcss
@@ -6,13 +6,6 @@ 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.
*/
-.mx_SetupEncryptionBody_reset {
- color: $light-fg-color;
- margin-top: $font-14px;
-
- .mx_SetupEncryptionBody_reset_link {
- &.mx_AccessibleButton_kind_link_inline {
- color: $alert;
- }
- }
+.mx_SetupEncryptionBody {
+ width: 600px;
}
diff --git a/res/css/views/auth/_AuthPage.pcss b/res/css/views/auth/_AuthPage.pcss
index 3ae17122be..492ee3d0d6 100644
--- a/res/css/views/auth/_AuthPage.pcss
+++ b/res/css/views/auth/_AuthPage.pcss
@@ -19,7 +19,6 @@ Please see LICENSE files in the repository root for full details.
display: flex;
margin: 100px auto auto;
border-radius: 4px;
- box-shadow: 0 2px 4px 0 rgb(0, 0, 0, 0.33);
background-color: $authpage-modal-bg-color;
@media only screen and (max-height: 768px) {
@@ -29,4 +28,9 @@ Please see LICENSE files in the repository root for full details.
@media only screen and (max-width: 480px) {
margin-top: 0;
}
+
+ /* Apply a blurred shadow around the modal */
+ &.mx_AuthPage_modal_withBlur {
+ box-shadow: 0 2px 4px 0 rgb(0, 0, 0, 0.33);
+ }
}
diff --git a/res/css/views/auth/_CompleteSecurityBody.pcss b/res/css/views/auth/_CompleteSecurityBody.pcss
index f070129005..3aa9cbf518 100644
--- a/res/css/views/auth/_CompleteSecurityBody.pcss
+++ b/res/css/views/auth/_CompleteSecurityBody.pcss
@@ -8,11 +8,10 @@ Please see LICENSE files in the repository root for full details.
*/
.mx_CompleteSecurityBody {
- width: 600px;
color: $authpage-primary-color;
background-color: $background;
border-radius: 4px;
- padding: 20px;
+ padding: 20px 20px 60px 20px;
box-sizing: border-box;
h2 {
diff --git a/res/css/views/dialogs/_DevtoolsDialog.pcss b/res/css/views/dialogs/_DevtoolsDialog.pcss
index 3a706345c9..557345ff0e 100644
--- a/res/css/views/dialogs/_DevtoolsDialog.pcss
+++ b/res/css/views/dialogs/_DevtoolsDialog.pcss
@@ -24,6 +24,12 @@ Please see LICENSE files in the repository root for full details.
}
}
+.mx_DevTools_toolHeading {
+ color: var(--cpd-color-text-secondary);
+ font-weight: var(--cpd-font-weight-semibold);
+ font-size: var(--cpd-font-size-heading-sm);
+}
+
.mx_DevTools_content {
overflow-y: auto;
}
diff --git a/res/css/views/dialogs/_InviteDialog.pcss b/res/css/views/dialogs/_InviteDialog.pcss
index 0f952049cf..fc13023f66 100644
--- a/res/css/views/dialogs/_InviteDialog.pcss
+++ b/res/css/views/dialogs/_InviteDialog.pcss
@@ -24,37 +24,7 @@ Please see LICENSE files in the repository root for full details.
.mx_InviteDialog_editor {
flex: 1;
- width: 100%; /* Needed to make the Field inside grow */
- background-color: $header-panel-bg-color;
- border-radius: 4px;
- min-height: 25px;
- padding-inline-start: $spacing-8;
- overflow-x: hidden;
- overflow-y: auto;
- display: flex;
- flex-wrap: wrap;
-
- .mx_InviteDialog_userTile {
- margin: 6px 6px 0 0;
- display: inline-block;
- min-width: max-content; /* prevent manipulation by flexbox */
- }
-
- /* overrides bunch of our default text input styles */
- > input[type="text"] {
- margin: 6px 0 !important;
- height: 24px;
- font: var(--cpd-font-body-md-regular);
- line-height: $font-24px;
- padding-inline-start: $spacing-12;
- border: 0 !important;
- outline: 0 !important;
- resize: none;
- box-sizing: border-box;
- min-width: 40%;
- flex: 1 !important;
- color: $primary-content !important;
- }
+ margin-left: var(--cpd-space-0-5x);
}
.mx_InviteDialog_goButton {
@@ -68,21 +38,6 @@ Please see LICENSE files in the repository root for full details.
.mx_InviteDialog_section {
padding-bottom: $spacing-4;
- h3 {
- font-size: $font-12px;
- color: $muted-fg-color;
- font-weight: bold;
- text-transform: uppercase;
- }
-
- > p {
- margin: 0;
- }
-
- > span {
- color: $primary-content;
- }
-
.mx_InviteDialog_section_showMore {
margin: 7px 18px;
display: block;
@@ -127,51 +82,6 @@ Please see LICENSE files in the repository root for full details.
}
}
-/* Many of these styles are stolen from mx_UserPill, but adjusted for the invite dialog. */
-.mx_InviteDialog_userTile {
- margin-inline-end: $spacing-8;
-
- .mx_InviteDialog_userTile_pill {
- background-color: var(--cpd-color-bg-canvas-default);
- border: 1px solid var(--cpd-color-gray-400);
- border-radius: 99px;
- display: inline-block;
- height: 24px;
- line-height: $font-24px;
- padding-inline: $spacing-8;
- vertical-align: middle;
- color: var(--cpd-color-gray-1100);
-
- .mx_SearchResultAvatar {
- border-radius: 20px;
- position: relative;
- left: -5px;
- top: 2px;
- }
-
- img.mx_SearchResultAvatar {
- vertical-align: top;
- }
-
- .mx_InviteDialog_userTile_name {
- vertical-align: top;
- }
-
- .mx_SearchResultAvatar_threepidAvatar {
- background-color: #ffffff; /* this is fine without a var because it's for both themes */
- }
- }
-
- .mx_InviteDialog_userTile_remove {
- display: inline-block;
- vertical-align: middle;
-
- svg {
- vertical-align: middle;
- }
- }
-}
-
.mx_InviteDialog_other {
/* Prevent the dialog from jumping around randomly when elements change. */
display: flex;
@@ -194,10 +104,13 @@ Please see LICENSE files in the repository root for full details.
.mx_InviteDialog_userSections {
flex-grow: 1;
padding-inline-end: 0;
+ display: flex;
+ flex-direction: column;
+ margin-top: var(--cpd-space-3x);
+ gap: var(--cpd-space-3x);
.mx_InviteDialog_section {
padding-bottom: 0;
- margin-top: $spacing-12;
}
}
}
@@ -249,7 +162,6 @@ Please see LICENSE files in the repository root for full details.
}
.mx_InviteDialog_userSections {
- margin-top: $spacing-4;
overflow-y: auto;
padding: 0 45px $spacing-4 0;
}
@@ -325,48 +237,6 @@ Please see LICENSE files in the repository root for full details.
gap: $spacing-8 $spacing-12;
align-items: center;
- &.mx_InviteDialog_tile--room {
- /* mx_InviteDialog_tile_avatarStack, mx_InviteDialog_tile_nameStack, time */
- grid-template-columns: min-content auto auto;
- padding: $spacing-4 $spacing-8;
-
- &:hover {
- background-color: $header-panel-bg-color;
- border-radius: 4px;
- }
-
- .mx_InviteDialog_tile--room_selected {
- border-radius: 36px;
- background-color: var(--cpd-color-bg-success-subtle);
-
- &::before {
- content: "";
- width: 24px;
- height: 24px;
- grid-column: 1;
- grid-row: 1;
- mask-image: url("@vector-im/compound-design-tokens/icons/check.svg");
- mask-size: 100%;
- mask-repeat: no-repeat;
- position: absolute;
- top: 6px; /* 50% */
- left: 6px; /* 50% */
- background-color: $primary-content;
- }
- }
-
- .mx_InviteDialog_tile--room_time {
- margin-inline-start: auto;
- width: max-content;
- font-size: $font-12px;
- color: $muted-fg-color;
- }
-
- .mx_InviteDialog_tile--room_highlight {
- font-weight: 900;
- }
- }
-
&.mx_InviteDialog_tile--inviterError {
grid-template-columns: max-content auto; /* max-content = avatar width */
margin-bottom: $spacing-24;
@@ -388,15 +258,11 @@ Please see LICENSE files in the repository root for full details.
vertical-align: middle;
}
- .mx_InviteDialog_tile_avatarStack,
- .mx_InviteDialog_tile--room_selected {
+ .mx_InviteDialog_tile_avatarStack {
width: 36px;
height: 36px;
display: inline-block;
position: relative;
- }
-
- .mx_InviteDialog_tile_avatarStack {
grid-row-start: 1;
grid-column-start: 1;
diff --git a/res/css/views/elements/_Pill.pcss b/res/css/views/elements/_Pill.pcss
index d692f812a4..4b3fd3bb68 100644
--- a/res/css/views/elements/_Pill.pcss
+++ b/res/css/views/elements/_Pill.pcss
@@ -11,8 +11,7 @@ Please see LICENSE files in the repository root for full details.
line-height: $font-17px;
border-radius: $font-16px;
vertical-align: text-top;
- display: inline-flex;
- align-items: center;
+ display: inline-block;
box-sizing: border-box;
max-width: 100%;
overflow: hidden;
@@ -57,6 +56,8 @@ Please see LICENSE files in the repository root for full details.
margin-inline-start: -0.3em; /* Otherwise the gap is too large */
margin-inline-end: 0.2em;
min-width: $font-16px; /* ensure the avatar is not compressed */
+ user-select: text;
+ vertical-align: -2.5px;
}
.mx_Pill_text {
diff --git a/res/css/views/messages/_PinnedMessageBadge.pcss b/res/css/views/messages/_PinnedMessageBadge.pcss
index 69b592d789..722945a49c 100644
--- a/res/css/views/messages/_PinnedMessageBadge.pcss
+++ b/res/css/views/messages/_PinnedMessageBadge.pcss
@@ -13,7 +13,7 @@
padding: var(--cpd-space-1x) var(--cpd-space-3x) var(--cpd-space-1x) var(--cpd-space-1x);
font: var(--cpd-font-body-xs-medium);
- background-color: var(--cpd-color-alpha-gray-200);
+ background-color: var(--cpd-color-bg-subtle-secondary);
color: var(--cpd-color-text-secondary);
border-radius: 99px;
diff --git a/res/css/views/rooms/RoomListPanel/_RoomListHeaderView.pcss b/res/css/views/rooms/RoomListPanel/_RoomListHeaderView.pcss
index 9af934c308..5427e1f133 100644
--- a/res/css/views/rooms/RoomListPanel/_RoomListHeaderView.pcss
+++ b/res/css/views/rooms/RoomListPanel/_RoomListHeaderView.pcss
@@ -32,4 +32,8 @@
transform: rotate(180deg);
}
}
+
+ .mx_RoomListHeaderView_ReleaseAnnouncementAnchor {
+ display: inline-flex;
+ }
}
diff --git a/res/css/views/rooms/RoomListPanel/_RoomListPrimaryFilters.pcss b/res/css/views/rooms/RoomListPanel/_RoomListPrimaryFilters.pcss
index f8fc31ae12..378f2e75da 100644
--- a/res/css/views/rooms/RoomListPanel/_RoomListPrimaryFilters.pcss
+++ b/res/css/views/rooms/RoomListPanel/_RoomListPrimaryFilters.pcss
@@ -12,10 +12,7 @@
display: none;
}
- ul {
- margin: unset;
- padding: unset;
- list-style-type: none;
+ .mx_RoomListPrimaryFilters_list {
/**
* The InteractionObserver needs the height to be set to work properly.
*/
diff --git a/res/css/views/rooms/_AppsDrawer.pcss b/res/css/views/rooms/_AppsDrawer.pcss
index e8baa1257b..1ee13c4b8d 100644
--- a/res/css/views/rooms/_AppsDrawer.pcss
+++ b/res/css/views/rooms/_AppsDrawer.pcss
@@ -208,7 +208,7 @@ Please see LICENSE files in the repository root for full details.
margin-right: 12px;
}
- h3 {
+ h1 {
font-size: inherit;
margin: 0;
}
diff --git a/res/css/views/rooms/_BasicMessageComposer.pcss b/res/css/views/rooms/_BasicMessageComposer.pcss
index 5729c22075..b017b03da3 100644
--- a/res/css/views/rooms/_BasicMessageComposer.pcss
+++ b/res/css/views/rooms/_BasicMessageComposer.pcss
@@ -10,6 +10,7 @@ Please see LICENSE files in the repository root for full details.
/* These are set in Javascript */
--avatar-letter: "";
--avatar-background: unset;
+ --avatar-color: unset;
--placeholder: "";
position: relative;
@@ -54,6 +55,8 @@ Please see LICENSE files in the repository root for full details.
span.mx_UserPill,
span.mx_RoomPill,
span.mx_SpacePill {
+ display: inline-flex;
+ align-items: center;
user-select: all;
position: relative;
cursor: unset; /* We don't want indicate clickability */
diff --git a/res/css/views/settings/encryption/_EncryptionCard.pcss b/res/css/views/settings/encryption/_EncryptionCard.pcss
index 5598facfa9..5aba3fe7d7 100644
--- a/res/css/views/settings/encryption/_EncryptionCard.pcss
+++ b/res/css/views/settings/encryption/_EncryptionCard.pcss
@@ -30,6 +30,13 @@
text-align: center;
}
}
+
+ /* extra class for specifying that we don't need a border */
+ &.mx_EncryptionCard_noBorder {
+ border: 0px none;
+ box-shadow: none;
+ padding: 0px;
+ }
}
.mx_EncryptionCard_buttons {
diff --git a/res/css/views/toasts/_IncomingCallToast.pcss b/res/css/views/toasts/_IncomingCallToast.pcss
index 2aafff6b04..e5ba041f12 100644
--- a/res/css/views/toasts/_IncomingCallToast.pcss
+++ b/res/css/views/toasts/_IncomingCallToast.pcss
@@ -11,76 +11,52 @@ Please see LICENSE files in the repository root for full details.
display: flex;
flex-direction: row;
pointer-events: initial; /* restore pointer events so the user can accept/decline */
- width: 250px;
- $closeButtonSize: 16px;
+ $closeButtonSize: var(--cpd-space-4x);
.mx_IncomingCallToast_content {
display: flex;
flex-direction: column;
- margin-left: 8px;
+ gap: var(--cpd-space-4x);
+ padding: var(--cpd-space-3x);
width: 100%;
overflow: hidden;
- .mx_IncomingCallToast_info {
- margin-bottom: $spacing-16;
-
- .mx_IncomingCallToast_room {
- display: inline-block;
-
- font-weight: var(--cpd-font-weight-semibold);
- font-size: $font-15px;
- line-height: $font-24px;
-
- /* Prevent overlap with the close button */
- width: calc(100% - $closeButtonSize - 2 * $spacing-4);
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-
- margin-bottom: $spacing-4;
- }
-
- .mx_IncomingCallToast_message {
- font-size: $font-12px;
- line-height: $font-15px;
-
- margin-bottom: $spacing-4;
- }
-
- .mx_LiveContentSummary {
- font-size: $font-12px;
- line-height: $font-15px;
-
- .mx_LiveContentSummary_participants::before {
- width: 15px;
- height: 15px;
- }
- }
+ .mx_IncomingCallToast_message {
+ font-size: var(--cpd-font-size-body-lg);
+ line-height: var(--cpd-font-size-heading-sm);
+ width: calc(100% - $closeButtonSize - 2 * var(--cpd-space-1x));
+ font-weight: var(--cpd-font-weight-semibold);
}
- .mx_IncomingCallToast_joinButton {
- position: relative;
+ .mx_LiveContentSummary_participants::before {
+ width: 15px;
+ height: 15px;
+ }
- bottom: $spacing-4;
- right: $spacing-4;
+ .mx_IncomingCallToast_buttons {
+ display: flex;
+ gap: var(--cpd-space-2x);
+ }
+
+ .mx_IncomingCallToast_actionButton {
+ position: relative;
align-self: flex-end;
box-sizing: border-box;
min-width: 120px;
- padding: $spacing-4 0;
-
- line-height: $font-24px;
+ padding: var(--cpd-space-1x) 0;
+ padding-right: var(--cpd-space-4x);
+ line-height: var(--cpd-space-6x);
}
}
.mx_IncomingCallToast_closeButton {
position: absolute;
- top: $spacing-4;
- right: $spacing-4;
+ right: 0;
display: flex;
height: $closeButtonSize;
@@ -99,4 +75,10 @@ Please see LICENSE files in the repository root for full details.
mask-position: center;
}
}
+ .mx_IncomingCallToast_toggleWithLabel {
+ display: flex;
+ span {
+ flex-grow: 1;
+ }
+ }
}
diff --git a/res/media/message.mp3 b/res/media/message.mp3
index 0e2f3207a7..5e9645adaf 100644
Binary files a/res/media/message.mp3 and b/res/media/message.mp3 differ
diff --git a/res/media/message.ogg b/res/media/message.ogg
index 23e5a89f1c..d5efbd27a3 100644
Binary files a/res/media/message.ogg and b/res/media/message.ogg differ
diff --git a/res/media/ring.mp3 b/res/media/ring.mp3
index 9b08ec7b36..386cce6998 100644
Binary files a/res/media/ring.mp3 and b/res/media/ring.mp3 differ
diff --git a/res/media/ring.ogg b/res/media/ring.ogg
index 41a22160b0..48c3f1c2ad 100644
Binary files a/res/media/ring.ogg and b/res/media/ring.ogg differ
diff --git a/scripts/get-version-from-git.sh b/scripts/get-version-from-git.sh
index c8459dba44..90de770478 100755
--- a/scripts/get-version-from-git.sh
+++ b/scripts/get-version-from-git.sh
@@ -1,11 +1,11 @@
#!/usr/bin/env bash
-# Echoes a version based on the git hashes of the element-web, react-sdk & js-sdk checkouts, for the case where
+# Echoes a version based on the git hashes of the element-web & js-sdk checkouts, for the case where
# these dependencies are git checkouts.
set -e
-# Since the deps are fetched from git, we can rev-parse
+# Since the deps are fetched from git & linked, we can rev-parse
JSSDK_SHA=$(git -C node_modules/matrix-js-sdk rev-parse --short=12 HEAD)
VECTOR_SHA=$(git rev-parse --short=12 HEAD) # use the ACTUAL SHA rather than assume develop
-echo $VECTOR_SHA-js-$JSSDK_SHA
+echo "$VECTOR_SHA-js-$JSSDK_SHA"
diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts
index 77e57e1434..289bda3f49 100644
--- a/src/@types/global.d.ts
+++ b/src/@types/global.d.ts
@@ -68,9 +68,17 @@ type ElectronChannel =
| "openDesktopCapturerSourcePicker"
| "userAccessToken"
| "homeserverUrl"
- | "serverSupportedVersions";
+ | "serverSupportedVersions"
+ | "showToast";
declare global {
+ // use `number` as the return type in all cases for globalThis.set{Interval,Timeout},
+ // so we don't accidentally use the methods on NodeJS.Timeout - they only exist in a subset of environments.
+ // The overload for clear{Interval,Timeout} is resolved as expected.
+ // We use `ReturnType` in the code to be agnostic of if this definition gets loaded.
+ function setInterval(handler: TimerHandler, timeout: number, ...arguments: any[]): number;
+ function setTimeout(handler: TimerHandler, timeout: number, ...arguments: any[]): number;
+
interface Window {
mxSendRageshake: (text: string, withLogs?: boolean) => void;
matrixLogger: typeof logger;
diff --git a/src/Avatar.ts b/src/Avatar.ts
index 921332c250..8852578f28 100644
--- a/src/Avatar.ts
+++ b/src/Avatar.ts
@@ -13,14 +13,18 @@ import DMRoomMap from "./utils/DMRoomMap";
import { mediaFromMxc } from "./customisations/Media";
import { isLocalRoom } from "./utils/localRoom/isLocalRoom";
import { getFirstGrapheme } from "./utils/strings";
+import ThemeWatcher from "./settings/watchers/ThemeWatcher";
/**
* Hardcoded from the Compound colors.
* Shade for background as defined in the compound web implementation
* https://github.com/vector-im/compound-web/blob/main/src/components/Avatar
*/
-const AVATAR_BG_COLORS = ["#e9f2ff", "#faeefb", "#e3f7ed", "#ffecf0", "#ffefe4", "#e3f5f8", "#f1efff", "#e0f8d9"];
-const AVATAR_TEXT_COLORS = ["#043894", "#671481", "#004933", "#7e0642", "#850000", "#004077", "#4c05b5", "#004b00"];
+const AVATAR_BG_LIGHT_COLORS = ["#e0f8d9", "#e3f5f8", "#faeefb", "#f1efff", "#ffecf0", "#ffefe4"];
+const AVATAR_TEXT_LIGHT_COLORS = ["#005f00", "#00548c", "#822198", "#5d26cd", "#9f0850", "#9b2200"];
+
+const AVATAR_BG_DARK_COLORS = ["#002600", "#001b4e", "#37004e", "#22006a", "#450018", "#470000"];
+const AVATAR_TEXT_DARK_COLORS = ["#56c02c", "#21bacd", "#d991de", "#ad9cfe", "#fe84a2", "#f6913d"];
// Not to be used for BaseAvatar urls as that has similar default avatar fallback already
export function avatarUrlForMember(
@@ -42,6 +46,13 @@ export function avatarUrlForMember(
return url;
}
+/**
+ * Determines if the current theme is dark
+ */
+function isDarkTheme(): boolean {
+ return new ThemeWatcher().getEffectiveTheme() === "dark";
+}
+
/**
* Determines the HEX color to use in the avatar pills
* @param id the user or room ID
@@ -51,7 +62,8 @@ export function getAvatarTextColor(id: string): string {
// eslint-disable-next-line react-hooks/rules-of-hooks
const index = useIdColorHash(id);
- return AVATAR_TEXT_COLORS[index - 1];
+ // Use light colors by default
+ return isDarkTheme() ? AVATAR_TEXT_DARK_COLORS[index - 1] : AVATAR_TEXT_LIGHT_COLORS[index - 1];
}
export function avatarUrlForUser(
@@ -103,7 +115,10 @@ export function defaultAvatarUrlForString(s: string): string {
// overwritten color value in custom themes
const cssVariable = `--avatar-background-colors_${colorIndex}`;
const cssValue = getComputedStyle(document.body).getPropertyValue(cssVariable);
- const color = cssValue || AVATAR_BG_COLORS[colorIndex - 1];
+ // Light colors are the default
+ const color =
+ cssValue || isDarkTheme() ? AVATAR_BG_DARK_COLORS[colorIndex - 1] : AVATAR_BG_LIGHT_COLORS[colorIndex - 1];
+
let dataUrl = colorToDataURLCache.get(color);
if (!dataUrl) {
// validate color as this can come from account_data
diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts
index 05dd437f94..21641aec09 100644
--- a/src/BasePlatform.ts
+++ b/src/BasePlatform.ts
@@ -508,4 +508,19 @@ export default abstract class BasePlatform {
* Begin update polling, if applicable
*/
public startUpdater(): void {}
+
+ /**
+ * Checks if the current session is lock-free, i.e., no other instance is holding the session lock.
+ * Platforms that support session locking should override this method.
+ * @returns {boolean} True if the session is lock-free, false otherwise.
+ */
+ public abstract checkSessionLockFree(): boolean;
+ /**
+ * Attempts to acquire a session lock for this instance.
+ * If another instance is detected, calls the provided callback.
+ * Platforms that support session locking should override this method.
+ * @param _onNewInstance Callback to invoke if a new instance is detected.
+ * @returns {Promise} True if the lock was acquired, false otherwise.
+ */
+ public abstract getSessionLock(_onNewInstance: () => Promise): Promise;
}
diff --git a/src/ContentMessages.ts b/src/ContentMessages.ts
index 54aaea3ae1..e608b3470a 100644
--- a/src/ContentMessages.ts
+++ b/src/ContentMessages.ts
@@ -63,7 +63,12 @@ import { blobIsAnimated } from "./utils/Image.ts";
const PHYS_HIDPI = [0x00, 0x00, 0x16, 0x25, 0x00, 0x00, 0x16, 0x25, 0x01];
export class UploadCanceledError extends Error {}
-export class UploadFailedError extends Error {}
+export class UploadFailedError extends Error {
+ public constructor(cause: any) {
+ super();
+ this.cause = cause;
+ }
+}
interface IMediaConfig {
"m.upload.size"?: number;
@@ -367,7 +372,7 @@ export async function uploadFile(
} catch (e) {
if (abortController.signal.aborted) throw new UploadCanceledError();
console.error("Failed to upload file", e);
- throw new UploadFailedError();
+ throw new UploadFailedError(e);
}
if (abortController.signal.aborted) throw new UploadCanceledError();
@@ -386,7 +391,7 @@ export async function uploadFile(
} catch (e) {
if (abortController.signal.aborted) throw new UploadCanceledError();
console.error("Failed to upload file", e);
- throw new UploadFailedError();
+ throw new UploadFailedError(e);
}
if (abortController.signal.aborted) throw new UploadCanceledError();
// If the attachment isn't encrypted then include the URL directly.
@@ -638,15 +643,18 @@ export default class ContentMessages {
dis.dispatch({ action: Action.UploadFinished, upload });
dis.dispatch({ action: "message_sent" });
} catch (error) {
+ // Unwrap UploadFailedError to get the underlying error
+ const unwrappedError = error instanceof UploadFailedError && error.cause ? error.cause : error;
+
// 413: File was too big or upset the server in some way:
// clear the media size limit so we fetch it again next time we try to upload
- if (error instanceof HTTPError && error.httpStatus === 413) {
+ if (unwrappedError instanceof HTTPError && unwrappedError.httpStatus === 413) {
this.mediaConfig = null;
}
if (!upload.cancelled) {
let desc = _t("upload_failed_generic", { fileName: upload.fileName });
- if (error instanceof HTTPError && error.httpStatus === 413) {
+ if (unwrappedError instanceof HTTPError && unwrappedError.httpStatus === 413) {
desc = _t("upload_failed_size", {
fileName: upload.fileName,
});
diff --git a/src/LegacyCallHandler.tsx b/src/LegacyCallHandler.tsx
index 9c3e7073d5..33770fe4bf 100644
--- a/src/LegacyCallHandler.tsx
+++ b/src/LegacyCallHandler.tsx
@@ -112,6 +112,7 @@ export enum LegacyCallHandlerEvent {
CallsChanged = "calls_changed",
CallChangeRoom = "call_change_room",
SilencedCallsChanged = "silenced_calls_changed",
+ ShownSidebarsChanged = "shown_sidebars_changed",
CallState = "call_state",
ProtocolSupport = "protocol_support",
}
@@ -120,6 +121,7 @@ type EventEmitterMap = {
[LegacyCallHandlerEvent.CallsChanged]: (calls: Map) => void;
[LegacyCallHandlerEvent.CallChangeRoom]: (call: MatrixCall) => void;
[LegacyCallHandlerEvent.SilencedCallsChanged]: (calls: Set) => void;
+ [LegacyCallHandlerEvent.ShownSidebarsChanged]: (sidebarsShown: Map) => void;
[LegacyCallHandlerEvent.CallState]: (mappedRoomId: string | null, status: CallState) => void;
[LegacyCallHandlerEvent.ProtocolSupport]: () => void;
};
@@ -144,6 +146,8 @@ export default class LegacyCallHandler extends TypedEventEmitter(); // callIds
+ private shownSidebars = new Map(); // callId (call) -> sidebar show
+
private backgroundAudio = new BackgroundAudio();
private playingSources: Record = {}; // Record them for stopping
@@ -240,6 +244,15 @@ export default class LegacyCallHandler extends TypedEventEmitter {
try {
const protocols = await MatrixClientPeg.safeGet().getThirdpartyProtocols();
@@ -377,6 +390,9 @@ export default class LegacyCallHandler extends TypedEventEmitter m.sender === cli.getUserId());
- // Check maximum age (<= 15 seconds) of a call notify event that will trigger a ringing notification
- if (EventType.CallNotify === ev.getType() && (ev.getAge() ?? 0) < 15000 && !thisUserHasConnectedDevice) {
- const content = ev.getContent();
+ if (EventType.RTCNotification === ev.getType() && !thisUserHasConnectedDevice) {
+ const content = ev.getContent() as IRTCNotificationContent;
const roomId = ev.getRoomId();
- if (typeof content.call_id !== "string") {
- logger.warn("Received malformatted CallNotify event. Did not contain 'call_id' of type 'string'");
+ const eventId = ev.getId();
+
+ // Check maximum age of a call notification event that will trigger a ringing notification
+ if (Date.now() - getNotificationEventSendTs(ev) > content.lifetime) {
+ logger.warn("Received outdated RTCNotification event.");
return;
}
if (!roomId) {
- logger.warn("Could not get roomId for CallNotify event");
+ logger.warn("Could not get roomId for RTCNotification event");
+ return;
+ }
+ if (!eventId) {
+ logger.warn("Could not get eventId for RTCNotification event");
return;
}
ToastStore.sharedInstance().addOrReplaceToast({
- key: getIncomingCallToastKey(content.call_id, roomId),
+ key: getIncomingCallToastKey(eventId, roomId),
priority: 100,
component: IncomingCallToast,
bodyClassName: "mx_IncomingCallToast",
- props: { notifyEvent: ev },
+ props: { notificationEvent: ev },
});
}
}
diff --git a/src/RoomAliasCache.ts b/src/RoomAliasCache.ts
index fd611a7559..ff57a70e6c 100644
--- a/src/RoomAliasCache.ts
+++ b/src/RoomAliasCache.ts
@@ -6,6 +6,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.
*/
+type CacheResult = { roomId: string; viaServers: string[] };
+
/**
* This is meant to be a cache of room alias to room ID so that moving between
* rooms happens smoothly (for example using browser back / forward buttons).
@@ -16,12 +18,12 @@ Please see LICENSE files in the repository root for full details.
* A similar thing could also be achieved via `pushState` with a state object,
* but keeping it separate like this seems easier in case we do want to extend.
*/
-const aliasToIDMap = new Map();
+const cache = new Map();
-export function storeRoomAliasInCache(alias: string, id: string): void {
- aliasToIDMap.set(alias, id);
+export function storeRoomAliasInCache(alias: string, roomId: string, viaServers: string[]): void {
+ cache.set(alias, { roomId, viaServers });
}
-export function getCachedRoomIDForAlias(alias: string): string | undefined {
- return aliasToIDMap.get(alias);
+export function getCachedRoomIdForAlias(alias: string): CacheResult | undefined {
+ return cache.get(alias);
}
diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts
index ecf895fd79..a5be789c8a 100644
--- a/src/SecurityManager.ts
+++ b/src/SecurityManager.ts
@@ -14,7 +14,6 @@ import { logger as rootLogger } from "matrix-js-sdk/src/logger";
import Modal from "./Modal";
import { MatrixClientPeg } from "./MatrixClientPeg";
import { _t } from "./languageHandler";
-import { isSecureBackupRequired } from "./utils/WellKnownUtils";
import AccessSecretStorageDialog, {
type KeyParams,
} from "./components/views/dialogs/security/AccessSecretStorageDialog";
@@ -232,15 +231,6 @@ async function doAccessSecretStorage(func: () => Promise, opts: AccessSecr
undefined,
/* priority = */ false,
/* static = */ true,
- /* options = */ {
- onBeforeClose: async (reason): Promise => {
- // If Secure Backup is required, you cannot leave the modal.
- if (reason === "backgroundClick") {
- return !isSecureBackupRequired(cli);
- }
- return true;
- },
- },
);
const [confirmed] = await finished;
if (!confirmed) {
diff --git a/src/accessibility/LandmarkNavigation.ts b/src/accessibility/LandmarkNavigation.ts
index 1c6fe54345..404e01e99c 100644
--- a/src/accessibility/LandmarkNavigation.ts
+++ b/src/accessibility/LandmarkNavigation.ts
@@ -9,6 +9,7 @@
import { TimelineRenderingType } from "../contexts/RoomContext";
import { Action } from "../dispatcher/actions";
import defaultDispatcher from "../dispatcher/dispatcher";
+import SettingsStore from "../settings/SettingsStore";
export const enum Landmark {
// This is the space/home button in the left panel.
@@ -72,10 +73,16 @@ export class LandmarkNavigation {
const landmarkToDomElementMap: Record HTMLElement | null | undefined> = {
[Landmark.ACTIVE_SPACE_BUTTON]: () => document.querySelector(".mx_SpaceButton_active"),
- [Landmark.ROOM_SEARCH]: () => document.querySelector(".mx_RoomSearch"),
+ [Landmark.ROOM_SEARCH]: () =>
+ SettingsStore.getValue("feature_new_room_list")
+ ? document.querySelector(".mx_RoomListSearch_search")
+ : document.querySelector(".mx_RoomSearch"),
[Landmark.ROOM_LIST]: () =>
- document.querySelector(".mx_RoomTile_selected") ||
- document.querySelector(".mx_RoomTile"),
+ SettingsStore.getValue("feature_new_room_list")
+ ? document.querySelector(".mx_RoomListItemView_selected") ||
+ document.querySelector(".mx_RoomListItemView")
+ : document.querySelector(".mx_RoomTile_selected") ||
+ document.querySelector(".mx_RoomTile"),
[Landmark.MESSAGE_COMPOSER_OR_HOME]: () => {
const isComposerOpen = !!document.querySelector(".mx_MessageComposer");
diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx
index 4ed369fb13..ed2fbe87a5 100644
--- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx
+++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx
@@ -25,11 +25,6 @@ import StyledRadioButton from "../../../../components/views/elements/StyledRadio
import AccessibleButton from "../../../../components/views/elements/AccessibleButton";
import DialogButtons from "../../../../components/views/elements/DialogButtons";
import InlineSpinner from "../../../../components/views/elements/InlineSpinner";
-import {
- getSecureBackupSetupMethods,
- isSecureBackupRequired,
- SecureBackupSetupMethod,
-} from "../../../../utils/WellKnownUtils";
import { ModuleRunner } from "../../../../modules/ModuleRunner";
import type Field from "../../../../components/views/elements/Field";
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
@@ -39,6 +34,11 @@ import { type IValidationResult } from "../../../../components/views/elements/Va
import PassphraseConfirmField from "../../../../components/views/auth/PassphraseConfirmField";
import { initialiseDehydrationIfEnabled } from "../../../../utils/device/dehydration";
+enum SecureBackupSetupMethod {
+ Key = "key",
+ Passphrase = "passphrase",
+}
+
// I made a mistake while converting this and it has to be fixed!
enum Phase {
Loading = "loading",
@@ -68,7 +68,6 @@ interface IState {
downloaded: boolean;
setPassphrase: boolean;
- canSkip: boolean;
passPhraseKeySelected: string;
error?: boolean;
}
@@ -93,16 +92,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent
@@ -410,7 +395,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent
);
@@ -601,7 +585,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent
@@ -672,7 +655,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent
diff --git a/src/audio/Playback.ts b/src/audio/Playback.ts
index 96930d43af..b0af13fb56 100644
--- a/src/audio/Playback.ts
+++ b/src/audio/Playback.ts
@@ -20,6 +20,7 @@ import { DEFAULT_WAVEFORM, PLAYBACK_WAVEFORM_SAMPLES } from "./consts";
import { PlaybackEncoder } from "../PlaybackEncoder";
export enum PlaybackState {
+ Preparing = "preparing", // preparing to decode
Decoding = "decoding",
Stopped = "stopped", // no progress on timeline
Paused = "paused", // some progress on timeline
@@ -146,6 +147,8 @@ export class Playback extends EventEmitter implements IDestroyable, PlaybackInte
return;
}
+ this.state = PlaybackState.Preparing;
+
// The point where we use an audio element is fairly arbitrary, though we don't want
// it to be too low. As of writing, voice messages want to show a waveform but audio
// messages do not. Using an audio element means we can't show a waveform preview, so
diff --git a/src/components/structures/EmbeddedPage.tsx b/src/components/structures/EmbeddedPage.tsx
index f1c7b75b96..b1702c87c7 100644
--- a/src/components/structures/EmbeddedPage.tsx
+++ b/src/components/structures/EmbeddedPage.tsx
@@ -72,7 +72,11 @@ export default class EmbeddedPage extends React.PureComponent {
return;
}
- let body = (await res.text()).replace(/_t\(['"]([\s\S]*?)['"]\)/gm, (match, g1) => this.translate(g1));
+ // Replace '," and HTML encoded variants
+ let body = (await res.text()).replace(
+ /_t\((?:['"]|(?:(?:34|27);))([\s\S]*?)(?:['"]|(?:(?:34|27);))\)/gm,
+ (match, g1) => this.translate(g1),
+ );
if (this.props.replaceMap) {
Object.keys(this.props.replaceMap).forEach((key) => {
diff --git a/src/components/structures/InteractiveAuth.tsx b/src/components/structures/InteractiveAuth.tsx
index d1c670c27c..a4de1e4d21 100644
--- a/src/components/structures/InteractiveAuth.tsx
+++ b/src/components/structures/InteractiveAuth.tsx
@@ -20,7 +20,7 @@ import { logger } from "matrix-js-sdk/src/logger";
import getEntryComponentForLoginType, {
type ContinueKind,
- type CustomAuthType,
+ CustomAuthType,
type IStageComponent,
} from "../views/auth/InteractiveAuthEntryComponents";
import Spinner from "../views/elements/Spinner";
@@ -117,6 +117,7 @@ export default class InteractiveAuthComponent extends React.Component {
private readonly stores: SdkContextClass;
private loadSessionAbortController = new AbortController();
+ private sessionLoadStarted = false;
+
public constructor(props: IProps) {
super(props);
this.stores = SdkContextClass.instance;
@@ -312,7 +313,8 @@ export default class MatrixChat extends React.PureComponent {
private async initSession(): Promise {
// The Rust Crypto SDK will break if two Element instances try to use the same datastore at once, so
// make sure we are the only Element instance in town (on this browser/domain).
- if (!(await getSessionLock(() => this.onSessionLockStolen()))) {
+ const platform = PlatformPeg.get();
+ if (platform && !(await platform.getSessionLock(() => this.onSessionLockStolen()))) {
// we failed to get the lock. onSessionLockStolen should already have been called, so nothing left to do.
return;
}
@@ -470,15 +472,21 @@ export default class MatrixChat extends React.PureComponent {
this.fontWatcher.start();
initSentry(SdkConfig.get("sentry"));
-
- if (!checkSessionLockFree()) {
- // another instance holds the lock; confirm its theft before proceeding
- setTimeout(() => this.setState({ view: Views.CONFIRM_LOCK_THEFT }), 0);
- } else {
- this.startInitSession();
- }
-
window.addEventListener("resize", this.onWindowResized);
+
+ // Once we start loading the MatrixClient, we can't stop, even if MatrixChat gets unmounted (as it does
+ // in React's Strict Mode). So, start loading the session now, but only if this MatrixChat was not previously
+ // mounted.
+ if (!this.sessionLoadStarted) {
+ this.sessionLoadStarted = true;
+ const platform = PlatformPeg.get();
+ if (platform && !platform.checkSessionLockFree()) {
+ // another instance holds the lock; confirm its theft before proceeding
+ setTimeout(() => this.setState({ view: Views.CONFIRM_LOCK_THEFT }), 0);
+ } else {
+ this.startInitSession();
+ }
+ }
}
public componentDidUpdate(prevProps: IProps, prevState: IState): void {
@@ -1019,7 +1027,7 @@ export default class MatrixChat extends React.PureComponent {
presentedId = theAlias;
// Store display alias of the presented room in cache to speed future
// navigation.
- storeRoomAliasInCache(theAlias, room.roomId);
+ storeRoomAliasInCache(theAlias, room.roomId, calculateRoomVia(room));
}
// Store this as the ID of the last room accessed. This is so that we can
diff --git a/src/components/structures/PipContainer.tsx b/src/components/structures/PipContainer.tsx
index e46267c149..18e8c526db 100644
--- a/src/components/structures/PipContainer.tsx
+++ b/src/components/structures/PipContainer.tsx
@@ -245,6 +245,7 @@ class PipContainerInner extends React.Component {
secondaryCall={this.state.secondaryCall}
pipMode={pipMode}
onResize={onResize}
+ sidebarShown={false}
/>
));
}
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 20951448af..6ed41bd25c 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -133,6 +133,7 @@ import { PinnedMessageBanner } from "../views/rooms/PinnedMessageBanner";
import { ScopedRoomContextProvider, useScopedRoomContext } from "../../contexts/ScopedRoomContext";
import { DeclineAndBlockInviteDialog } from "../views/dialogs/DeclineAndBlockInviteDialog";
import { type FocusMessageSearchPayload } from "../../dispatcher/payloads/FocusMessageSearchPayload.ts";
+import { isRoomEncrypted } from "../../hooks/useIsEncrypted";
const DEBUG = false;
const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000;
@@ -257,6 +258,7 @@ interface LocalRoomViewProps {
roomView: RefObject;
onFileDrop: (dataTransfer: DataTransfer) => Promise;
mainSplitContentType: MainSplitContentType;
+ e2eStatus?: E2EStatus;
}
/**
@@ -304,6 +306,7 @@ function LocalRoomView(props: LocalRoomViewProps): ReactElement {
} else {
composer = (
{
}
private async getIsRoomEncrypted(roomId = this.state.roomId): Promise {
- const crypto = this.context.client?.getCrypto();
- if (!crypto || !roomId) return false;
+ if (!roomId) return false;
- return await crypto.isEncryptionEnabledInRoom(roomId);
+ const room = this.context.client?.getRoom(roomId);
+ const crypto = this.context.client?.getCrypto();
+ if (!room || !crypto) return false;
+
+ return isRoomEncrypted(room, crypto);
}
private async calculateRecommendedVersion(room: Room): Promise {
@@ -2061,6 +2067,7 @@ export class RoomView extends React.Component {
return (
{
diff --git a/src/components/structures/auth/CompleteSecurity.tsx b/src/components/structures/auth/CompleteSecurity.tsx
index 5bade7b24a..07a4852895 100644
--- a/src/components/structures/auth/CompleteSecurity.tsx
+++ b/src/components/structures/auth/CompleteSecurity.tsx
@@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import React from "react";
+import { Glass } from "@vector-im/compound-web";
import { _t } from "../../../languageHandler";
import { SetupEncryptionStore, Phase } from "../../../stores/SetupEncryptionStore";
@@ -22,15 +23,17 @@ interface IProps {
interface IState {
phase?: Phase;
- lostKeys: boolean;
}
+/**
+ * Prompts the user to verify their device when they first log in.
+ */
export default class CompleteSecurity extends React.Component {
public constructor(props: IProps) {
super(props);
const store = SetupEncryptionStore.sharedInstance();
store.start();
- this.state = { phase: store.phase, lostKeys: store.lostKeys() };
+ this.state = { phase: store.phase };
}
public componentDidMount(): void {
@@ -40,7 +43,7 @@ export default class CompleteSecurity extends React.Component {
private onStoreUpdate = (): void => {
const store = SetupEncryptionStore.sharedInstance();
- this.setState({ phase: store.phase, lostKeys: store.lostKeys() });
+ this.setState({ phase: store.phase });
};
private onSkipClick = (): void => {
@@ -55,20 +58,14 @@ export default class CompleteSecurity extends React.Component {
}
public render(): React.ReactNode {
- const { phase, lostKeys } = this.state;
+ const { phase } = this.state;
let icon;
let title;
if (phase === Phase.Loading) {
return null;
} else if (phase === Phase.Intro) {
- if (lostKeys) {
- icon = ;
- title = _t("encryption|verification|after_new_login|unable_to_verify");
- } else {
- icon = ;
- title = _t("encryption|verification|after_new_login|verify_this_device");
- }
+ // We don't specify an icon nor title since `SetupEncryptionBody` provides its own
} else if (phase === Phase.Done) {
icon = ;
title = _t("encryption|verification|after_new_login|device_verified");
@@ -98,17 +95,19 @@ export default class CompleteSecurity extends React.Component {
}
return (
-
-
-
- {icon}
- {title}
- {skipButton}
-
-
-
-
-
+
+
+
+
+ {icon}
+ {title}
+ {skipButton}
+
+
+
+
+
+
);
}
diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx
index b76a623e2c..5098265e67 100644
--- a/src/components/structures/auth/SetupEncryptionBody.tsx
+++ b/src/components/structures/auth/SetupEncryptionBody.tsx
@@ -9,7 +9,9 @@ Please see LICENSE files in the repository root for full details.
import React, { type JSX } from "react";
import { type KeyBackupInfo, type VerificationRequest } from "matrix-js-sdk/src/crypto-api";
import { logger } from "matrix-js-sdk/src/logger";
-import { type SecretStorageKeyDescription } from "matrix-js-sdk/src/secret-storage";
+import DevicesIcon from "@vector-im/compound-design-tokens/assets/web/icons/devices";
+import LockIcon from "@vector-im/compound-design-tokens/assets/web/icons/lock-solid";
+import { Button } from "@vector-im/compound-web";
import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
@@ -17,25 +19,38 @@ import Modal from "../../../Modal";
import VerificationRequestDialog from "../../views/dialogs/VerificationRequestDialog";
import { SetupEncryptionStore, Phase } from "../../../stores/SetupEncryptionStore";
import EncryptionPanel from "../../views/right_panel/EncryptionPanel";
-import AccessibleButton, { type ButtonEvent } from "../../views/elements/AccessibleButton";
+import AccessibleButton from "../../views/elements/AccessibleButton";
import Spinner from "../../views/elements/Spinner";
import { ResetIdentityDialog } from "../../views/dialogs/ResetIdentityDialog";
-
-function keyHasPassphrase(keyInfo: SecretStorageKeyDescription): boolean {
- return Boolean(keyInfo.passphrase && keyInfo.passphrase.salt && keyInfo.passphrase.iterations);
-}
+import { EncryptionCard } from "../../views/settings/encryption/EncryptionCard";
+import { EncryptionCardButtons } from "../../views/settings/encryption/EncryptionCardButtons";
+import { EncryptionCardEmphasisedContent } from "../../views/settings/encryption/EncryptionCardEmphasisedContent";
+import ExternalLink from "../../views/elements/ExternalLink";
+import dispatcher from "../../../dispatcher/dispatcher";
interface IProps {
onFinished: () => void;
+ /**
+ * Offer the user an option to log out, instead of setting up encryption.
+ *
+ * This is used when this component is shown when the user is initially
+ * prompted to set up encryption, before the user is shown the main chat
+ * interface.
+ *
+ * Defaults to `false` if omitted.
+ */
+ allowLogout?: boolean;
}
interface IState {
phase?: Phase;
verificationRequest: VerificationRequest | null;
backupInfo: KeyBackupInfo | null;
- lostKeys: boolean;
}
+/**
+ * Component to set up encryption by verifying the current device.
+ */
export default class SetupEncryptionBody extends React.Component {
public constructor(props: IProps) {
super(props);
@@ -48,7 +63,6 @@ export default class SetupEncryptionBody extends React.Component
// Because of the latter, it lives in the state.
verificationRequest: store.verificationRequest,
backupInfo: store.backupInfo,
- lostKeys: store.lostKeys(),
};
}
@@ -67,7 +81,6 @@ export default class SetupEncryptionBody extends React.Component
phase: store.phase,
verificationRequest: store.verificationRequest,
backupInfo: store.backupInfo,
- lostKeys: store.lostKeys(),
});
};
@@ -112,8 +125,8 @@ export default class SetupEncryptionBody extends React.Component
store.returnAfterSkip();
};
- private onResetClick = (ev: ButtonEvent): void => {
- ev.preventDefault();
+ private onCantConfirmClick = (): void => {
+ const store = SetupEncryptionStore.sharedInstance();
Modal.createDialog(ResetIdentityDialog, {
onReset: () => {
// The user completed the reset process - close this dialog
@@ -121,10 +134,14 @@ export default class SetupEncryptionBody extends React.Component
const store = SetupEncryptionStore.sharedInstance();
store.done();
},
- variant: "confirm",
+ variant: store.lostKeys() ? "no_verification_method" : "confirm",
});
};
+ private onSignOutClick = (): void => {
+ dispatcher.dispatch({ action: "logout" });
+ };
+
private onDoneClick = (): void => {
const store = SetupEncryptionStore.sharedInstance();
store.done();
@@ -136,7 +153,7 @@ export default class SetupEncryptionBody extends React.Component
public render(): React.ReactNode {
const cli = MatrixClientPeg.safeGet();
- const { phase, lostKeys } = this.state;
+ const { phase } = this.state;
if (this.state.verificationRequest && cli.getUser(this.state.verificationRequest.otherUserId)) {
return (
@@ -149,69 +166,59 @@ export default class SetupEncryptionBody extends React.Component
/>
);
} else if (phase === Phase.Intro) {
- if (lostKeys) {
- return (
-
-
{_t("encryption|verification|no_key_or_device")}
+ const store = SetupEncryptionStore.sharedInstance();
-
+ let verifyButton;
+ if (store.hasDevicesToVerifyAgainst) {
+ verifyButton = (
+
);
}
+
+ let useRecoveryKeyButton;
+ if (store.keyInfo) {
+ useRecoveryKeyButton = (
+
+ );
+ }
+
+ let signOutButton;
+ if (this.props.allowLogout) {
+ signOutButton = (
+
+ );
+ }
+
+ return (
+
+
+ {_t("encryption|verification|confirm_identity_description")}
+
+
+ {_t("action|learn_more")}
+
+
+
+
+ {verifyButton}
+ {useRecoveryKeyButton}
+
+ {signOutButton}
+
+
+ );
} else if (phase === Phase.Done) {
let message: JSX.Element;
if (this.state.backupInfo) {
diff --git a/src/components/utils/ListView.tsx b/src/components/utils/ListView.tsx
index b11a3d638c..f99b7ab944 100644
--- a/src/components/utils/ListView.tsx
+++ b/src/components/utils/ListView.tsx
@@ -133,13 +133,13 @@ export function ListView(props: IListViewProps {
+ setTabIndexKey(key);
isScrollingToItem.current = false;
},
});
@@ -184,28 +184,32 @@ export function ListView(props: IListViewProps(props: IListViewProps = new Map([
[FilterKey.UnreadFilter, _td("room_list|filters|unread")],
[FilterKey.PeopleFilter, _td("room_list|filters|people")],
[FilterKey.RoomsFilter, _td("room_list|filters|rooms")],
+ [FilterKey.FavouriteFilter, _td("room_list|filters|favourite")],
[FilterKey.MentionsFilter, _td("room_list|filters|mentions")],
[FilterKey.InvitesFilter, _td("room_list|filters|invites")],
- [FilterKey.FavouriteFilter, _td("room_list|filters|favourite")],
[FilterKey.LowPriorityFilter, _td("room_list|filters|low_priority")],
]);
@@ -74,9 +72,6 @@ export function useFilteredRooms(): FilteredRooms {
setRoomsResult(newRooms);
}, []);
- // Reset filters when active space changes
- useEventEmitter(SpaceStore.instance, UPDATE_SELECTED_SPACE, () => setPrimaryFilter(undefined));
-
const filterUndefined = (array: (FilterKey | undefined)[]): FilterKey[] =>
array.filter((f) => f !== undefined) as FilterKey[];
diff --git a/src/components/viewmodels/settings/encryption/KeyStoragePanelViewModel.ts b/src/components/viewmodels/settings/encryption/KeyStoragePanelViewModel.ts
index ee301bd27f..60ad21236f 100644
--- a/src/components/viewmodels/settings/encryption/KeyStoragePanelViewModel.ts
+++ b/src/components/viewmodels/settings/encryption/KeyStoragePanelViewModel.ts
@@ -79,12 +79,30 @@ export function useKeyStoragePanelViewModel(): KeyStoragePanelState {
return;
}
if (enable) {
- // If there is no existing key backup on the server, create one.
- // `resetKeyBackup` will delete any existing backup, so we only do this if there is no existing backup.
- const currentKeyBackup = await crypto.checkKeyBackupAndEnable();
+ const childLogger = logger.getChild("[enable key storage]");
+ childLogger.info("User requested enabling key storage");
+ let currentKeyBackup = await crypto.checkKeyBackupAndEnable();
+ if (currentKeyBackup) {
+ logger.info(
+ `Existing key backup is present. version: ${currentKeyBackup.backupInfo.version}`,
+ currentKeyBackup.trustInfo,
+ );
+ // Check if the current key backup can be used. Either of these properties causes the key backup to be used.
+ if (currentKeyBackup.trustInfo.trusted || currentKeyBackup.trustInfo.matchesDecryptionKey) {
+ logger.info("Existing key backup can be used");
+ } else {
+ logger.warn("Existing key backup cannot be used, creating new backup");
+ // There aren't any *usable* backups, so we need to create a new one.
+ currentKeyBackup = null;
+ }
+ } else {
+ logger.info("No existing key backup versions are present, creating new backup");
+ }
+
+ // If there is no usable key backup on the server, create one.
+ // `resetKeyBackup` will delete any existing backup, so we only do this if there is no usable backup.
if (currentKeyBackup === null) {
await crypto.resetKeyBackup();
-
// resetKeyBackup fires this off in the background without waiting, so we need to do it
// explicitly and wait for it, otherwise it won't be enabled yet when we check again.
await crypto.checkKeyBackupAndEnable();
@@ -93,6 +111,7 @@ export function useKeyStoragePanelViewModel(): KeyStoragePanelState {
// Set the flag so that EX no longer thinks the user wants backup disabled
await matrixClient.setAccountData(BACKUP_DISABLED_ACCOUNT_DATA_KEY, { disabled: false });
} else {
+ logger.info("User requested disabling key backup");
// This method will delete the key backup as well as server side recovery keys and other
// server-side crypto data.
await crypto.disableKeyStorage();
diff --git a/src/components/views/audio_messages/RecordingPlayback.tsx b/src/components/views/audio_messages/RecordingPlayback.tsx
index 75e1419ce6..4f5c392d41 100644
--- a/src/components/views/audio_messages/RecordingPlayback.tsx
+++ b/src/components/views/audio_messages/RecordingPlayback.tsx
@@ -74,7 +74,11 @@ export default class RecordingPlayback extends AudioPlayerBase {
}
return (
-
+
{
+interface IProps {
+ /**
+ * Whether to add a blurred shadow around the modal.
+ *
+ * If the modal component provides its own shadow or blurring, this can be
+ * disabled. Defaults to `true`.
+ */
+ addBlur?: boolean;
+}
+
+export default class AuthPage extends React.PureComponent> {
private static welcomeBackgroundUrl?: string;
// cache the url as a static to prevent it changing without refreshing
@@ -58,14 +69,26 @@ export default class AuthPage extends React.PureComponent;
+ modalContentStyle.background = "rgba(255, 255, 255, 0.59)";
+ }
+
+ const modalClasses = classNames({
+ mx_AuthPage_modal: true,
+ mx_AuthPage_modal_withBlur: this.props.addBlur !== false,
+ });
+
return (
-
-
+
+ {modalBlur}
{this.props.children}
diff --git a/src/components/views/avatars/WidgetAvatar.tsx b/src/components/views/avatars/WidgetAvatar.tsx
index c43cd98216..5baba7ca01 100644
--- a/src/components/views/avatars/WidgetAvatar.tsx
+++ b/src/components/views/avatars/WidgetAvatar.tsx
@@ -37,6 +37,8 @@ const WidgetAvatar: React.FC = ({ app, className, size = "20px", ...prop
return (
= ({ roomId, call }) =>
action: Action.ViewRoom,
room_id: roomId,
view_call: true,
- skipLobby: "shiftKey" in ev ? ev.shiftKey : false,
+ skipLobby: ("shiftKey" in ev && ev.shiftKey) || undefined,
metricsTrigger: undefined,
});
},
diff --git a/src/components/views/dialogs/BaseDialog.tsx b/src/components/views/dialogs/BaseDialog.tsx
index 56f6d0c528..a946a44e91 100644
--- a/src/components/views/dialogs/BaseDialog.tsx
+++ b/src/components/views/dialogs/BaseDialog.tsx
@@ -145,6 +145,9 @@ export default class BaseDialog extends React.Component {
const lockProps: Record = {
"onKeyDown": this.onKeyDown,
"role": "dialog",
+ // Allow the dialog to be keyboard focusable
+ // So the escape key handling works in more cases (say you select the header)
+ "tabIndex": -1,
// This should point to a node describing the dialog.
// If we were about to completely follow this recommendation we'd need to
// make all the components relying on BaseDialog to be aware of it.
diff --git a/src/components/views/dialogs/ConfirmKeyStorageOffDialog.tsx b/src/components/views/dialogs/ConfirmKeyStorageOffDialog.tsx
index 49e7cad17b..d6a5f79aeb 100644
--- a/src/components/views/dialogs/ConfirmKeyStorageOffDialog.tsx
+++ b/src/components/views/dialogs/ConfirmKeyStorageOffDialog.tsx
@@ -17,6 +17,7 @@ import { EncryptionCardButtons } from "../settings/encryption/EncryptionCardButt
import { type OpenToTabPayload } from "../../../dispatcher/payloads/OpenToTabPayload";
import { Action } from "../../../dispatcher/actions";
import { UserTab } from "./UserTab";
+import SdkConfig from "../../../SdkConfig";
interface Props {
onFinished: (dismissed: boolean) => void;
@@ -60,7 +61,7 @@ export default class ConfirmKeyStorageOffDialog extends React.Component {
a: (sub) => (
<>
-
+
{sub}
>
diff --git a/src/components/views/dialogs/CreateRoomDialog.tsx b/src/components/views/dialogs/CreateRoomDialog.tsx
index 3ff42cde22..d3771d102d 100644
--- a/src/components/views/dialogs/CreateRoomDialog.tsx
+++ b/src/components/views/dialogs/CreateRoomDialog.tsx
@@ -26,6 +26,7 @@ import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { privateShouldBeEncrypted } from "../../../utils/rooms";
import SettingsStore from "../../../settings/SettingsStore";
import LabelledCheckbox from "../elements/LabelledCheckbox";
+import { UIFeature } from "../../../settings/UIFeature";
interface IProps {
type?: RoomType;
@@ -83,6 +84,8 @@ interface IState {
export default class CreateRoomDialog extends React.Component {
private readonly askToJoinEnabled: boolean;
+ private readonly advancedSettingsEnabled: boolean;
+ private readonly allowCreatingPublicRooms: boolean;
private readonly supportsRestricted: boolean;
private nameField = createRef();
private aliasField = createRef();
@@ -91,10 +94,14 @@ export default class CreateRoomDialog extends React.Component {
super(props);
this.askToJoinEnabled = SettingsStore.getValue("feature_ask_to_join");
+ this.advancedSettingsEnabled = SettingsStore.getValue(UIFeature.AdvancedSettings);
+ this.allowCreatingPublicRooms = SettingsStore.getValue(UIFeature.AllowCreatingPublicRooms);
+
this.supportsRestricted = !!this.props.parentSpace;
+ const defaultPublic = this.allowCreatingPublicRooms && this.props.defaultPublic;
let joinRule = JoinRule.Invite;
- if (this.props.defaultPublic) {
+ if (defaultPublic) {
joinRule = JoinRule.Public;
} else if (this.supportsRestricted) {
joinRule = JoinRule.Restricted;
@@ -102,7 +109,7 @@ export default class CreateRoomDialog extends React.Component {
const cli = MatrixClientPeg.safeGet();
this.state = {
- isPublicKnockRoom: this.props.defaultPublic || false,
+ isPublicKnockRoom: defaultPublic || false,
isEncrypted: this.props.defaultEncrypted ?? privateShouldBeEncrypted(cli),
joinRule,
name: this.props.defaultName || "",
@@ -415,7 +422,7 @@ export default class CreateRoomDialog extends React.Component {
labelKnock={
this.askToJoinEnabled ? _t("room_settings|security|join_rule_knock") : undefined
}
- labelPublic={_t("common|public_room")}
+ labelPublic={this.allowCreatingPublicRooms ? _t("common|public_room") : undefined}
labelRestricted={
this.supportsRestricted ? _t("create_room|join_rule_restricted") : undefined
}
@@ -427,19 +434,21 @@ export default class CreateRoomDialog extends React.Component {
{visibilitySection}
{e2eeSection}
{aliasField}
-
-
- {this.state.detailsOpen ? _t("action|hide_advanced") : _t("action|show_advanced")}
-
-
-