Merge branch 'develop' into midhun/mvvm/state-management-2
3
.github/CODEOWNERS
vendored
@ -20,6 +20,7 @@
|
||||
# Ignore translations as those will be updated by GHA for Localazy download
|
||||
/src/i18n/strings
|
||||
/src/i18n/strings/en_EN.json @element-hq/element-web-reviewers
|
||||
# Ignore the synapse plugin as this is updated by GHA for docker image updating
|
||||
# Ignore the synapse & mas plugins as this is updated by GHA for docker image updating
|
||||
/playwright/testcontainers/synapse.ts
|
||||
/playwright/testcontainers/mas.ts
|
||||
|
||||
|
||||
2
.github/workflows/build.yml
vendored
@ -43,7 +43,7 @@ jobs:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
|
||||
2
.github/workflows/build_debian.yaml
vendored
@ -14,7 +14,7 @@ jobs:
|
||||
R2_URL: ${{ vars.CF_R2_S3_API }}
|
||||
VERSION: ${{ github.ref_name }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- name: Download package
|
||||
run: |
|
||||
|
||||
2
.github/workflows/build_develop.yml
vendored
@ -26,7 +26,7 @@ jobs:
|
||||
R2_URL: ${{ vars.CF_R2_S3_API }}
|
||||
R2_PUBLIC_URL: "https://element-web-develop.element.io"
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
|
||||
2
.github/workflows/deploy.yml
vendored
@ -34,7 +34,7 @@ jobs:
|
||||
env:
|
||||
SITE: ${{ inputs.site || 'staging.element.io' }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- name: Load GPG key
|
||||
run: |
|
||||
|
||||
2
.github/workflows/docker.yaml
vendored
@ -20,7 +20,7 @@ jobs:
|
||||
env:
|
||||
TEST_TAG: vectorim/element-web:test
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
fetch-depth: 0 # needed for docker-package to be able to calculate the version
|
||||
|
||||
|
||||
6
.github/workflows/docs.yml
vendored
@ -17,18 +17,18 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Fetch element-desktop
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
repository: element-hq/element-desktop
|
||||
path: element-desktop
|
||||
|
||||
- name: Fetch element-web
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
path: element-web
|
||||
|
||||
- name: Fetch matrix-js-sdk
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
repository: matrix-org/matrix-js-sdk
|
||||
path: matrix-js-sdk
|
||||
|
||||
@ -25,7 +25,7 @@ jobs:
|
||||
actions: read
|
||||
steps:
|
||||
- name: Download HTML report
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
|
||||
12
.github/workflows/end-to-end-tests.yaml
vendored
@ -50,7 +50,7 @@ jobs:
|
||||
runners-matrix: ${{ steps.runner-vars.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
repository: element-hq/element-web
|
||||
|
||||
@ -129,13 +129,13 @@ jobs:
|
||||
- runAllTests: false
|
||||
project: Pinecone
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
repository: element-hq/element-web
|
||||
|
||||
- name: 📥 Download artifact
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
|
||||
with:
|
||||
name: webapp
|
||||
path: webapp
|
||||
@ -154,7 +154,7 @@ jobs:
|
||||
run: echo "version=$(yarn list --pattern @playwright/test --depth=0 --json --non-interactive --no-progress | jq -r '.data.trees[].name')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache playwright binaries
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
@ -201,7 +201,7 @@ jobs:
|
||||
if: always()
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
if: inputs.skip != true
|
||||
with:
|
||||
persist-credentials: false
|
||||
@ -219,7 +219,7 @@ jobs:
|
||||
|
||||
- name: Download blob reports from GitHub Actions Artifacts
|
||||
if: inputs.skip != true
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
|
||||
with:
|
||||
pattern: all-blob-reports-*
|
||||
path: all-blob-reports
|
||||
|
||||
2
.github/workflows/netlify.yaml
vendored
@ -28,7 +28,7 @@ jobs:
|
||||
Exercise caution. Use test accounts.
|
||||
|
||||
- name: 📥 Download artifact
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
|
||||
11
.github/workflows/playwright-image-updates.yaml
vendored
@ -10,7 +10,7 @@ jobs:
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- name: Update synapse image
|
||||
run: |
|
||||
@ -21,6 +21,15 @@ jobs:
|
||||
env:
|
||||
IMAGE: ghcr.io/element-hq/synapse:develop
|
||||
|
||||
- name: Update MAS image
|
||||
run: |
|
||||
docker pull "$IMAGE"
|
||||
INSPECT=$(docker inspect --format='{{index .RepoDigests 0}}' "$IMAGE")
|
||||
DIGEST=${INSPECT#*@}
|
||||
sed -i "s/const TAG.*/const TAG = \"main@$DIGEST\";/" playwright/testcontainers/mas.ts
|
||||
env:
|
||||
IMAGE: ghcr.io/element-hq/matrix-authentication-service:main
|
||||
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7
|
||||
|
||||
6
.github/workflows/release_prepare.yml
vendored
@ -41,7 +41,7 @@ jobs:
|
||||
REPOS: matrix-js-sdk element-web element-desktop
|
||||
steps:
|
||||
- name: Checkout Element Desktop
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
if: inputs.element-desktop
|
||||
with:
|
||||
repository: element-hq/element-desktop
|
||||
@ -51,7 +51,7 @@ jobs:
|
||||
fetch-tags: true
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
- name: Checkout Element Web
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
if: inputs.element-web
|
||||
with:
|
||||
repository: element-hq/element-web
|
||||
@ -61,7 +61,7 @@ jobs:
|
||||
fetch-tags: true
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
- name: Checkout Matrix JS SDK
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
if: inputs.matrix-js-sdk
|
||||
with:
|
||||
repository: matrix-org/matrix-js-sdk
|
||||
|
||||
@ -27,7 +27,7 @@ jobs:
|
||||
run: "sudo apt-get install -y tree"
|
||||
|
||||
- name: Download Diffs
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
|
||||
@ -21,7 +21,7 @@ jobs:
|
||||
issues: read
|
||||
pull-requests: read
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
repository: element-hq/element-web
|
||||
@ -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@5a3ec84eff668545956fd18022155c47e93e2684 # v4
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
|
||||
13
.github/workflows/static_analysis.yaml
vendored
@ -23,7 +23,7 @@ jobs:
|
||||
name: "Typescript Syntax Check"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
@ -52,12 +52,13 @@ jobs:
|
||||
error|misconfigured
|
||||
welcome_to_element
|
||||
devtools|settings|elementCallUrl
|
||||
labs|sliding_sync_description
|
||||
|
||||
rethemendex_lint:
|
||||
name: "Rethemendex Check"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- run: ./res/css/rethemendex.sh
|
||||
|
||||
@ -67,7 +68,7 @@ jobs:
|
||||
name: "ESLint"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
@ -85,7 +86,7 @@ jobs:
|
||||
name: "Style Lint"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
@ -103,7 +104,7 @@ jobs:
|
||||
name: "Workflow Lint"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
@ -121,7 +122,7 @@ jobs:
|
||||
name: "Analyse Dead Code"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
|
||||
4
.github/workflows/tests.yml
vendored
@ -39,7 +39,7 @@ jobs:
|
||||
runner: [1, 2]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
repository: ${{ inputs.matrix-js-sdk-sha && 'element-hq/element-web' || github.repository }}
|
||||
|
||||
@ -55,7 +55,7 @@ jobs:
|
||||
JS_SDK_GITHUB_BASE_REF: ${{ inputs.matrix-js-sdk-sha }}
|
||||
|
||||
- name: Jest Cache
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4
|
||||
with:
|
||||
path: /tmp/jest_cache
|
||||
key: ${{ hashFiles('**/yarn.lock') }}
|
||||
|
||||
2
.github/workflows/update-jitsi.yml
vendored
@ -9,7 +9,7 @@ jobs:
|
||||
update:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
|
||||
33
CHANGELOG.md
@ -1,3 +1,36 @@
|
||||
Changes in [1.11.109](https://github.com/element-hq/element-web/releases/tag/v1.11.109) (2025-08-11)
|
||||
====================================================================================================
|
||||
This release supports the upcoming v12 ("hydra") Matrix room version and is necessary to view and participate in these rooms.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* [Backport staging] Allow /upgraderoom command without developer mode enabled ([#30529](https://github.com/element-hq/element-web/pull/30529)). Contributed by @RiotRobot.
|
||||
* [Backport staging] Support for creator/owner power level ([#30526](https://github.com/element-hq/element-web/pull/30526)). Contributed by @RiotRobot.
|
||||
* New room list: change icon and label of menu item for to start a DM ([#30470](https://github.com/element-hq/element-web/pull/30470)). Contributed by @florianduros.
|
||||
* Implement the member list with virtuoso ([#29869](https://github.com/element-hq/element-web/pull/29869)). Contributed by @langleyd.
|
||||
* Add labs option for history sharing on invite ([#30313](https://github.com/element-hq/element-web/pull/30313)). Contributed by @richvdh.
|
||||
* Bump wysiwyg to 2.39.0 adding support for pasting rich text content in the Rich Text Edtior ([#30421](https://github.com/element-hq/element-web/pull/30421)). Contributed by @langleyd.
|
||||
* Support `EventShieldReason.MISMATCHED_SENDER` ([#30403](https://github.com/element-hq/element-web/pull/30403)). Contributed by @richvdh.
|
||||
* Change unencrypted and public pills to blue ([#30399](https://github.com/element-hq/element-web/pull/30399)). Contributed by @florianduros.
|
||||
* Change color of public room icon ([#30390](https://github.com/element-hq/element-web/pull/30390)). Contributed by @florianduros.
|
||||
* Script for updating storybook screenshots ([#30340](https://github.com/element-hq/element-web/pull/30340)). Contributed by @dbkr.
|
||||
* Add toggle to hide empty state in devtools ([#30352](https://github.com/element-hq/element-web/pull/30352)). Contributed by @toger5.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* [Backport staging] Use userId to filter users in non-federated rooms when showing the InviteDialog ([#30537](https://github.com/element-hq/element-web/pull/30537)). Contributed by @RiotRobot.
|
||||
* [Backport staging] Catch error when encountering invalid m.room.pinned\_events event ([#30536](https://github.com/element-hq/element-web/pull/30536)). Contributed by @RiotRobot.
|
||||
* Update for compatibility with v12 rooms ([#30452](https://github.com/element-hq/element-web/pull/30452)). Contributed by @dbkr.
|
||||
* New room list: fix tooltip on presence ([#30474](https://github.com/element-hq/element-web/pull/30474)). Contributed by @florianduros.
|
||||
* New room list: add tooltip for presence and room status ([#30472](https://github.com/element-hq/element-web/pull/30472)). Contributed by @florianduros.
|
||||
* Fix: Clicking on an item in the member list causes it to scroll to the top rather than show the profile view ([#30455](https://github.com/element-hq/element-web/pull/30455)). Contributed by @langleyd.
|
||||
* Put the 'decrypting' tooltip back ([#30446](https://github.com/element-hq/element-web/pull/30446)). Contributed by @dbkr.
|
||||
* Use server name explicitly for via. ([#30362](https://github.com/element-hq/element-web/pull/30362)). Contributed by @Half-Shot.
|
||||
* fix: replace hardcoded string in poll history dialog ([#30402](https://github.com/element-hq/element-web/pull/30402)). Contributed by @florianduros.
|
||||
* fix: replace hardcoded string on qr code back button ([#30401](https://github.com/element-hq/element-web/pull/30401)). Contributed by @florianduros.
|
||||
* Fix color of icon button with outline ([#30361](https://github.com/element-hq/element-web/pull/30361)). Contributed by @florianduros.
|
||||
|
||||
|
||||
Changes in [1.11.108](https://github.com/element-hq/element-web/releases/tag/v1.11.108) (2025-07-30)
|
||||
====================================================================================================
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# syntax=docker.io/docker/dockerfile:1.17-labs@sha256:9187104f31e3a002a8a6a3209ea1f937fb7486c093cbbde1e14b0fa0d7e4f1b5
|
||||
|
||||
# Builder
|
||||
FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:2d63e0f812d023c4c764e83d7e30dc94949304443ebc372d5c445e63a5ae49c1 AS builder
|
||||
FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:9e34ba52e1f3c31ed9bd4d0bcf784f5909db17cda61c220e29c8d7a8ebfb402e 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:e61b77b27c8f3124fad6d19e894ca5b603bcaf6a34a2df035511299dfa6fad35
|
||||
FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:ea6c4b8b568824ea94cd1fabd47e1c4e7c0c04744f344a3793f7e9c8ac3a3636
|
||||
|
||||
# Need root user to install packages & manipulate the usr directory
|
||||
USER root
|
||||
|
||||
@ -40,8 +40,6 @@ const config: Config = {
|
||||
"^!!raw-loader!.*": "jest-raw-loader",
|
||||
"recorderWorkletFactory": "<rootDir>/__mocks__/empty.js",
|
||||
"^fetch-mock$": "<rootDir>/node_modules/fetch-mock",
|
||||
// Requires ESM which is incompatible with our current Jest setup
|
||||
"^@element-hq/element-web-module-api$": "<rootDir>/__mocks__/empty.js",
|
||||
},
|
||||
transformIgnorePatterns: ["/node_modules/(?!(mime|matrix-js-sdk)).+$"],
|
||||
collectCoverageFrom: [
|
||||
|
||||
16
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "element-web",
|
||||
"version": "1.11.108",
|
||||
"version": "1.11.109",
|
||||
"description": "Element: the future of secure communication",
|
||||
"author": "New Vector Ltd.",
|
||||
"repository": {
|
||||
@ -75,7 +75,7 @@
|
||||
"resolutions": {
|
||||
"**/pretty-format/react-is": "19.1.1",
|
||||
"@playwright/test": "1.54.2",
|
||||
"@types/react": "19.1.9",
|
||||
"@types/react": "19.1.10",
|
||||
"@types/react-dom": "19.1.7",
|
||||
"oidc-client-ts": "3.3.0",
|
||||
"jwt-decode": "4.0.0",
|
||||
@ -86,7 +86,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@element-hq/element-web-module-api": "1.3.0",
|
||||
"@element-hq/element-web-module-api": "1.4.1",
|
||||
"@fontsource/inconsolata": "^5",
|
||||
"@fontsource/inter": "^5",
|
||||
"@formatjs/intl-segmenter": "^11.5.7",
|
||||
@ -96,7 +96,6 @@
|
||||
"@matrix-org/spec": "^1.7.0",
|
||||
"@sentry/browser": "^10.0.0",
|
||||
"@types/png-chunks-extract": "^1.0.2",
|
||||
"@types/react-virtualized": "^9.21.30",
|
||||
"@vector-im/compound-design-tokens": "^6.0.0",
|
||||
"@vector-im/compound-web": "^8.1.2",
|
||||
"@vector-im/matrix-wysiwyg": "2.39.0",
|
||||
@ -143,7 +142,7 @@
|
||||
"opus-recorder": "^8.0.3",
|
||||
"pako": "^2.0.3",
|
||||
"png-chunks-extract": "^1.0.0",
|
||||
"posthog-js": "1.257.0",
|
||||
"posthog-js": "1.260.1",
|
||||
"qrcode": "1.5.4",
|
||||
"re-resizable": "6.11.2",
|
||||
"react": "^19.0.0",
|
||||
@ -153,8 +152,7 @@
|
||||
"react-focus-lock": "^2.5.1",
|
||||
"react-string-replace": "^1.1.1",
|
||||
"react-transition-group": "^4.4.1",
|
||||
"react-virtualized": "^9.22.5",
|
||||
"react-virtuoso": "^4.12.6",
|
||||
"react-virtuoso": "^4.14.0",
|
||||
"rfc4648": "^1.4.0",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"sanitize-html": "2.17.0",
|
||||
@ -187,7 +185,7 @@
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@casualbot/jest-sonar-reporter": "2.2.7",
|
||||
"@element-hq/element-call-embedded": "0.14.1",
|
||||
"@element-hq/element-web-playwright-common": "^1.4.4",
|
||||
"@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",
|
||||
@ -223,7 +221,7 @@
|
||||
"@types/node-fetch": "^2.6.2",
|
||||
"@types/pako": "^2.0.0",
|
||||
"@types/qrcode": "^1.3.5",
|
||||
"@types/react": "19.1.9",
|
||||
"@types/react": "19.1.10",
|
||||
"@types/react-beautiful-dnd": "^13.0.0",
|
||||
"@types/react-dom": "19.1.7",
|
||||
"@types/react-transition-group": "^4.4.0",
|
||||
|
||||
@ -158,7 +158,8 @@ test.describe("Cryptography", function () {
|
||||
await page.getByRole("textbox", { name: "Send an unencrypted message…" }).press("Enter");
|
||||
await checkDMRoom(page);
|
||||
const bobRoomId = await bobJoin(page, bob);
|
||||
await expect(page.locator(".mx_MessageComposer_e2eIcon")).toMatchScreenshot("composer-e2e-icon-normal.png");
|
||||
// We no longer show the grey badge in the composer, check that it is not there.
|
||||
await expect(page.locator(".mx_MessageComposer_e2eIcon")).toHaveCount(0);
|
||||
|
||||
await testMessages(page, bob, bobRoomId);
|
||||
await verify(app, bob);
|
||||
|
||||
@ -58,108 +58,108 @@ test.describe("Cryptography", function () {
|
||||
await app.client.network.setupRoute();
|
||||
});
|
||||
|
||||
test("should show the correct shield on e2e events", async ({
|
||||
page,
|
||||
app,
|
||||
bot: bob,
|
||||
homeserver,
|
||||
}, workerInfo) => {
|
||||
// Bob has a second, not cross-signed, device
|
||||
const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob);
|
||||
test(
|
||||
"should show the correct shield on e2e events",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, bot: bob, homeserver }, workerInfo) => {
|
||||
// Bob has a second, not cross-signed, device
|
||||
const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob);
|
||||
|
||||
// Dismiss the toasts nagging us, otherwise they get in the way of clicking the room list
|
||||
await page.getByRole("button", { name: "Dismiss" }).click();
|
||||
await page.getByRole("button", { name: "Yes, dismiss" }).click();
|
||||
// Dismiss the toasts nagging us, otherwise they get in the way of clicking the room list
|
||||
await page.getByRole("button", { name: "Dismiss" }).click();
|
||||
await page.getByRole("button", { name: "Yes, dismiss" }).click();
|
||||
|
||||
await bob.sendEvent(testRoomId, null, "m.room.encrypted", {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
ciphertext: "the bird is in the hand",
|
||||
});
|
||||
await bob.sendEvent(testRoomId, null, "m.room.encrypted", {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
ciphertext: "the bird is in the hand",
|
||||
});
|
||||
|
||||
const last = page.locator(".mx_EventTile_last");
|
||||
await expect(last).toContainText("Unable to decrypt message");
|
||||
const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon");
|
||||
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_decryption_failure/);
|
||||
await lastE2eIcon.focus();
|
||||
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
|
||||
"This message could not be decrypted",
|
||||
);
|
||||
const last = page.locator(".mx_EventTile_last");
|
||||
await expect(last).toContainText("Unable to decrypt message");
|
||||
const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon");
|
||||
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_decryption_failure/);
|
||||
await lastE2eIcon.focus();
|
||||
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
|
||||
"This message could not be decrypted",
|
||||
);
|
||||
|
||||
/* Should show a red padlock for an unencrypted message in an e2e room */
|
||||
await bob.evaluate(
|
||||
(cli, testRoomId) =>
|
||||
cli.http.authedRequest(
|
||||
window.matrixcs.Method.Put,
|
||||
`/rooms/${encodeURIComponent(testRoomId)}/send/m.room.message/test_txn_1`,
|
||||
undefined,
|
||||
{
|
||||
msgtype: "m.text",
|
||||
body: "test unencrypted",
|
||||
},
|
||||
),
|
||||
testRoomId,
|
||||
);
|
||||
/* Should show a red padlock for an unencrypted message in an e2e room */
|
||||
await bob.evaluate(
|
||||
(cli, testRoomId) =>
|
||||
cli.http.authedRequest(
|
||||
window.matrixcs.Method.Put,
|
||||
`/rooms/${encodeURIComponent(testRoomId)}/send/m.room.message/test_txn_1`,
|
||||
undefined,
|
||||
{
|
||||
msgtype: "m.text",
|
||||
body: "test unencrypted",
|
||||
},
|
||||
),
|
||||
testRoomId,
|
||||
);
|
||||
|
||||
await expect(last).toContainText("test unencrypted");
|
||||
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
||||
await lastE2eIcon.focus();
|
||||
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText("Not encrypted");
|
||||
await expect(last).toContainText("test unencrypted");
|
||||
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
||||
await expect(lastE2eIcon).toMatchScreenshot("event-shield-warning.png");
|
||||
await lastE2eIcon.focus();
|
||||
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText("Not encrypted");
|
||||
|
||||
/* Should show no padlock for an unverified user */
|
||||
// bob sends a valid event
|
||||
await bob.sendMessage(testRoomId, "test encrypted 1");
|
||||
/* Should show no padlock for an unverified user */
|
||||
// bob sends a valid event
|
||||
await bob.sendMessage(testRoomId, "test encrypted 1");
|
||||
|
||||
// the message should appear, decrypted, with no warning, but also no "verified"
|
||||
const lastTile = page.locator(".mx_EventTile_last");
|
||||
const lastTileE2eIcon = lastTile.locator(".mx_EventTile_e2eIcon");
|
||||
await expect(lastTile).toContainText("test encrypted 1");
|
||||
// no e2e icon
|
||||
await expect(lastTileE2eIcon).not.toBeVisible();
|
||||
// the message should appear, decrypted, with no warning, but also no "verified"
|
||||
const lastTile = page.locator(".mx_EventTile_last");
|
||||
const lastTileE2eIcon = lastTile.locator(".mx_EventTile_e2eIcon");
|
||||
await expect(lastTile).toContainText("test encrypted 1");
|
||||
// no e2e icon
|
||||
await expect(lastTileE2eIcon).not.toBeVisible();
|
||||
|
||||
/* Now verify Bob */
|
||||
await verify(app, bob);
|
||||
/* Now verify Bob */
|
||||
await verify(app, bob);
|
||||
|
||||
/* Existing message should be updated when user is verified. */
|
||||
await expect(last).toContainText("test encrypted 1");
|
||||
// still no e2e icon
|
||||
await expect(last.locator(".mx_EventTile_e2eIcon")).not.toBeVisible();
|
||||
/* Existing message should be updated when user is verified. */
|
||||
await expect(last).toContainText("test encrypted 1");
|
||||
// still no e2e icon
|
||||
await expect(last.locator(".mx_EventTile_e2eIcon")).not.toBeVisible();
|
||||
|
||||
/* should show no padlock, and be verified, for a message from a verified device */
|
||||
await bob.sendMessage(testRoomId, "test encrypted 2");
|
||||
/* should show no padlock, and be verified, for a message from a verified device */
|
||||
await bob.sendMessage(testRoomId, "test encrypted 2");
|
||||
|
||||
await expect(lastTile).toContainText("test encrypted 2");
|
||||
// no e2e icon
|
||||
await expect(lastTileE2eIcon).not.toBeVisible();
|
||||
await expect(lastTile).toContainText("test encrypted 2");
|
||||
// no e2e icon
|
||||
await expect(lastTileE2eIcon).not.toBeVisible();
|
||||
|
||||
/* should show red padlock for a message from an unverified device */
|
||||
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from unverified");
|
||||
await expect(lastTile).toContainText("test encrypted from unverified");
|
||||
await expect(lastTileE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
||||
await lastTileE2eIcon.focus();
|
||||
await expect(await app.getTooltipForElement(lastTileE2eIcon)).toContainText(
|
||||
"Encrypted by a device not verified by its owner.",
|
||||
);
|
||||
/* should show red padlock for a message from an unverified device */
|
||||
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from unverified");
|
||||
await expect(lastTile).toContainText("test encrypted from unverified");
|
||||
await expect(lastTileE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
||||
await lastTileE2eIcon.focus();
|
||||
await expect(await app.getTooltipForElement(lastTileE2eIcon)).toContainText(
|
||||
"Encrypted by a device not verified by its owner.",
|
||||
);
|
||||
|
||||
/* Should show a red padlock for a message from an unverified device.
|
||||
* Rust crypto remembers the verification state of the sending device, so it will know that the device was
|
||||
* unverified, even if it gets deleted. */
|
||||
// bob deletes his second device
|
||||
await bobSecondDevice.evaluate((cli) => cli.logout(true));
|
||||
/* Should show a red padlock for a message from an unverified device.
|
||||
* Rust crypto remembers the verification state of the sending device, so it will know that the device was
|
||||
* unverified, even if it gets deleted. */
|
||||
// bob deletes his second device
|
||||
await bobSecondDevice.evaluate((cli) => cli.logout(true));
|
||||
|
||||
// wait for the logout to propagate.
|
||||
await waitForDevices(app, bob.credentials.userId, 1);
|
||||
// wait for the logout to propagate.
|
||||
await waitForDevices(app, bob.credentials.userId, 1);
|
||||
|
||||
// close and reopen the room, to get the shield to update.
|
||||
await app.viewRoomByName("Bob");
|
||||
await app.viewRoomByName("TestRoom");
|
||||
// close and reopen the room, to get the shield to update.
|
||||
await app.viewRoomByName("Bob");
|
||||
await app.viewRoomByName("TestRoom");
|
||||
|
||||
await expect(last).toContainText("test encrypted from unverified");
|
||||
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
||||
await lastE2eIcon.focus();
|
||||
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
|
||||
"Encrypted by a device not verified by its owner.",
|
||||
);
|
||||
});
|
||||
await expect(last).toContainText("test encrypted from unverified");
|
||||
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
||||
await lastE2eIcon.focus();
|
||||
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
|
||||
"Encrypted by a device not verified by its owner.",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test("Should show a grey padlock for a key restored from backup", async ({
|
||||
page,
|
||||
|
||||
@ -68,7 +68,7 @@ test.describe("Room list filters and sort", () => {
|
||||
So we expect 'Old Room' to show up in the room list.
|
||||
*/
|
||||
const roomListView = getRoomList(page);
|
||||
const oldRoomTile = roomListView.getByRole("gridcell", { name: "Open room Old Room" });
|
||||
const oldRoomTile = roomListView.getByRole("option", { name: "Open room Old Room" });
|
||||
await expect(oldRoomTile).toBeVisible();
|
||||
|
||||
/*
|
||||
@ -139,8 +139,9 @@ test.describe("Room list filters and sort", () => {
|
||||
|
||||
// Open the non-favourite room
|
||||
const roomListView = getRoomList(page);
|
||||
const tile = roomListView.getByRole("gridcell", { name: "Open room room-non-fav" });
|
||||
await tile.scrollIntoViewIfNeeded();
|
||||
const tile = roomListView.getByRole("option", { name: "Open room room-non-fav" });
|
||||
// item may not be in the DOM using scrollListToBottom rather than scrollIntoViewIfNeeded
|
||||
await app.scrollListToBottom(roomListView);
|
||||
await tile.click();
|
||||
|
||||
// Enable Favourite filter
|
||||
@ -151,7 +152,7 @@ test.describe("Room list filters and sort", () => {
|
||||
|
||||
// Ensure the room list is not scrolled
|
||||
const isScrolledDown = await page
|
||||
.getByRole("grid", { name: "Room list" })
|
||||
.getByRole("listbox", { name: "Room list", exact: true })
|
||||
.evaluate((e) => e.scrollTop !== 0);
|
||||
expect(isScrolledDown).toStrictEqual(false);
|
||||
});
|
||||
@ -227,37 +228,37 @@ test.describe("Room list filters and sort", () => {
|
||||
|
||||
await primaryFilters.getByRole("option", { name: "Unread" }).click();
|
||||
// only one room should be visible
|
||||
await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible();
|
||||
await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible();
|
||||
await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(4);
|
||||
await expect(roomList.getByRole("option", { name: "unread dm" })).toBeVisible();
|
||||
await expect(roomList.getByRole("option", { name: "unread room" })).toBeVisible();
|
||||
await expect.poll(() => roomList.locator("role=option").count()).toBe(4);
|
||||
await expect(primaryFilters).toMatchScreenshot("unread-primary-filters.png");
|
||||
|
||||
await primaryFilters.getByRole("option", { name: "People" }).click();
|
||||
await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible();
|
||||
await expect(roomList.getByRole("gridcell", { name: "invited room" })).toBeVisible();
|
||||
await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(2);
|
||||
await expect(roomList.getByRole("option", { name: "unread dm" })).toBeVisible();
|
||||
await expect(roomList.getByRole("option", { name: "invited room" })).toBeVisible();
|
||||
await expect.poll(() => roomList.locator("role=option").count()).toBe(2);
|
||||
|
||||
await primaryFilters.getByRole("option", { name: "Rooms" }).click();
|
||||
await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible();
|
||||
await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible();
|
||||
await expect(roomList.getByRole("gridcell", { name: "empty room" })).toBeVisible();
|
||||
await expect(roomList.getByRole("gridcell", { name: "room with mention" })).toBeVisible();
|
||||
await expect(roomList.getByRole("gridcell", { name: "Low prio room" })).toBeVisible();
|
||||
await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(5);
|
||||
await expect(roomList.getByRole("option", { name: "unread room" })).toBeVisible();
|
||||
await expect(roomList.getByRole("option", { name: "favourite room" })).toBeVisible();
|
||||
await expect(roomList.getByRole("option", { name: "empty room" })).toBeVisible();
|
||||
await expect(roomList.getByRole("option", { name: "room with mention" })).toBeVisible();
|
||||
await expect(roomList.getByRole("option", { name: "Low prio room" })).toBeVisible();
|
||||
await expect.poll(() => roomList.locator("role=option").count()).toBe(5);
|
||||
|
||||
await getFilterExpandButton(page).click();
|
||||
|
||||
await primaryFilters.getByRole("option", { name: "Favourite" }).click();
|
||||
await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible();
|
||||
await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(1);
|
||||
await expect(roomList.getByRole("option", { name: "favourite room" })).toBeVisible();
|
||||
await expect.poll(() => roomList.locator("role=option").count()).toBe(1);
|
||||
|
||||
await primaryFilters.getByRole("option", { name: "Mentions" }).click();
|
||||
await expect(roomList.getByRole("gridcell", { name: "room with mention" })).toBeVisible();
|
||||
await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(1);
|
||||
await expect(roomList.getByRole("option", { name: "room with mention" })).toBeVisible();
|
||||
await expect.poll(() => roomList.locator("role=option").count()).toBe(1);
|
||||
|
||||
await primaryFilters.getByRole("option", { name: "Invites" }).click();
|
||||
await expect(roomList.getByRole("gridcell", { name: "invited room" })).toBeVisible();
|
||||
await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(1);
|
||||
await expect(roomList.getByRole("option", { name: "invited room" })).toBeVisible();
|
||||
await expect.poll(() => roomList.locator("role=option").count()).toBe(1);
|
||||
|
||||
await getFilterCollapseButton(page).click();
|
||||
await expect(primaryFilters.locator("role=option").first()).toHaveText("Invites");
|
||||
@ -268,6 +269,7 @@ test.describe("Room list filters and sort", () => {
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, bot }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
const primaryFilters = getPrimaryFilters(page);
|
||||
|
||||
// Let's configure unread dm room so that we only get notification for mentions and keywords
|
||||
await app.viewRoomById(unReadDmId);
|
||||
@ -276,20 +278,20 @@ test.describe("Room list filters and sort", () => {
|
||||
await app.settings.closeDialog();
|
||||
|
||||
// Let's open a room other than unread room or unread dm
|
||||
await roomListView.getByRole("gridcell", { name: "Open room favourite room" }).click();
|
||||
await roomListView.getByRole("option", { name: "Open room favourite room" }).click();
|
||||
|
||||
// Let's make the bot send a new message in both rooms
|
||||
await bot.sendMessage(unReadDmId, "Hello!");
|
||||
await bot.sendMessage(unReadRoomId, "Hello!");
|
||||
|
||||
// Let's activate the unread filter now
|
||||
await page.getByRole("option", { name: "Unread" }).click();
|
||||
await primaryFilters.getByRole("option", { name: "Unread" }).click();
|
||||
|
||||
// Unread filter should only show unread room and not unread dm!
|
||||
const unreadDm = roomListView.getByRole("gridcell", { name: "Open room unread room" });
|
||||
const unreadDm = roomListView.getByRole("option", { name: "Open room unread room" });
|
||||
await expect(unreadDm).toBeVisible();
|
||||
await expect(unreadDm).toMatchScreenshot("unread-dm.png");
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room unread dm" })).not.toBeVisible();
|
||||
await expect(roomListView.getByRole("option", { name: "Open room unread dm" })).not.toBeVisible();
|
||||
},
|
||||
);
|
||||
|
||||
@ -299,7 +301,7 @@ test.describe("Room list filters and sort", () => {
|
||||
await getRoomOptionsMenu(page).click();
|
||||
await page.getByRole("menuitemradio", { name: "A-Z" }).click();
|
||||
|
||||
await expect(roomListView.getByRole("gridcell").first()).toHaveText(/empty room/);
|
||||
await expect(roomListView.getByRole("option").first()).toHaveText(/empty room/);
|
||||
});
|
||||
|
||||
test("should move room to the top on message when sorting by activity", async ({ page, bot }) => {
|
||||
@ -307,7 +309,7 @@ test.describe("Room list filters and sort", () => {
|
||||
|
||||
await bot.sendMessage(unReadDmId, "Hello!");
|
||||
|
||||
await expect(roomListView.getByRole("gridcell").first()).toHaveText(/unread dm/);
|
||||
await expect(roomListView.getByRole("option").first()).toHaveText(/unread dm/);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -38,7 +38,7 @@ test.describe("Room list panel", () => {
|
||||
test("should render the room list panel", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomListView = getRoomListView(page);
|
||||
// Wait for the last room to be visible
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room room19" })).toBeVisible();
|
||||
await expect(roomListView.getByRole("option", { name: "Open room room19" })).toBeVisible();
|
||||
await expect(roomListView).toMatchScreenshot("room-list-panel.png");
|
||||
});
|
||||
|
||||
|
||||
@ -43,31 +43,35 @@ test.describe("Room list", () => {
|
||||
|
||||
test("should render the room list", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room room29" })).toBeVisible();
|
||||
await expect(roomListView.getByRole("option", { name: "Open room room29" })).toBeVisible();
|
||||
await expect(roomListView).toMatchScreenshot("room-list.png");
|
||||
|
||||
// Put focus on the room list
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
|
||||
await roomListView.getByRole("option", { name: "Open room room29" }).click();
|
||||
// Scroll to the end of the room list
|
||||
await app.scrollListToBottom(page.locator(".mx_RoomList_List"));
|
||||
await app.scrollListToBottom(roomListView);
|
||||
|
||||
// scrollListToBottom seems to leave the mouse hovered over the list, move it away.
|
||||
await page.getByRole("button", { name: "User menu" }).hover();
|
||||
|
||||
await expect(roomListView).toMatchScreenshot("room-list-scrolled.png");
|
||||
});
|
||||
|
||||
test("should open the room when it is clicked", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
|
||||
await roomListView.getByRole("option", { name: "Open room room29" }).click();
|
||||
await expect(page.getByRole("heading", { name: "room29", level: 1 })).toBeVisible();
|
||||
});
|
||||
|
||||
test("should open the context menu", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click({ button: "right" });
|
||||
await roomListView.getByRole("option", { name: "Open room room29" }).click({ button: "right" });
|
||||
await expect(page.getByRole("menu", { name: "More Options" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("should open the more options menu", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
const roomItem = roomListView.getByRole("gridcell", { name: "Open room room29" });
|
||||
const roomItem = roomListView.getByRole("option", { name: "Open room room29" });
|
||||
await roomItem.hover();
|
||||
|
||||
await expect(roomItem).toMatchScreenshot("room-list-item-hover.png");
|
||||
@ -97,7 +101,7 @@ test.describe("Room list", () => {
|
||||
test("should open the notification options menu", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
const roomItem = roomListView.getByRole("gridcell", { name: "Open room room29" });
|
||||
const roomItem = roomListView.getByRole("option", { name: "Open room room29" });
|
||||
await roomItem.hover();
|
||||
|
||||
await expect(roomItem).toMatchScreenshot("room-list-item-hover.png");
|
||||
@ -117,10 +121,10 @@ test.describe("Room list", () => {
|
||||
await expect(roomItem.getByTestId("notification-decoration")).not.toBeVisible();
|
||||
|
||||
// Put focus on the room list
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room28" }).click();
|
||||
await roomListView.getByRole("option", { name: "Open room room28" }).click();
|
||||
|
||||
// Scroll to the end of the room list
|
||||
await app.scrollListToBottom(page.locator(".mx_RoomList_List"));
|
||||
await app.scrollListToBottom(roomListView);
|
||||
|
||||
// The room decoration should have the muted icon
|
||||
await expect(roomItem.getByTestId("notification-decoration")).toBeVisible();
|
||||
@ -139,25 +143,25 @@ test.describe("Room list", () => {
|
||||
test("should scroll to the current room", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
// Put focus on the room list
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
|
||||
await roomListView.getByRole("option", { name: "Open room room29" }).click();
|
||||
// Scroll to the end of the room list
|
||||
await app.scrollListToBottom(page.locator(".mx_RoomList_List"));
|
||||
await app.scrollListToBottom(roomListView);
|
||||
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room0" }).click();
|
||||
await expect(roomListView.getByRole("option", { name: "Open room room0" })).toBeVisible();
|
||||
await roomListView.getByRole("option", { name: "Open room room0" }).click();
|
||||
|
||||
const filters = page.getByRole("listbox", { name: "Room list filters" });
|
||||
await filters.getByRole("option", { name: "People" }).click();
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).not.toBeVisible();
|
||||
await expect(roomListView.getByRole("option", { name: "Open room room0" })).not.toBeVisible();
|
||||
|
||||
await filters.getByRole("option", { name: "People" }).click();
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
|
||||
await expect(roomListView.getByRole("option", { name: "Open room room0" })).toBeVisible();
|
||||
});
|
||||
|
||||
test.describe("Shortcuts", () => {
|
||||
test("should select the next room", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
|
||||
await roomListView.getByRole("option", { name: "Open room room29" }).click();
|
||||
await page.keyboard.press("Alt+ArrowDown");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "room28", level: 1 })).toBeVisible();
|
||||
@ -165,7 +169,7 @@ test.describe("Room list", () => {
|
||||
|
||||
test("should select the previous room", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room28" }).click();
|
||||
await roomListView.getByRole("option", { name: "Open room room28" }).click();
|
||||
await page.keyboard.press("Alt+ArrowUp");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "room29", level: 1 })).toBeVisible();
|
||||
@ -173,7 +177,7 @@ test.describe("Room list", () => {
|
||||
|
||||
test("should select the last room", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
|
||||
await roomListView.getByRole("option", { name: "Open room room29" }).click();
|
||||
await page.keyboard.press("Alt+ArrowUp");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "room0", level: 1 })).toBeVisible();
|
||||
@ -187,7 +191,7 @@ test.describe("Room list", () => {
|
||||
await bot.joinRoom(roomId);
|
||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room20" }).click();
|
||||
await roomListView.getByRole("option", { name: "Open room room20" }).click();
|
||||
|
||||
await page.keyboard.press("Alt+Shift+ArrowDown");
|
||||
|
||||
@ -199,8 +203,8 @@ test.describe("Room list", () => {
|
||||
test("should navigate to the room list", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
const room29 = roomListView.getByRole("gridcell", { name: "Open room room29" });
|
||||
const room28 = roomListView.getByRole("gridcell", { name: "Open room room28" });
|
||||
const room29 = roomListView.getByRole("option", { name: "Open room room29" });
|
||||
const room28 = roomListView.getByRole("option", { name: "Open room room28" });
|
||||
|
||||
// open the room
|
||||
await room29.click();
|
||||
@ -219,7 +223,7 @@ test.describe("Room list", () => {
|
||||
|
||||
test("should navigate to the notification menu", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
const room29 = roomListView.getByRole("gridcell", { name: "Open room room29" });
|
||||
const room29 = roomListView.getByRole("option", { name: "Open room room29" });
|
||||
const moreButton = room29.getByRole("button", { name: "More options" });
|
||||
const notificationButton = room29.getByRole("button", { name: "Notification options" });
|
||||
|
||||
@ -258,7 +262,7 @@ test.describe("Room list", () => {
|
||||
await page.getByRole("button", { name: "User menu" }).focus();
|
||||
|
||||
const roomListView = getRoomList(page);
|
||||
const publicRoom = roomListView.getByRole("gridcell", { name: "public room" });
|
||||
const publicRoom = roomListView.getByRole("option", { name: "public room" });
|
||||
|
||||
await expect(publicRoom).toBeVisible();
|
||||
await expect(publicRoom).toMatchScreenshot("room-list-item-public.png");
|
||||
@ -268,7 +272,7 @@ test.describe("Room list", () => {
|
||||
// @ts-ignore Visibility enum is not accessible
|
||||
await app.client.createRoom({ name: "low priority room", visibility: "public" });
|
||||
const roomListView = getRoomList(page);
|
||||
const publicRoom = roomListView.getByRole("gridcell", { name: "low priority room" });
|
||||
const publicRoom = roomListView.getByRole("option", { name: "low priority room" });
|
||||
|
||||
// Make room low priority
|
||||
await publicRoom.hover();
|
||||
@ -293,7 +297,7 @@ test.describe("Room list", () => {
|
||||
await page.getByRole("button", { name: "Create video room" }).click();
|
||||
|
||||
const roomListView = getRoomList(page);
|
||||
const videoRoom = roomListView.getByRole("gridcell", { name: "video room" });
|
||||
const videoRoom = roomListView.getByRole("option", { name: "video room" });
|
||||
|
||||
// focus the user menu to avoid to have hover decoration
|
||||
await page.getByRole("button", { name: "User menu" }).focus();
|
||||
@ -312,7 +316,7 @@ test.describe("Room list", () => {
|
||||
invite: [user.userId],
|
||||
is_direct: true,
|
||||
});
|
||||
const invitedRoom = roomListView.getByRole("gridcell", { name: "invited room" });
|
||||
const invitedRoom = roomListView.getByRole("option", { name: "invited room" });
|
||||
await expect(invitedRoom).toBeVisible();
|
||||
await expect(invitedRoom).toMatchScreenshot("room-list-item-invited.png");
|
||||
});
|
||||
@ -327,7 +331,7 @@ test.describe("Room list", () => {
|
||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||
|
||||
const room = roomListView.getByRole("gridcell", { name: "2 notifications" });
|
||||
const room = roomListView.getByRole("option", { name: "2 notifications" });
|
||||
await expect(room).toBeVisible();
|
||||
await expect(room.getByTestId("notification-decoration")).toHaveText("2");
|
||||
await expect(room).toMatchScreenshot("room-list-item-notification.png");
|
||||
@ -358,7 +362,7 @@ test.describe("Room list", () => {
|
||||
);
|
||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||
|
||||
const room = roomListView.getByRole("gridcell", { name: "mention" });
|
||||
const room = roomListView.getByRole("option", { name: "mention" });
|
||||
await expect(room).toBeVisible();
|
||||
await expect(room).toMatchScreenshot("room-list-item-mention.png");
|
||||
});
|
||||
@ -379,7 +383,7 @@ test.describe("Room list", () => {
|
||||
await bot.joinRoom(roomId);
|
||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||
|
||||
const room = roomListView.getByRole("gridcell", { name: "activity" });
|
||||
const room = roomListView.getByRole("option", { name: "activity" });
|
||||
await expect(room.getByText("I am a robot. Beep.")).toBeVisible();
|
||||
await expect(room).toMatchScreenshot("room-list-item-message-preview.png");
|
||||
});
|
||||
@ -406,7 +410,7 @@ test.describe("Room list", () => {
|
||||
await app.viewRoomById(otherRoomId);
|
||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||
|
||||
const room = roomListView.getByRole("gridcell", { name: "activity" });
|
||||
const room = roomListView.getByRole("option", { name: "activity" });
|
||||
await expect(room.getByTestId("notification-decoration")).toBeVisible();
|
||||
await expect(room).toMatchScreenshot("room-list-item-activity.png");
|
||||
});
|
||||
@ -418,7 +422,7 @@ test.describe("Room list", () => {
|
||||
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||
await bot.joinRoom(roomId);
|
||||
|
||||
const room = roomListView.getByRole("gridcell", { name: "mark as unread" });
|
||||
const room = roomListView.getByRole("option", { name: "mark as unread" });
|
||||
await room.hover();
|
||||
await room.getByRole("button", { name: "More Options" }).click();
|
||||
await page.getByRole("menuitem", { name: "mark as unread" }).click();
|
||||
@ -441,7 +445,7 @@ test.describe("Room list", () => {
|
||||
await page.getByText("Off").click();
|
||||
await app.settings.closeDialog();
|
||||
|
||||
const room = roomListView.getByRole("gridcell", { name: "silent" });
|
||||
const room = roomListView.getByRole("option", { name: "silent" });
|
||||
await expect(room.getByTestId("notification-decoration")).toBeVisible();
|
||||
await expect(room).toMatchScreenshot("room-list-item-silent.png");
|
||||
});
|
||||
|
||||
@ -95,10 +95,6 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
const result = await mas.manage("kill-sessions", userId);
|
||||
expect(result.output).toContain("Ended 1 active OAuth 2.0 session");
|
||||
|
||||
// Workaround for Synapse's 2 minute cache on MAS token validity
|
||||
// (https://github.com/element-hq/synapse/pull/18231)
|
||||
await homeserver.restart();
|
||||
|
||||
await page.goto("http://localhost:8080");
|
||||
await expect(
|
||||
page.getByText("For security, this session has been signed out. Please sign in again."),
|
||||
|
||||
@ -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 Download, type Page } from "@playwright/test";
|
||||
import { type Page } from "@playwright/test";
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { viewRoomSummaryByName } from "./utils";
|
||||
@ -189,23 +189,13 @@ test.describe("FilePanel", () => {
|
||||
|
||||
const link = imageBody.locator(".mx_MFileBody_download a");
|
||||
|
||||
const newPagePromise = context.waitForEvent("page");
|
||||
|
||||
const downloadPromise = new Promise<Download>((resolve) => {
|
||||
page.once("download", resolve);
|
||||
});
|
||||
const downloadPromise = page.waitForEvent("download");
|
||||
|
||||
// Click the anchor link (not the image itself)
|
||||
await link.click();
|
||||
|
||||
const newPage = await newPagePromise;
|
||||
// XXX: Clicking the link opens the image in a new tab on some browsers rather than downloading
|
||||
await expect(newPage)
|
||||
.toHaveURL(/.+\/_matrix\/media\/\w+\/download\/localhost\/\w+/)
|
||||
.catch(async () => {
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toBe("riot.png");
|
||||
});
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toBe("riot.png");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -37,11 +37,8 @@ test.describe("Room Header", () => {
|
||||
await expect(header.locator(".mx_FacePile")).toBeVisible();
|
||||
|
||||
// There should be both a voice and a video call button
|
||||
// but they'll be disabled
|
||||
const callButtons = header.getByRole("button", { name: "There's no one here to call" });
|
||||
await expect(callButtons).toHaveCount(2);
|
||||
await expect(callButtons.first()).toBeVisible();
|
||||
await expect(callButtons.last()).toBeVisible();
|
||||
await expect(header.getByRole("button", { name: "Video call" })).toBeVisible();
|
||||
await expect(header.getByRole("button", { name: "Voice call" })).toBeVisible();
|
||||
|
||||
await expect(header.getByRole("button", { name: "Threads" })).toBeVisible();
|
||||
await expect(header.getByRole("button", { name: "Notifications" })).toBeVisible();
|
||||
|
||||
@ -18,13 +18,14 @@ test.describe("share from URL", () => {
|
||||
|
||||
test("should share message when users navigates to share URL", async ({ page, user, room, app }) => {
|
||||
await page.goto("/#/share?msg=Hello+world");
|
||||
const dialog = page.getByRole("dialog", { name: "Forward message" });
|
||||
// The forward message dialog doesn't update as new infomation arrives via sync, which means sometimes
|
||||
// this is just says, "Empty room". For the same reason, we can't reliably write a test for loading the
|
||||
// app straight away with a /#/share url as the room doesn't appear until the client syncs.]
|
||||
// Ideally we should fix the forward dialog to update and eliminate races, until then, there is only one
|
||||
// room so we click the first button.
|
||||
await page.getByRole("listitem" /*, { name: "A test room" }*/).getByRole("button", { name: "Send" }).click();
|
||||
await page.keyboard.press("Escape");
|
||||
await dialog.getByRole("listitem" /*, { name: "A test room" }*/).getByRole("button", { name: "Send" }).click();
|
||||
await dialog.getByRole("button", { name: "Close" }).click();
|
||||
await app.viewRoomByName("A test room");
|
||||
const lastMessage = page.locator(".mx_RoomView_MessageList .mx_EventTile_last");
|
||||
await expect(lastMessage).toBeVisible();
|
||||
|
||||
@ -1,38 +1,49 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024-2025 New Vector Ltd.
|
||||
Copyright 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 { MatrixAuthenticationServiceContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers";
|
||||
|
||||
import { MatrixAuthenticationServiceContainer } from "../../../testcontainers/mas.ts";
|
||||
import { type Fixtures } from "../../../element-web-test.ts";
|
||||
|
||||
export const masHomeserver: Fixtures = {
|
||||
mas: [
|
||||
async ({ _homeserver: homeserver, logger, network, postgres, mailpit }, use) => {
|
||||
const config = {
|
||||
clients: [
|
||||
{
|
||||
client_id: "0000000000000000000SYNAPSE",
|
||||
client_auth_method: "client_secret_basic",
|
||||
client_secret: "SomeRandomSecret",
|
||||
},
|
||||
],
|
||||
matrix: {
|
||||
homeserver: "localhost",
|
||||
secret: "AnotherRandomSecret",
|
||||
endpoint: "http://homeserver:8008",
|
||||
},
|
||||
};
|
||||
const secret = "AnotherRandomSecret";
|
||||
|
||||
const limits = { burst: 10, per_second: 10 };
|
||||
const container = await new MatrixAuthenticationServiceContainer(postgres)
|
||||
.withNetwork(network)
|
||||
.withNetworkAliases("mas")
|
||||
.withLogConsumer(logger.getConsumer("mas"))
|
||||
.withConfig(config)
|
||||
.withConfig({
|
||||
matrix: {
|
||||
kind: "synapse",
|
||||
homeserver: "localhost",
|
||||
secret,
|
||||
endpoint: "http://homeserver:8008",
|
||||
},
|
||||
rate_limiting: {
|
||||
login: {
|
||||
per_ip: limits,
|
||||
per_account: limits,
|
||||
},
|
||||
registration: limits,
|
||||
email_authentication: {
|
||||
per_ip: limits,
|
||||
per_address: limits,
|
||||
emails_per_session: limits,
|
||||
attempt_per_session: limits,
|
||||
},
|
||||
account_recovery: {
|
||||
per_ip: limits,
|
||||
per_address: limits,
|
||||
},
|
||||
},
|
||||
})
|
||||
.start();
|
||||
|
||||
homeserver.withConfig({
|
||||
@ -40,16 +51,10 @@ export const masHomeserver: Fixtures = {
|
||||
enable_registration_without_verification: undefined,
|
||||
disable_msisdn_registration: undefined,
|
||||
password_config: undefined,
|
||||
experimental_features: {
|
||||
msc3861: {
|
||||
enabled: true,
|
||||
issuer: `http://mas:8080/`,
|
||||
introspection_endpoint: "http://mas:8080/oauth2/introspect",
|
||||
client_id: config.clients[0].client_id,
|
||||
client_auth_method: config.clients[0].client_auth_method,
|
||||
client_secret: config.clients[0].client_secret,
|
||||
admin_token: config.matrix.secret,
|
||||
},
|
||||
matrix_authentication_service: {
|
||||
enabled: true,
|
||||
endpoint: "http://mas:8080/",
|
||||
secret,
|
||||
},
|
||||
});
|
||||
|
||||
@ -59,28 +64,6 @@ export const masHomeserver: Fixtures = {
|
||||
{ scope: "worker" },
|
||||
],
|
||||
|
||||
config: async ({ homeserver, context, mas }, use) => {
|
||||
const issuer = `${mas.baseUrl}/`;
|
||||
const wellKnown = {
|
||||
"m.homeserver": {
|
||||
base_url: homeserver.baseUrl,
|
||||
},
|
||||
"org.matrix.msc2965.authentication": {
|
||||
issuer,
|
||||
account: `${issuer}account`,
|
||||
},
|
||||
};
|
||||
|
||||
// Ensure org.matrix.msc2965.authentication is in well-known
|
||||
await context.route("https://localhost/.well-known/matrix/client", async (route) => {
|
||||
await route.fulfill({ json: wellKnown });
|
||||
});
|
||||
|
||||
await use({
|
||||
default_server_config: wellKnown,
|
||||
});
|
||||
},
|
||||
|
||||
context: async ({ homeserverType, context }, use, testInfo) => {
|
||||
testInfo.skip(homeserverType !== "synapse", "does not yet support MAS");
|
||||
await use(context);
|
||||
|
||||
|
Before Width: | Height: | Size: 268 B |
|
After Width: | Height: | Size: 406 B |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 19 KiB |
24
playwright/testcontainers/mas.ts
Normal file
@ -0,0 +1,24 @@
|
||||
/*
|
||||
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 {
|
||||
MatrixAuthenticationServiceContainer as BaseMatrixAuthenticationServiceContainer,
|
||||
type StartedPostgreSqlContainer,
|
||||
} from "@element-hq/element-web-playwright-common/lib/testcontainers";
|
||||
|
||||
const TAG = "main@sha256:e1004d0213a985064766c99df344b0ac799869aff503be5aba1a720478258873";
|
||||
|
||||
/**
|
||||
* MatrixAuthenticationServiceContainer which freezes the docker digest to
|
||||
* stabilise tests, updated periodically by the `playwright-image-updates.yaml`
|
||||
* workflow.
|
||||
*/
|
||||
export class MatrixAuthenticationServiceContainer extends BaseMatrixAuthenticationServiceContainer {
|
||||
public constructor(db: StartedPostgreSqlContainer) {
|
||||
super(db, `ghcr.io/element-hq/matrix-authentication-service:${TAG}`);
|
||||
}
|
||||
}
|
||||
@ -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:b67c77a7ccb360afa16f037243d52920a47fc0648f7bbdc76ff551ec75fc2b6f";
|
||||
const TAG = "develop@sha256:78b32934c4a7a616ada0a0af6e6ba3a102f97ff747fc0d2d1e3bd930e16e529e";
|
||||
|
||||
/**
|
||||
* SynapseContainer which freezes the docker digest to stabilise tests,
|
||||
|
||||
@ -142,6 +142,7 @@
|
||||
@import "./views/dialogs/_GenericFeatureFeedbackDialog.pcss";
|
||||
@import "./views/dialogs/_IncomingSasDialog.pcss";
|
||||
@import "./views/dialogs/_InviteDialog.pcss";
|
||||
@import "./views/dialogs/_InviteProgressBody.pcss";
|
||||
@import "./views/dialogs/_JoinRuleDropdown.pcss";
|
||||
@import "./views/dialogs/_LeaveSpaceDialog.pcss";
|
||||
@import "./views/dialogs/_LocationViewDialog.pcss";
|
||||
|
||||
@ -12,31 +12,39 @@ Please see LICENSE files in the repository root for full details.
|
||||
* These are defined in `rem` so that they scale with the `font-size` of the root element (which is adjustable via the
|
||||
* "Font size" setting). They exist to make the job of converting designs (which tend to be based in pixels) into CSS
|
||||
* easier.
|
||||
*/
|
||||
|
||||
/*
|
||||
* These variables are now *deprecated* and should not be used in new code; instead Compound typographic tokens
|
||||
* should be used. Direct equivalents for these old font size tokens are listed below; where no equivalent exists,
|
||||
* that suggests that the design is using a non-standard font size and should be updated.
|
||||
*
|
||||
* In fact, modern Figma designs should actually use a named Typography style such as "Web/font/heading/sm/semibold",
|
||||
* translates directly to `font: var(--cpd-font-heading-sm-semibold)`.
|
||||
*/
|
||||
$font-1px: 0.0625rem;
|
||||
$font-8px: 0.5rem;
|
||||
$font-9px: 0.5625rem;
|
||||
$font-10px: 0.625rem;
|
||||
$font-10-4px: 0.6275rem;
|
||||
$font-11px: 0.6875rem;
|
||||
$font-11px: 0.6875rem; /* Compound equivalent: --cpd-font-size-body-xs */
|
||||
$font-12px: 0.75rem;
|
||||
$font-13px: 0.8125rem;
|
||||
$font-13px: 0.8125rem; /* Compound equivalent: --cpd-font-size-body-sm */
|
||||
$font-14px: 0.875rem;
|
||||
$font-15px: 0.9375rem;
|
||||
$font-15px: 0.9375rem; /* Compound equivalent: --cpd-font-size-body-md */
|
||||
$font-16px: 1rem;
|
||||
$font-17px: 1.0625rem;
|
||||
$font-17px: 1.0625rem; /* Compound equivalent: --cpd-font-size-body-lg */
|
||||
$font-18px: 1.125rem;
|
||||
$font-20px: 1.25rem;
|
||||
$font-20px: 1.25rem; /* Compound equivalent: --cpd-font-size-heading-sm */
|
||||
$font-22px: 1.375rem;
|
||||
$font-23px: 1.4375rem;
|
||||
$font-24px: 1.5rem;
|
||||
$font-24px: 1.5rem; /* Compound equivalent: --cpd-font-size-heading-md */
|
||||
$font-25px: 1.5625rem;
|
||||
$font-26px: 1.625rem;
|
||||
$font-28px: 1.75rem;
|
||||
$font-28px: 1.75rem; /* Compound equivalent: --cpd-font-size-heading-lg */
|
||||
$font-29px: 1.8125rem;
|
||||
$font-30px: 1.875rem;
|
||||
$font-32px: 2rem;
|
||||
$font-32px: 2rem; /* Compound equivalent: --cpd-font-size-heading-xl */
|
||||
$font-34px: 2.125rem;
|
||||
$font-35px: 2.1875rem;
|
||||
$font-39px: 2.4375rem;
|
||||
|
||||
@ -63,17 +63,6 @@ Please see LICENSE files in the repository root for full details.
|
||||
height: 25px;
|
||||
line-height: $font-25px;
|
||||
}
|
||||
|
||||
.mx_InviteDialog_buttonAndSpinner {
|
||||
.mx_Spinner {
|
||||
/* Width and height are required to trick the layout engine. */
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-inline-start: 5px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_InviteDialog_section {
|
||||
@ -218,6 +207,10 @@ Please see LICENSE files in the repository root for full details.
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.mx_InviteProgressBody {
|
||||
margin-top: var(--cpd-space-12x);
|
||||
}
|
||||
}
|
||||
|
||||
.mx_InviteDialog_transfer {
|
||||
|
||||
16
res/css/views/dialogs/_InviteProgressBody.pcss
Normal file
@ -0,0 +1,16 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
.mx_InviteProgressBody {
|
||||
text-align: center;
|
||||
font: var(--cpd-font-body-lg-regular);
|
||||
|
||||
h1 {
|
||||
color: var(--cpd-color-text-primary);
|
||||
font: var(--cpd-font-heading-sm-semibold);
|
||||
}
|
||||
}
|
||||
@ -15,40 +15,44 @@
|
||||
* |-------------------------------------------------------|
|
||||
*/
|
||||
.mx_RoomListItemView {
|
||||
all: unset;
|
||||
/* Remove button default style */
|
||||
background: unset;
|
||||
border: none;
|
||||
padding: 0;
|
||||
text-align: unset;
|
||||
|
||||
cursor: pointer;
|
||||
height: 48px;
|
||||
width: 100%;
|
||||
|
||||
.mx_RoomListItemView_container {
|
||||
padding-left: var(--cpd-space-3x);
|
||||
font: var(--cpd-font-body-md-regular);
|
||||
padding-left: var(--cpd-space-3x);
|
||||
font: var(--cpd-font-body-md-regular);
|
||||
|
||||
.mx_RoomListItemView_content {
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
/* The border is only under the room name and the future hover menu */
|
||||
border-bottom: var(--cpd-border-width-0-5) solid var(--cpd-color-bg-subtle-secondary);
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
padding-right: var(--cpd-space-5x);
|
||||
|
||||
.mx_RoomListItemView_content {
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
/* The border is only under the room name and the future hover menu */
|
||||
border-bottom: var(--cpd-border-width-0-5) solid var(--cpd-color-bg-subtle-secondary);
|
||||
box-sizing: border-box;
|
||||
.mx_RoomListItemView_text {
|
||||
min-width: 0;
|
||||
padding-right: var(--cpd-space-5x);
|
||||
}
|
||||
|
||||
.mx_RoomListItemView_text {
|
||||
min-width: 0;
|
||||
}
|
||||
.mx_RoomListItemView_roomName {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.mx_RoomListItemView_roomName {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.mx_RoomListItemView_messagePreview {
|
||||
font: var(--cpd-font-body-sm-regular);
|
||||
color: var(--cpd-color-text-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.mx_RoomListItemView_messagePreview {
|
||||
font: var(--cpd-font-body-sm-regular);
|
||||
color: var(--cpd-color-text-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -57,7 +61,7 @@
|
||||
background-color: var(--cpd-color-bg-action-secondary-hovered);
|
||||
}
|
||||
|
||||
.mx_RoomListItemView_menu_open .mx_RoomListItemView_container .mx_RoomListItemView_content {
|
||||
.mx_RoomListItemView_menu_open .mx_RoomListItemView_content {
|
||||
/**
|
||||
* The figma uses 16px padding (--cpd-space-4x) but due to https://github.com/element-hq/compound-web/issues/331
|
||||
* the icon size of the menu is 18px instead of 20px with a different internal padding
|
||||
|
||||
@ -55,7 +55,8 @@ Please see LICENSE files in the repository root for full details.
|
||||
background-color: var(--cpd-color-icon-tertiary);
|
||||
}
|
||||
|
||||
.mx_E2EIcon_verified {
|
||||
.mx_E2EIcon_verified,
|
||||
.mx_E2EIcon_warning {
|
||||
.mx_E2EIcon_normal::after {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
@ -68,5 +68,28 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--cpd-space-8x);
|
||||
|
||||
.mx_KeyForm_password {
|
||||
> input[name="recoveryKey"] {
|
||||
/*
|
||||
* From figma https://www.figma.com/design/qTWRfItpO3RdCjnTKPu4mL/Settings?node-id=375-77506&t=d82NdRBDoKsUe1C9-4
|
||||
*/
|
||||
height: 70px;
|
||||
padding: var(--cpd-space-3x) var(--cpd-space-3x) var(--cpd-space-3x) var(--cpd-space-4x);
|
||||
border: var(--cpd-border-width-1) solid;
|
||||
border-radius: 8px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
> button {
|
||||
/*
|
||||
* See figma https://www.figma.com/design/qTWRfItpO3RdCjnTKPu4mL/Settings?node-id=375-77506&t=d82NdRBDoKsUe1C9-4
|
||||
* Avoid stretching the hide/show symbol to the height of the input, and centre it vertically.
|
||||
*/
|
||||
height: 24.5px;
|
||||
padding: var(--cpd-space-1x);
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -47,7 +47,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
.mx_VerificationShowSas_emojiSas_label {
|
||||
font-size: $font-12px;
|
||||
word-break: break-all;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.mx_VerificationShowSas_emojiSas_break {
|
||||
|
||||
@ -477,6 +477,8 @@ export default abstract class BasePlatform {
|
||||
// The redirect URL has to exactly match that registered at the OIDC server, so
|
||||
// ensure that the fragment part of the URL is empty.
|
||||
url.hash = "";
|
||||
// Set no_universal_links=true to prevent the callback being handled by Element X installed on macOS Apple Silicon
|
||||
url.searchParams.set("no_universal_links", "true");
|
||||
return url;
|
||||
}
|
||||
|
||||
|
||||
@ -79,3 +79,12 @@ export function isOnlyCtrlOrCmdKeyEvent(ev: React.KeyboardEvent | KeyboardEvent)
|
||||
return ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given keyboard event is a modified key event (i.e., if any modifier keys are active).
|
||||
* @param ev The keyboard event to check
|
||||
* @returns True if the event is a modified key event, false otherwise
|
||||
*/
|
||||
export function isModifiedKeyEvent(ev: React.KeyboardEvent | KeyboardEvent): boolean {
|
||||
return ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey;
|
||||
}
|
||||
|
||||
@ -7,10 +7,9 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type ComponentProps } from "react";
|
||||
import { type Room, type MatrixEvent, type MatrixClient, type User, EventType } from "matrix-js-sdk/src/matrix";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { EventType, type MatrixClient, type MatrixEvent, type Room, type User } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import MultiInviter, { type CompletionStates } from "./utils/MultiInviter";
|
||||
import MultiInviter, { type CompletionStates, type MultiInviterOptions } from "./utils/MultiInviter";
|
||||
import Modal from "./Modal";
|
||||
import { _t } from "./languageHandler";
|
||||
import InviteDialog from "./components/views/dialogs/InviteDialog";
|
||||
@ -30,18 +29,20 @@ export interface IInviteResult {
|
||||
*
|
||||
* Simpler interface to {@link MultiInviter}.
|
||||
*
|
||||
* Any failures are returned via the `states` in the result.
|
||||
*
|
||||
* @param {string} roomId The ID of the room to invite to
|
||||
* @param {string[]} addresses Array of strings of addresses to invite. May be matrix IDs or 3pids.
|
||||
* @param {function} progressCallback optional callback, fired after each invite.
|
||||
* @param options Options object.
|
||||
* @returns {Promise} Promise
|
||||
*/
|
||||
export async function inviteMultipleToRoom(
|
||||
client: MatrixClient,
|
||||
roomId: string,
|
||||
addresses: string[],
|
||||
progressCallback?: () => void,
|
||||
options: MultiInviterOptions = {},
|
||||
): Promise<IInviteResult> {
|
||||
const inviter = new MultiInviter(client, roomId, progressCallback);
|
||||
const inviter = new MultiInviter(client, roomId, options);
|
||||
return { states: await inviter.invite(addresses), inviter };
|
||||
}
|
||||
|
||||
@ -89,26 +90,6 @@ export function isValid3pidInvite(event: MatrixEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function inviteUsersToRoom(
|
||||
client: MatrixClient,
|
||||
roomId: string,
|
||||
userIds: string[],
|
||||
progressCallback?: () => void,
|
||||
): Promise<void> {
|
||||
return inviteMultipleToRoom(client, roomId, userIds, progressCallback)
|
||||
.then((result) => {
|
||||
const room = client.getRoom(roomId)!;
|
||||
showAnyInviteErrors(result.states, room, result.inviter);
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error(err.stack);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: _t("invite|failed_title"),
|
||||
description: err?.message ?? _t("invite|failed_generic"),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function showAnyInviteErrors(
|
||||
states: CompletionStates,
|
||||
room: Room,
|
||||
|
||||
@ -572,8 +572,11 @@ function textForPinnedEvent(event: MatrixEvent, client: MatrixClient, allowJSX:
|
||||
const senderName = getSenderName(event);
|
||||
const roomId = event.getRoomId()!;
|
||||
|
||||
const pinned = event.getContent<{ pinned: string[] }>().pinned ?? [];
|
||||
const previouslyPinned: string[] = event.getPrevContent().pinned ?? [];
|
||||
const content = event.getContent<{ pinned: string[] }>();
|
||||
const prevContent = event.getPrevContent();
|
||||
|
||||
const pinned = Array.isArray(content.pinned) ? content.pinned : [];
|
||||
const previouslyPinned: string[] = Array.isArray(prevContent.pinned) ? prevContent.pinned : [];
|
||||
const newlyPinned = pinned.filter((item) => previouslyPinned.indexOf(item) < 0);
|
||||
const newlyUnpinned = previouslyPinned.filter((item) => pinned.indexOf(item) < 0);
|
||||
|
||||
|
||||
@ -39,7 +39,12 @@ export function eventTriggersUnreadCount(client: MatrixClient, ev: MatrixEvent):
|
||||
}
|
||||
|
||||
if (ev.isRedacted()) return false;
|
||||
return haveRendererForEvent(ev, client, false /* hidden messages should never trigger unread counts anyways */);
|
||||
try {
|
||||
return haveRendererForEvent(ev, client, false /* hidden messages should never trigger unread counts anyways */);
|
||||
} catch (e) {
|
||||
console.warn("Error determining if event should trigger unread count", e);
|
||||
return false; // If we can't determine if the event should trigger an unread count, assume it does not.
|
||||
}
|
||||
}
|
||||
|
||||
export function doesRoomHaveUnreadMessages(room: Room, includeThreads: boolean): boolean {
|
||||
|
||||
@ -34,6 +34,7 @@ import { Action } from "../../dispatcher/actions";
|
||||
import { type XOR } from "../../@types/common";
|
||||
import ExtensionsCard from "../views/right_panel/ExtensionsCard";
|
||||
import MemberListView from "../views/rooms/MemberList/MemberListView";
|
||||
import { _t } from "../../languageHandler";
|
||||
|
||||
interface BaseProps {
|
||||
overwriteCard?: IRightPanelCard; // used to display a custom card and ignoring the RightPanelStore (used for UserView)
|
||||
@ -64,6 +65,7 @@ interface IState {
|
||||
export default class RightPanel extends React.Component<Props, IState> {
|
||||
public static contextType = MatrixClientContext;
|
||||
declare public context: React.ContextType<typeof MatrixClientContext>;
|
||||
private ref = React.createRef<HTMLDivElement>();
|
||||
|
||||
public constructor(props: Props) {
|
||||
super(props);
|
||||
@ -82,6 +84,7 @@ export default class RightPanel extends React.Component<Props, IState> {
|
||||
public componentDidMount(): void {
|
||||
this.context.on(RoomStateEvent.Members, this.onRoomStateMember);
|
||||
RightPanelStore.instance.on(UPDATE_EVENT, this.onRightPanelStoreUpdate);
|
||||
this.ref.current?.focus();
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
@ -119,7 +122,13 @@ export default class RightPanel extends React.Component<Props, IState> {
|
||||
};
|
||||
|
||||
private onRightPanelStoreUpdate = (): void => {
|
||||
this.setState({ ...(RightPanel.getDerivedStateFromProps(this.props) as IState) });
|
||||
const oldPhase = this.state.phase;
|
||||
const newState = RightPanel.getDerivedStateFromProps(this.props) as IState;
|
||||
this.setState({ ...newState });
|
||||
|
||||
if (oldPhase !== newState.phase) {
|
||||
this.ref.current?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
private onClose = (): void => {
|
||||
@ -282,7 +291,14 @@ export default class RightPanel extends React.Component<Props, IState> {
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className="mx_RightPanel" id="mx_RightPanel" data-testid="right-panel">
|
||||
<aside
|
||||
aria-label={_t("right_panel|title")}
|
||||
ref={this.ref}
|
||||
className="mx_RightPanel"
|
||||
id="mx_RightPanel"
|
||||
data-testid="right-panel"
|
||||
tabIndex={-1}
|
||||
>
|
||||
{card}
|
||||
</aside>
|
||||
);
|
||||
|
||||
@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
import React, { useRef, type JSX, useCallback, useEffect, useState } from "react";
|
||||
import { type VirtuosoHandle, type ListRange, Virtuoso, type VirtuosoProps } from "react-virtuoso";
|
||||
|
||||
import { isModifiedKeyEvent, Key } from "../../Keyboard";
|
||||
/**
|
||||
* Context object passed to each list item containing the currently focused key
|
||||
* and any additional context data from the parent component.
|
||||
@ -29,17 +30,12 @@ export interface IListViewProps<Item, Context>
|
||||
*/
|
||||
items: Item[];
|
||||
|
||||
/**
|
||||
* Callback function called when an item is selected (via Enter/Space key).
|
||||
* @param item - The selected item from the items array
|
||||
*/
|
||||
onSelectItem: (item: Item) => void;
|
||||
|
||||
/**
|
||||
* Function that renders each list item as a JSX element.
|
||||
* @param index - The index of the item in the list
|
||||
* @param item - The data item to render
|
||||
* @param context - The context object containing the focused key and any additional data
|
||||
* @param onFocus - A callback that is required to be called when the item component receives focus
|
||||
* @returns JSX element representing the rendered item
|
||||
*/
|
||||
getItemComponent: (
|
||||
@ -68,6 +64,14 @@ export interface IListViewProps<Item, Context>
|
||||
* @return The key to use for focusing the item
|
||||
*/
|
||||
getItemKey: (item: Item) => string;
|
||||
/**
|
||||
* Callback function to handle key down events on the list container.
|
||||
* ListView handles keyboard navigation for focus(up, down, home, end, pageUp, pageDown)
|
||||
* and stops propagation otherwise the event bubbles and this callback is called for the use of the parent.
|
||||
* @param e - The keyboard event
|
||||
* @returns
|
||||
*/
|
||||
onKeyDown?: (e: React.KeyboardEvent<HTMLDivElement>) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -79,7 +83,7 @@ export interface IListViewProps<Item, Context>
|
||||
*/
|
||||
export function ListView<Item, Context = any>(props: IListViewProps<Item, Context>): React.ReactElement {
|
||||
// Extract our custom props to avoid conflicts with Virtuoso props
|
||||
const { items, onSelectItem, getItemComponent, isItemFocusable, getItemKey, context, ...virtuosoProps } = props;
|
||||
const { items, getItemComponent, isItemFocusable, getItemKey, context, onKeyDown, ...virtuosoProps } = props;
|
||||
/** Reference to the Virtuoso component for programmatic scrolling */
|
||||
const virtuosoHandleRef = useRef<VirtuosoHandle>(null);
|
||||
/** Reference to the DOM element containing the virtualized list */
|
||||
@ -131,7 +135,7 @@ export function ListView<Item, Context = any>(props: IListViewProps<Item, Contex
|
||||
const key = getItemKey(items[clampedIndex]);
|
||||
setTabIndexKey(key);
|
||||
isScrollingToItem.current = true;
|
||||
virtuosoHandleRef?.current?.scrollIntoView({
|
||||
virtuosoHandleRef.current?.scrollIntoView({
|
||||
index: clampedIndex,
|
||||
align: align,
|
||||
behavior: "auto",
|
||||
@ -174,44 +178,44 @@ export function ListView<Item, Context = any>(props: IListViewProps<Item, Contex
|
||||
* Supports Arrow keys, Home, End, Page Up/Down, Enter, and Space.
|
||||
*/
|
||||
const keyDownCallback = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (!e) return; // Guard against null/undefined events
|
||||
|
||||
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
const currentIndex = tabIndexKey ? keyToIndexMap.get(tabIndexKey) : undefined;
|
||||
|
||||
let handled = false;
|
||||
if (e.code === "ArrowUp" && currentIndex !== undefined) {
|
||||
scrollToItem(currentIndex - 1, false);
|
||||
handled = true;
|
||||
} else if (e.code === "ArrowDown" && currentIndex !== undefined) {
|
||||
scrollToItem(currentIndex + 1, true);
|
||||
handled = true;
|
||||
} else if ((e.code === "Enter" || e.code === "Space") && currentIndex !== undefined) {
|
||||
const item = items[currentIndex];
|
||||
onSelectItem(item);
|
||||
handled = true;
|
||||
} else if (e.code === "Home") {
|
||||
scrollToIndex(0);
|
||||
handled = true;
|
||||
} else if (e.code === "End") {
|
||||
scrollToIndex(items.length - 1);
|
||||
handled = true;
|
||||
} else if (e.code === "PageDown" && visibleRange && currentIndex !== undefined) {
|
||||
const numberDisplayed = visibleRange.endIndex - visibleRange.startIndex;
|
||||
scrollToItem(Math.min(currentIndex + numberDisplayed, items.length - 1), true, `start`);
|
||||
handled = true;
|
||||
} else if (e.code === "PageUp" && visibleRange && currentIndex !== undefined) {
|
||||
const numberDisplayed = visibleRange.endIndex - visibleRange.startIndex;
|
||||
scrollToItem(Math.max(currentIndex - numberDisplayed, 0), false, `start`);
|
||||
handled = true;
|
||||
|
||||
// Guard against null/undefined events and modified keys which we don't want to handle here but do
|
||||
// at the settings level shortcuts(E.g. Select next room, etc )
|
||||
if (e || !isModifiedKeyEvent(e)) {
|
||||
if (e.code === Key.ARROW_UP && currentIndex !== undefined) {
|
||||
scrollToItem(currentIndex - 1, false);
|
||||
handled = true;
|
||||
} else if (e.code === Key.ARROW_DOWN && currentIndex !== undefined) {
|
||||
scrollToItem(currentIndex + 1, true);
|
||||
handled = true;
|
||||
} else if (e.code === Key.HOME) {
|
||||
scrollToIndex(0);
|
||||
handled = true;
|
||||
} else if (e.code === Key.END) {
|
||||
scrollToIndex(items.length - 1);
|
||||
handled = true;
|
||||
} else if (e.code === Key.PAGE_DOWN && visibleRange && currentIndex !== undefined) {
|
||||
const numberDisplayed = visibleRange.endIndex - visibleRange.startIndex;
|
||||
scrollToItem(Math.min(currentIndex + numberDisplayed, items.length - 1), true, `start`);
|
||||
handled = true;
|
||||
} else if (e.code === Key.PAGE_UP && visibleRange && currentIndex !== undefined) {
|
||||
const numberDisplayed = visibleRange.endIndex - visibleRange.startIndex;
|
||||
scrollToItem(Math.max(currentIndex - numberDisplayed, 0), false, `start`);
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
} else {
|
||||
onKeyDown?.(e);
|
||||
}
|
||||
},
|
||||
[scrollToIndex, scrollToItem, tabIndexKey, keyToIndexMap, visibleRange, items, onSelectItem],
|
||||
[scrollToIndex, scrollToItem, tabIndexKey, keyToIndexMap, visibleRange, items, onKeyDown],
|
||||
);
|
||||
|
||||
/**
|
||||
@ -261,8 +265,12 @@ export function ListView<Item, Context = any>(props: IListViewProps<Item, Contex
|
||||
[keyToIndexMap, visibleRange, scrollToIndex, tabIndexKey],
|
||||
);
|
||||
|
||||
const onBlur = useCallback((): void => {
|
||||
setIsFocused(false);
|
||||
const onBlur = useCallback((event: React.FocusEvent<HTMLDivElement>): void => {
|
||||
// Only set isFocused to false if the focus is moving outside the list
|
||||
// This prevents the list from losing focus when interacting with menus inside it
|
||||
if (!event.currentTarget.contains(event.relatedTarget)) {
|
||||
setIsFocused(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const listContext: ListContext<Context> = {
|
||||
@ -274,8 +282,8 @@ export function ListView<Item, Context = any>(props: IListViewProps<Item, Contex
|
||||
return (
|
||||
<Virtuoso
|
||||
tabIndex={props.tabIndex || undefined} // We don't need to focus the container, so leave it undefined by default
|
||||
scrollerRef={scrollerRef}
|
||||
ref={virtuosoHandleRef}
|
||||
scrollerRef={scrollerRef}
|
||||
onKeyDown={keyDownCallback}
|
||||
context={listContext}
|
||||
rangeChanged={setVisibleRange}
|
||||
|
||||