Compare commits
8 Commits
hs/user-pr
...
rav/invite
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2db2a77d56 | ||
|
|
fc3a52cd4c | ||
|
|
b0a15d97f3 | ||
|
|
a646f3575f | ||
|
|
0541b7a961 | ||
|
|
58f1893997 | ||
|
|
a562b2928a | ||
|
|
f43fadd9b8 |
3
.github/CODEOWNERS
vendored
@@ -20,7 +20,6 @@
|
||||
# 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 & mas plugins as this is updated by GHA for docker image updating
|
||||
# Ignore the synapse plugin 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
repository: element-hq/element-desktop
|
||||
path: element-desktop
|
||||
|
||||
- name: Fetch element-web
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
path: element-web
|
||||
|
||||
- name: Fetch matrix-js-sdk
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
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@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
repository: element-hq/element-web
|
||||
|
||||
@@ -129,13 +129,13 @@ jobs:
|
||||
- runAllTests: false
|
||||
project: Pinecone
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
repository: element-hq/element-web
|
||||
|
||||
- name: 📥 Download artifact
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
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@0400d5f644dc74513175e3cd8d07132dd4860809 # v4
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
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@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
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@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- name: Update synapse image
|
||||
run: |
|
||||
@@ -21,15 +21,6 @@ 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
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@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
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@0400d5f644dc74513175e3cd8d07132dd4860809 # v4
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
|
||||
12
.github/workflows/static_analysis.yaml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
name: "Typescript Syntax Check"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
name: "Rethemendex Check"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- run: ./res/css/rethemendex.sh
|
||||
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
name: "ESLint"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
@@ -86,7 +86,7 @@ jobs:
|
||||
name: "Style Lint"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
@@ -104,7 +104,7 @@ jobs:
|
||||
name: "Workflow Lint"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
@@ -122,7 +122,7 @@ jobs:
|
||||
name: "Analyse Dead Code"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
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@0400d5f644dc74513175e3cd8d07132dd4860809 # v4
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# syntax=docker.io/docker/dockerfile:1.17-labs@sha256:9187104f31e3a002a8a6a3209ea1f937fb7486c093cbbde1e14b0fa0d7e4f1b5
|
||||
|
||||
# Builder
|
||||
FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:9e34ba52e1f3c31ed9bd4d0bcf784f5909db17cda61c220e29c8d7a8ebfb402e AS builder
|
||||
FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:2d63e0f812d023c4c764e83d7e30dc94949304443ebc372d5c445e63a5ae49c1 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:e61b77b27c8f3124fad6d19e894ca5b603bcaf6a34a2df035511299dfa6fad35
|
||||
|
||||
# Need root user to install packages & manipulate the usr directory
|
||||
USER root
|
||||
|
||||
@@ -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
|
||||
|
||||
12
package.json
@@ -75,7 +75,7 @@
|
||||
"resolutions": {
|
||||
"**/pretty-format/react-is": "19.1.1",
|
||||
"@playwright/test": "1.54.2",
|
||||
"@types/react": "19.1.10",
|
||||
"@types/react": "19.1.9",
|
||||
"@types/react-dom": "19.1.7",
|
||||
"oidc-client-ts": "3.3.0",
|
||||
"jwt-decode": "4.0.0",
|
||||
@@ -96,6 +96,7 @@
|
||||
"@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",
|
||||
@@ -142,7 +143,7 @@
|
||||
"opus-recorder": "^8.0.3",
|
||||
"pako": "^2.0.3",
|
||||
"png-chunks-extract": "^1.0.0",
|
||||
"posthog-js": "1.260.1",
|
||||
"posthog-js": "1.259.0",
|
||||
"qrcode": "1.5.4",
|
||||
"re-resizable": "6.11.2",
|
||||
"react": "^19.0.0",
|
||||
@@ -152,7 +153,8 @@
|
||||
"react-focus-lock": "^2.5.1",
|
||||
"react-string-replace": "^1.1.1",
|
||||
"react-transition-group": "^4.4.1",
|
||||
"react-virtuoso": "^4.14.0",
|
||||
"react-virtualized": "^9.22.5",
|
||||
"react-virtuoso": "^4.12.6",
|
||||
"rfc4648": "^1.4.0",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"sanitize-html": "2.17.0",
|
||||
@@ -185,7 +187,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.6",
|
||||
"@element-hq/element-web-playwright-common": "^1.4.4",
|
||||
"@peculiar/webcrypto": "^1.4.3",
|
||||
"@playwright/test": "^1.50.1",
|
||||
"@principalstudio/html-webpack-inject-preload": "^1.2.7",
|
||||
@@ -221,7 +223,7 @@
|
||||
"@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.9",
|
||||
"@types/react-beautiful-dnd": "^13.0.0",
|
||||
"@types/react-dom": "19.1.7",
|
||||
"@types/react-transition-group": "^4.4.0",
|
||||
|
||||
@@ -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("option", { name: "Open room Old Room" });
|
||||
const oldRoomTile = roomListView.getByRole("gridcell", { name: "Open room Old Room" });
|
||||
await expect(oldRoomTile).toBeVisible();
|
||||
|
||||
/*
|
||||
@@ -139,9 +139,8 @@ test.describe("Room list filters and sort", () => {
|
||||
|
||||
// Open the non-favourite room
|
||||
const roomListView = getRoomList(page);
|
||||
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);
|
||||
const tile = roomListView.getByRole("gridcell", { name: "Open room room-non-fav" });
|
||||
await tile.scrollIntoViewIfNeeded();
|
||||
await tile.click();
|
||||
|
||||
// Enable Favourite filter
|
||||
@@ -152,7 +151,7 @@ test.describe("Room list filters and sort", () => {
|
||||
|
||||
// Ensure the room list is not scrolled
|
||||
const isScrolledDown = await page
|
||||
.getByRole("listbox", { name: "Room list", exact: true })
|
||||
.getByRole("grid", { name: "Room list" })
|
||||
.evaluate((e) => e.scrollTop !== 0);
|
||||
expect(isScrolledDown).toStrictEqual(false);
|
||||
});
|
||||
@@ -228,37 +227,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("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(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(primaryFilters).toMatchScreenshot("unread-primary-filters.png");
|
||||
|
||||
await primaryFilters.getByRole("option", { name: "People" }).click();
|
||||
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 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 primaryFilters.getByRole("option", { name: "Rooms" }).click();
|
||||
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 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 getFilterExpandButton(page).click();
|
||||
|
||||
await primaryFilters.getByRole("option", { name: "Favourite" }).click();
|
||||
await expect(roomList.getByRole("option", { name: "favourite room" })).toBeVisible();
|
||||
await expect.poll(() => roomList.locator("role=option").count()).toBe(1);
|
||||
await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible();
|
||||
await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(1);
|
||||
|
||||
await primaryFilters.getByRole("option", { name: "Mentions" }).click();
|
||||
await expect(roomList.getByRole("option", { name: "room with mention" })).toBeVisible();
|
||||
await expect.poll(() => roomList.locator("role=option").count()).toBe(1);
|
||||
await expect(roomList.getByRole("gridcell", { name: "room with mention" })).toBeVisible();
|
||||
await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(1);
|
||||
|
||||
await primaryFilters.getByRole("option", { name: "Invites" }).click();
|
||||
await expect(roomList.getByRole("option", { name: "invited room" })).toBeVisible();
|
||||
await expect.poll(() => roomList.locator("role=option").count()).toBe(1);
|
||||
await expect(roomList.getByRole("gridcell", { name: "invited room" })).toBeVisible();
|
||||
await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(1);
|
||||
|
||||
await getFilterCollapseButton(page).click();
|
||||
await expect(primaryFilters.locator("role=option").first()).toHaveText("Invites");
|
||||
@@ -269,7 +268,6 @@ 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);
|
||||
@@ -278,20 +276,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("option", { name: "Open room favourite room" }).click();
|
||||
await roomListView.getByRole("gridcell", { 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 primaryFilters.getByRole("option", { name: "Unread" }).click();
|
||||
await page.getByRole("option", { name: "Unread" }).click();
|
||||
|
||||
// Unread filter should only show unread room and not unread dm!
|
||||
const unreadDm = roomListView.getByRole("option", { name: "Open room unread room" });
|
||||
const unreadDm = roomListView.getByRole("gridcell", { name: "Open room unread room" });
|
||||
await expect(unreadDm).toBeVisible();
|
||||
await expect(unreadDm).toMatchScreenshot("unread-dm.png");
|
||||
await expect(roomListView.getByRole("option", { name: "Open room unread dm" })).not.toBeVisible();
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room unread dm" })).not.toBeVisible();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -301,7 +299,7 @@ test.describe("Room list filters and sort", () => {
|
||||
await getRoomOptionsMenu(page).click();
|
||||
await page.getByRole("menuitemradio", { name: "A-Z" }).click();
|
||||
|
||||
await expect(roomListView.getByRole("option").first()).toHaveText(/empty room/);
|
||||
await expect(roomListView.getByRole("gridcell").first()).toHaveText(/empty room/);
|
||||
});
|
||||
|
||||
test("should move room to the top on message when sorting by activity", async ({ page, bot }) => {
|
||||
@@ -309,7 +307,7 @@ test.describe("Room list filters and sort", () => {
|
||||
|
||||
await bot.sendMessage(unReadDmId, "Hello!");
|
||||
|
||||
await expect(roomListView.getByRole("option").first()).toHaveText(/unread dm/);
|
||||
await expect(roomListView.getByRole("gridcell").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("option", { name: "Open room room19" })).toBeVisible();
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room room19" })).toBeVisible();
|
||||
await expect(roomListView).toMatchScreenshot("room-list-panel.png");
|
||||
});
|
||||
|
||||
|
||||
@@ -43,35 +43,31 @@ test.describe("Room list", () => {
|
||||
|
||||
test("should render the room list", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
await expect(roomListView.getByRole("option", { name: "Open room room29" })).toBeVisible();
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room room29" })).toBeVisible();
|
||||
await expect(roomListView).toMatchScreenshot("room-list.png");
|
||||
|
||||
// Put focus on the room list
|
||||
await roomListView.getByRole("option", { name: "Open room room29" }).click();
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
|
||||
// Scroll to the end of the room 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 app.scrollListToBottom(page.locator(".mx_RoomList_List"));
|
||||
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("option", { name: "Open room room29" }).click();
|
||||
await roomListView.getByRole("gridcell", { 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("option", { name: "Open room room29" }).click({ button: "right" });
|
||||
await roomListView.getByRole("gridcell", { 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("option", { name: "Open room room29" });
|
||||
const roomItem = roomListView.getByRole("gridcell", { name: "Open room room29" });
|
||||
await roomItem.hover();
|
||||
|
||||
await expect(roomItem).toMatchScreenshot("room-list-item-hover.png");
|
||||
@@ -101,7 +97,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("option", { name: "Open room room29" });
|
||||
const roomItem = roomListView.getByRole("gridcell", { name: "Open room room29" });
|
||||
await roomItem.hover();
|
||||
|
||||
await expect(roomItem).toMatchScreenshot("room-list-item-hover.png");
|
||||
@@ -121,10 +117,10 @@ test.describe("Room list", () => {
|
||||
await expect(roomItem.getByTestId("notification-decoration")).not.toBeVisible();
|
||||
|
||||
// Put focus on the room list
|
||||
await roomListView.getByRole("option", { name: "Open room room28" }).click();
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room28" }).click();
|
||||
|
||||
// Scroll to the end of the room list
|
||||
await app.scrollListToBottom(roomListView);
|
||||
await app.scrollListToBottom(page.locator(".mx_RoomList_List"));
|
||||
|
||||
// The room decoration should have the muted icon
|
||||
await expect(roomItem.getByTestId("notification-decoration")).toBeVisible();
|
||||
@@ -143,25 +139,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("option", { name: "Open room room29" }).click();
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
|
||||
// Scroll to the end of the room list
|
||||
await app.scrollListToBottom(roomListView);
|
||||
await app.scrollListToBottom(page.locator(".mx_RoomList_List"));
|
||||
|
||||
await expect(roomListView.getByRole("option", { name: "Open room room0" })).toBeVisible();
|
||||
await roomListView.getByRole("option", { name: "Open room room0" }).click();
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
|
||||
await roomListView.getByRole("gridcell", { 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("option", { name: "Open room room0" })).not.toBeVisible();
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).not.toBeVisible();
|
||||
|
||||
await filters.getByRole("option", { name: "People" }).click();
|
||||
await expect(roomListView.getByRole("option", { name: "Open room room0" })).toBeVisible();
|
||||
await expect(roomListView.getByRole("gridcell", { 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("option", { name: "Open room room29" }).click();
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
|
||||
await page.keyboard.press("Alt+ArrowDown");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "room28", level: 1 })).toBeVisible();
|
||||
@@ -169,7 +165,7 @@ test.describe("Room list", () => {
|
||||
|
||||
test("should select the previous room", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
await roomListView.getByRole("option", { name: "Open room room28" }).click();
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room28" }).click();
|
||||
await page.keyboard.press("Alt+ArrowUp");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "room29", level: 1 })).toBeVisible();
|
||||
@@ -177,7 +173,7 @@ test.describe("Room list", () => {
|
||||
|
||||
test("should select the last room", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
await roomListView.getByRole("option", { name: "Open room room29" }).click();
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
|
||||
await page.keyboard.press("Alt+ArrowUp");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "room0", level: 1 })).toBeVisible();
|
||||
@@ -191,7 +187,7 @@ test.describe("Room list", () => {
|
||||
await bot.joinRoom(roomId);
|
||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||
|
||||
await roomListView.getByRole("option", { name: "Open room room20" }).click();
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room20" }).click();
|
||||
|
||||
await page.keyboard.press("Alt+Shift+ArrowDown");
|
||||
|
||||
@@ -203,8 +199,8 @@ test.describe("Room list", () => {
|
||||
test("should navigate to the room list", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
const room29 = roomListView.getByRole("option", { name: "Open room room29" });
|
||||
const room28 = roomListView.getByRole("option", { name: "Open room room28" });
|
||||
const room29 = roomListView.getByRole("gridcell", { name: "Open room room29" });
|
||||
const room28 = roomListView.getByRole("gridcell", { name: "Open room room28" });
|
||||
|
||||
// open the room
|
||||
await room29.click();
|
||||
@@ -223,7 +219,7 @@ test.describe("Room list", () => {
|
||||
|
||||
test("should navigate to the notification menu", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
const room29 = roomListView.getByRole("option", { name: "Open room room29" });
|
||||
const room29 = roomListView.getByRole("gridcell", { name: "Open room room29" });
|
||||
const moreButton = room29.getByRole("button", { name: "More options" });
|
||||
const notificationButton = room29.getByRole("button", { name: "Notification options" });
|
||||
|
||||
@@ -262,7 +258,7 @@ test.describe("Room list", () => {
|
||||
await page.getByRole("button", { name: "User menu" }).focus();
|
||||
|
||||
const roomListView = getRoomList(page);
|
||||
const publicRoom = roomListView.getByRole("option", { name: "public room" });
|
||||
const publicRoom = roomListView.getByRole("gridcell", { name: "public room" });
|
||||
|
||||
await expect(publicRoom).toBeVisible();
|
||||
await expect(publicRoom).toMatchScreenshot("room-list-item-public.png");
|
||||
@@ -272,7 +268,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("option", { name: "low priority room" });
|
||||
const publicRoom = roomListView.getByRole("gridcell", { name: "low priority room" });
|
||||
|
||||
// Make room low priority
|
||||
await publicRoom.hover();
|
||||
@@ -297,7 +293,7 @@ test.describe("Room list", () => {
|
||||
await page.getByRole("button", { name: "Create video room" }).click();
|
||||
|
||||
const roomListView = getRoomList(page);
|
||||
const videoRoom = roomListView.getByRole("option", { name: "video room" });
|
||||
const videoRoom = roomListView.getByRole("gridcell", { name: "video room" });
|
||||
|
||||
// focus the user menu to avoid to have hover decoration
|
||||
await page.getByRole("button", { name: "User menu" }).focus();
|
||||
@@ -316,7 +312,7 @@ test.describe("Room list", () => {
|
||||
invite: [user.userId],
|
||||
is_direct: true,
|
||||
});
|
||||
const invitedRoom = roomListView.getByRole("option", { name: "invited room" });
|
||||
const invitedRoom = roomListView.getByRole("gridcell", { name: "invited room" });
|
||||
await expect(invitedRoom).toBeVisible();
|
||||
await expect(invitedRoom).toMatchScreenshot("room-list-item-invited.png");
|
||||
});
|
||||
@@ -331,7 +327,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("option", { name: "2 notifications" });
|
||||
const room = roomListView.getByRole("gridcell", { name: "2 notifications" });
|
||||
await expect(room).toBeVisible();
|
||||
await expect(room.getByTestId("notification-decoration")).toHaveText("2");
|
||||
await expect(room).toMatchScreenshot("room-list-item-notification.png");
|
||||
@@ -362,7 +358,7 @@ test.describe("Room list", () => {
|
||||
);
|
||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||
|
||||
const room = roomListView.getByRole("option", { name: "mention" });
|
||||
const room = roomListView.getByRole("gridcell", { name: "mention" });
|
||||
await expect(room).toBeVisible();
|
||||
await expect(room).toMatchScreenshot("room-list-item-mention.png");
|
||||
});
|
||||
@@ -383,7 +379,7 @@ test.describe("Room list", () => {
|
||||
await bot.joinRoom(roomId);
|
||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||
|
||||
const room = roomListView.getByRole("option", { name: "activity" });
|
||||
const room = roomListView.getByRole("gridcell", { name: "activity" });
|
||||
await expect(room.getByText("I am a robot. Beep.")).toBeVisible();
|
||||
await expect(room).toMatchScreenshot("room-list-item-message-preview.png");
|
||||
});
|
||||
@@ -410,7 +406,7 @@ test.describe("Room list", () => {
|
||||
await app.viewRoomById(otherRoomId);
|
||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||
|
||||
const room = roomListView.getByRole("option", { name: "activity" });
|
||||
const room = roomListView.getByRole("gridcell", { name: "activity" });
|
||||
await expect(room.getByTestId("notification-decoration")).toBeVisible();
|
||||
await expect(room).toMatchScreenshot("room-list-item-activity.png");
|
||||
});
|
||||
@@ -422,7 +418,7 @@ test.describe("Room list", () => {
|
||||
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||
await bot.joinRoom(roomId);
|
||||
|
||||
const room = roomListView.getByRole("option", { name: "mark as unread" });
|
||||
const room = roomListView.getByRole("gridcell", { name: "mark as unread" });
|
||||
await room.hover();
|
||||
await room.getByRole("button", { name: "More Options" }).click();
|
||||
await page.getByRole("menuitem", { name: "mark as unread" }).click();
|
||||
@@ -445,7 +441,7 @@ test.describe("Room list", () => {
|
||||
await page.getByText("Off").click();
|
||||
await app.settings.closeDialog();
|
||||
|
||||
const room = roomListView.getByRole("option", { name: "silent" });
|
||||
const room = roomListView.getByRole("gridcell", { name: "silent" });
|
||||
await expect(room.getByTestId("notification-decoration")).toBeVisible();
|
||||
await expect(room).toMatchScreenshot("room-list-item-silent.png");
|
||||
});
|
||||
|
||||
@@ -95,6 +95,10 @@ 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."),
|
||||
|
||||
@@ -1,49 +1,38 @@
|
||||
/*
|
||||
Copyright 2024-2025 New Vector Ltd.
|
||||
Copyright 2024 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 "../../../testcontainers/mas.ts";
|
||||
import { MatrixAuthenticationServiceContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers";
|
||||
|
||||
import { type Fixtures } from "../../../element-web-test.ts";
|
||||
|
||||
export const masHomeserver: Fixtures = {
|
||||
mas: [
|
||||
async ({ _homeserver: homeserver, logger, network, postgres, mailpit }, use) => {
|
||||
const secret = "AnotherRandomSecret";
|
||||
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 limits = { burst: 10, per_second: 10 };
|
||||
const container = await new MatrixAuthenticationServiceContainer(postgres)
|
||||
.withNetwork(network)
|
||||
.withNetworkAliases("mas")
|
||||
.withLogConsumer(logger.getConsumer("mas"))
|
||||
.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,
|
||||
},
|
||||
},
|
||||
})
|
||||
.withConfig(config)
|
||||
.start();
|
||||
|
||||
homeserver.withConfig({
|
||||
@@ -51,10 +40,16 @@ export const masHomeserver: Fixtures = {
|
||||
enable_registration_without_verification: undefined,
|
||||
disable_msisdn_registration: undefined,
|
||||
password_config: undefined,
|
||||
matrix_authentication_service: {
|
||||
enabled: true,
|
||||
endpoint: "http://mas:8080/",
|
||||
secret,
|
||||
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,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -64,6 +59,28 @@ 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: 2.6 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.6 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.0 KiB After Width: | Height: | Size: 3.3 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.2 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 20 KiB |
@@ -1,24 +0,0 @@
|
||||
/*
|
||||
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:430b1f00e74c3f89f078670f676b4333f6bbe5a339962344b3ae84e99e9bcd7f";
|
||||
|
||||
/**
|
||||
* 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:18e9e77eac01709e9ab4d26cf20c36bf5a1567756bb5a78c00cabf366d65a950";
|
||||
const TAG = "develop@sha256:969499cf0c6150fa52d1dd41c8ec488f36dfb76b36ac81b035b27663a76f8e0e";
|
||||
|
||||
/**
|
||||
* SynapseContainer which freezes the docker digest to stabilise tests,
|
||||
|
||||
@@ -12,39 +12,31 @@ 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; /* Compound equivalent: --cpd-font-size-body-xs */
|
||||
$font-11px: 0.6875rem;
|
||||
$font-12px: 0.75rem;
|
||||
$font-13px: 0.8125rem; /* Compound equivalent: --cpd-font-size-body-sm */
|
||||
$font-13px: 0.8125rem;
|
||||
$font-14px: 0.875rem;
|
||||
$font-15px: 0.9375rem; /* Compound equivalent: --cpd-font-size-body-md */
|
||||
$font-15px: 0.9375rem;
|
||||
$font-16px: 1rem;
|
||||
$font-17px: 1.0625rem; /* Compound equivalent: --cpd-font-size-body-lg */
|
||||
$font-17px: 1.0625rem;
|
||||
$font-18px: 1.125rem;
|
||||
$font-20px: 1.25rem; /* Compound equivalent: --cpd-font-size-heading-sm */
|
||||
$font-20px: 1.25rem;
|
||||
$font-22px: 1.375rem;
|
||||
$font-23px: 1.4375rem;
|
||||
$font-24px: 1.5rem; /* Compound equivalent: --cpd-font-size-heading-md */
|
||||
$font-24px: 1.5rem;
|
||||
$font-25px: 1.5625rem;
|
||||
$font-26px: 1.625rem;
|
||||
$font-28px: 1.75rem; /* Compound equivalent: --cpd-font-size-heading-lg */
|
||||
$font-28px: 1.75rem;
|
||||
$font-29px: 1.8125rem;
|
||||
$font-30px: 1.875rem;
|
||||
$font-32px: 2rem; /* Compound equivalent: --cpd-font-size-heading-xl */
|
||||
$font-32px: 2rem;
|
||||
$font-34px: 2.125rem;
|
||||
$font-35px: 2.1875rem;
|
||||
$font-39px: 2.4375rem;
|
||||
|
||||
@@ -15,44 +15,40 @@
|
||||
* |-------------------------------------------------------|
|
||||
*/
|
||||
.mx_RoomListItemView {
|
||||
/* Remove button default style */
|
||||
background: unset;
|
||||
border: none;
|
||||
padding: 0;
|
||||
text-align: unset;
|
||||
|
||||
all: unset;
|
||||
cursor: pointer;
|
||||
height: 48px;
|
||||
width: 100%;
|
||||
|
||||
padding-left: var(--cpd-space-3x);
|
||||
font: var(--cpd-font-body-md-regular);
|
||||
|
||||
.mx_RoomListItemView_content {
|
||||
.mx_RoomListItemView_container {
|
||||
padding-left: var(--cpd-space-3x);
|
||||
font: var(--cpd-font-body-md-regular);
|
||||
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_text {
|
||||
.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_roomName {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.mx_RoomListItemView_text {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.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_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,7 +57,7 @@
|
||||
background-color: var(--cpd-color-bg-action-secondary-hovered);
|
||||
}
|
||||
|
||||
.mx_RoomListItemView_menu_open .mx_RoomListItemView_content {
|
||||
.mx_RoomListItemView_menu_open .mx_RoomListItemView_container .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
|
||||
|
||||
@@ -68,28 +68,5 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,12 +79,3 @@ 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;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ 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)
|
||||
@@ -65,7 +64,6 @@ 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);
|
||||
@@ -84,7 +82,6 @@ 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 {
|
||||
@@ -122,13 +119,7 @@ export default class RightPanel extends React.Component<Props, IState> {
|
||||
};
|
||||
|
||||
private onRightPanelStoreUpdate = (): void => {
|
||||
const oldPhase = this.state.phase;
|
||||
const newState = RightPanel.getDerivedStateFromProps(this.props) as IState;
|
||||
this.setState({ ...newState });
|
||||
|
||||
if (oldPhase !== newState.phase) {
|
||||
this.ref.current?.focus();
|
||||
}
|
||||
this.setState({ ...(RightPanel.getDerivedStateFromProps(this.props) as IState) });
|
||||
};
|
||||
|
||||
private onClose = (): void => {
|
||||
@@ -291,14 +282,7 @@ export default class RightPanel extends React.Component<Props, IState> {
|
||||
}
|
||||
|
||||
return (
|
||||
<aside
|
||||
aria-label={_t("right_panel|title")}
|
||||
ref={this.ref}
|
||||
className="mx_RightPanel"
|
||||
id="mx_RightPanel"
|
||||
data-testid="right-panel"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<aside className="mx_RightPanel" id="mx_RightPanel" data-testid="right-panel">
|
||||
{card}
|
||||
</aside>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,6 @@ 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.
|
||||
@@ -30,12 +29,17 @@ 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: (
|
||||
@@ -64,14 +68,6 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,7 +79,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, getItemComponent, isItemFocusable, getItemKey, context, onKeyDown, ...virtuosoProps } = props;
|
||||
const { items, onSelectItem, getItemComponent, isItemFocusable, getItemKey, context, ...virtuosoProps } = props;
|
||||
/** Reference to the Virtuoso component for programmatic scrolling */
|
||||
const virtuosoHandleRef = useRef<VirtuosoHandle>(null);
|
||||
/** Reference to the DOM element containing the virtualized list */
|
||||
@@ -135,7 +131,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",
|
||||
@@ -178,44 +174,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<HTMLDivElement>) => {
|
||||
const currentIndex = tabIndexKey ? keyToIndexMap.get(tabIndexKey) : undefined;
|
||||
let handled = false;
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (!e) return; // Guard against null/undefined events
|
||||
|
||||
// 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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
} else {
|
||||
onKeyDown?.(e);
|
||||
}
|
||||
},
|
||||
[scrollToIndex, scrollToItem, tabIndexKey, keyToIndexMap, visibleRange, items, onKeyDown],
|
||||
[scrollToIndex, scrollToItem, tabIndexKey, keyToIndexMap, visibleRange, items, onSelectItem],
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -265,12 +261,8 @@ export function ListView<Item, Context = any>(props: IListViewProps<Item, Contex
|
||||
[keyToIndexMap, visibleRange, scrollToIndex, tabIndexKey],
|
||||
);
|
||||
|
||||
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 onBlur = useCallback((): void => {
|
||||
setIsFocused(false);
|
||||
}, []);
|
||||
|
||||
const listContext: ListContext<Context> = {
|
||||
@@ -282,8 +274,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
|
||||
ref={virtuosoHandleRef}
|
||||
scrollerRef={scrollerRef}
|
||||
ref={virtuosoHandleRef}
|
||||
onKeyDown={keyDownCallback}
|
||||
context={listContext}
|
||||
rangeChanged={setVisibleRange}
|
||||
|
||||
@@ -38,6 +38,8 @@ import { isValid3pidInvite } from "../../../RoomInvite";
|
||||
import { type ThreePIDInvite } from "../../../models/rooms/ThreePIDInvite";
|
||||
import { type XOR } from "../../../@types/common";
|
||||
import { useTypedEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
|
||||
type Member = XOR<{ member: RoomMember }, { threePidInvite: ThreePIDInvite }>;
|
||||
|
||||
@@ -111,6 +113,7 @@ export interface MemberListViewState {
|
||||
shouldShowSearch: boolean;
|
||||
isLoading: boolean;
|
||||
canInvite: boolean;
|
||||
onClickMember: (member: RoomMember | ThreePIDInvite) => void;
|
||||
onInviteButtonClick: (ev: ButtonEvent) => void;
|
||||
}
|
||||
export function useMemberListViewModel(roomId: string): MemberListViewState {
|
||||
@@ -133,6 +136,14 @@ export function useMemberListViewModel(roomId: string): MemberListViewState {
|
||||
*/
|
||||
const [memberCount, setMemberCount] = useState(0);
|
||||
|
||||
const onClickMember = (member: RoomMember | ThreePIDInvite): void => {
|
||||
dis.dispatch({
|
||||
action: Action.ViewUser,
|
||||
member: member,
|
||||
push: true,
|
||||
});
|
||||
};
|
||||
|
||||
const loadMembers = useMemo(
|
||||
() =>
|
||||
throttle(
|
||||
@@ -267,6 +278,7 @@ export function useMemberListViewModel(roomId: string): MemberListViewState {
|
||||
isPresenceEnabled,
|
||||
isLoading,
|
||||
onInviteButtonClick,
|
||||
onClickMember,
|
||||
shouldShowSearch: totalMemberCount >= 20,
|
||||
canInvite,
|
||||
};
|
||||
|
||||
@@ -18,7 +18,6 @@ import { Action } from "../../../dispatcher/actions";
|
||||
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
|
||||
import { useStickyRoomList } from "./useStickyRoomList";
|
||||
import { useRoomListNavigation } from "./useRoomListNavigation";
|
||||
import { type RoomsResult } from "../../../stores/room-list-v3/RoomListStoreV3";
|
||||
|
||||
export interface RoomListViewState {
|
||||
/**
|
||||
@@ -27,9 +26,9 @@ export interface RoomListViewState {
|
||||
isLoadingRooms: boolean;
|
||||
|
||||
/**
|
||||
* The room results to be displayed (along with the spaceId and filter keys at the time of query)
|
||||
* A list of rooms to be displayed in the left panel.
|
||||
*/
|
||||
roomsResult: RoomsResult;
|
||||
rooms: Room[];
|
||||
|
||||
/**
|
||||
* Create a chat room
|
||||
@@ -72,10 +71,10 @@ export interface RoomListViewState {
|
||||
*/
|
||||
export function useRoomListViewModel(): RoomListViewState {
|
||||
const matrixClient = useMatrixClientContext();
|
||||
const { isLoadingRooms, primaryFilters, activePrimaryFilter, roomsResult: filteredRooms } = useFilteredRooms();
|
||||
const { activeIndex, roomsResult } = useStickyRoomList(filteredRooms);
|
||||
const { isLoadingRooms, primaryFilters, activePrimaryFilter, rooms: filteredRooms } = useFilteredRooms();
|
||||
const { activeIndex, rooms } = useStickyRoomList(filteredRooms);
|
||||
|
||||
useRoomListNavigation(roomsResult.rooms);
|
||||
useRoomListNavigation(rooms);
|
||||
|
||||
const currentSpace = useEventEmitterState<Room | null>(
|
||||
SpaceStore.instance,
|
||||
@@ -89,7 +88,7 @@ export function useRoomListViewModel(): RoomListViewState {
|
||||
|
||||
return {
|
||||
isLoadingRooms,
|
||||
roomsResult,
|
||||
rooms,
|
||||
canCreateRoom,
|
||||
createRoom,
|
||||
createChatRoom,
|
||||
|
||||
@@ -5,15 +5,12 @@ 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 { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
|
||||
import type { Room } from "matrix-js-sdk/src/matrix";
|
||||
import { FilterKey } from "../../../stores/room-list-v3/skip-list/filters";
|
||||
import { _t, _td, type TranslationKey } from "../../../languageHandler";
|
||||
import RoomListStoreV3, {
|
||||
LISTS_LOADED_EVENT,
|
||||
LISTS_UPDATE_EVENT,
|
||||
type RoomsResult,
|
||||
} from "../../../stores/room-list-v3/RoomListStoreV3";
|
||||
import RoomListStoreV3, { LISTS_LOADED_EVENT, LISTS_UPDATE_EVENT } from "../../../stores/room-list-v3/RoomListStoreV3";
|
||||
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
import SpaceStore from "../../../stores/spaces/SpaceStore";
|
||||
import { UPDATE_SELECTED_SPACE } from "../../../stores/spaces";
|
||||
@@ -38,7 +35,7 @@ export interface PrimaryFilter {
|
||||
interface FilteredRooms {
|
||||
primaryFilters: PrimaryFilter[];
|
||||
isLoadingRooms: boolean;
|
||||
roomsResult: RoomsResult;
|
||||
rooms: Room[];
|
||||
/**
|
||||
* The currently active primary filter.
|
||||
* If no primary filter is active, this will be undefined.
|
||||
@@ -66,12 +63,12 @@ export function useFilteredRooms(): FilteredRooms {
|
||||
*/
|
||||
const [primaryFilter, setPrimaryFilter] = useState<FilterKey | undefined>();
|
||||
|
||||
const [roomsResult, setRoomsResult] = useState(() => RoomListStoreV3.instance.getSortedRoomsInActiveSpace());
|
||||
const [rooms, setRooms] = useState(() => RoomListStoreV3.instance.getSortedRoomsInActiveSpace());
|
||||
const [isLoadingRooms, setIsLoadingRooms] = useState(() => RoomListStoreV3.instance.isLoadingRooms);
|
||||
|
||||
const updateRoomsFromStore = useCallback((filters: FilterKey[] = []): void => {
|
||||
const newRooms = RoomListStoreV3.instance.getSortedRoomsInActiveSpace(filters);
|
||||
setRoomsResult(newRooms);
|
||||
setRooms(newRooms);
|
||||
}, []);
|
||||
|
||||
// Reset filters when active space changes
|
||||
@@ -80,15 +77,9 @@ export function useFilteredRooms(): FilteredRooms {
|
||||
const filterUndefined = (array: (FilterKey | undefined)[]): FilterKey[] =>
|
||||
array.filter((f) => f !== undefined) as FilterKey[];
|
||||
|
||||
const getAppliedFilters = useCallback((): FilterKey[] => {
|
||||
const getAppliedFilters = (): FilterKey[] => {
|
||||
return filterUndefined([primaryFilter]);
|
||||
}, [primaryFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
// Update the rooms state when the primary filter changes
|
||||
const filters = getAppliedFilters();
|
||||
updateRoomsFromStore(filters);
|
||||
}, [getAppliedFilters, updateRoomsFromStore]);
|
||||
};
|
||||
|
||||
useEventEmitter(RoomListStoreV3.instance, LISTS_UPDATE_EVENT, () => {
|
||||
const filters = getAppliedFilters();
|
||||
@@ -131,6 +122,6 @@ export function useFilteredRooms(): FilteredRooms {
|
||||
isLoadingRooms,
|
||||
primaryFilters,
|
||||
activePrimaryFilter,
|
||||
roomsResult,
|
||||
rooms,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import { Action } from "../../../dispatcher/actions";
|
||||
import type { Room } from "matrix-js-sdk/src/matrix";
|
||||
import type { Optional } from "matrix-events-sdk";
|
||||
import SpaceStore from "../../../stores/spaces/SpaceStore";
|
||||
import { type RoomsResult } from "../../../stores/room-list-v3/RoomListStoreV3";
|
||||
|
||||
function getIndexByRoomId(rooms: Room[], roomId: Optional<string>): number | undefined {
|
||||
const index = rooms.findIndex((room) => room.roomId === roomId);
|
||||
@@ -68,11 +67,11 @@ function getRoomsWithStickyRoom(
|
||||
return { newIndex: oldIndex, newRooms };
|
||||
}
|
||||
|
||||
export interface StickyRoomListResult {
|
||||
interface StickyRoomListResult {
|
||||
/**
|
||||
* The rooms result with the active sticky room applied
|
||||
* List of rooms with sticky active room.
|
||||
*/
|
||||
roomsResult: RoomsResult;
|
||||
rooms: Room[];
|
||||
/**
|
||||
* Index of the active room in the room list.
|
||||
*/
|
||||
@@ -86,10 +85,10 @@ export interface StickyRoomListResult {
|
||||
* @param rooms list of rooms
|
||||
* @see {@link StickyRoomListResult} details what this hook returns..
|
||||
*/
|
||||
export function useStickyRoomList(roomsResult: RoomsResult): StickyRoomListResult {
|
||||
const [listState, setListState] = useState<StickyRoomListResult>({
|
||||
activeIndex: getIndexByRoomId(roomsResult.rooms, SdkContextClass.instance.roomViewStore.getRoomId()),
|
||||
roomsResult: roomsResult,
|
||||
export function useStickyRoomList(rooms: Room[]): StickyRoomListResult {
|
||||
const [listState, setListState] = useState<{ index: number | undefined; roomsWithStickyRoom: Room[] }>({
|
||||
index: undefined,
|
||||
roomsWithStickyRoom: rooms,
|
||||
});
|
||||
|
||||
const currentSpaceRef = useRef(SpaceStore.instance.activeSpace);
|
||||
@@ -98,18 +97,13 @@ export function useStickyRoomList(roomsResult: RoomsResult): StickyRoomListResul
|
||||
(newRoomId: string | null, isRoomChange: boolean = false) => {
|
||||
setListState((current) => {
|
||||
const activeRoomId = newRoomId ?? SdkContextClass.instance.roomViewStore.getRoomId();
|
||||
const newActiveIndex = getIndexByRoomId(roomsResult.rooms, activeRoomId);
|
||||
const oldIndex = current.activeIndex;
|
||||
const { newIndex, newRooms } = getRoomsWithStickyRoom(
|
||||
roomsResult.rooms,
|
||||
oldIndex,
|
||||
newActiveIndex,
|
||||
isRoomChange,
|
||||
);
|
||||
return { activeIndex: newIndex, roomsResult: { ...roomsResult, rooms: newRooms } };
|
||||
const newActiveIndex = getIndexByRoomId(rooms, activeRoomId);
|
||||
const oldIndex = current.index;
|
||||
const { newIndex, newRooms } = getRoomsWithStickyRoom(rooms, oldIndex, newActiveIndex, isRoomChange);
|
||||
return { index: newIndex, roomsWithStickyRoom: newRooms };
|
||||
});
|
||||
},
|
||||
[roomsResult],
|
||||
[rooms],
|
||||
);
|
||||
|
||||
// Re-calculate the index when the active room has changed.
|
||||
@@ -121,19 +115,20 @@ export function useStickyRoomList(roomsResult: RoomsResult): StickyRoomListResul
|
||||
useEffect(() => {
|
||||
let newRoomId: string | null = null;
|
||||
let isRoomChange = false;
|
||||
if (currentSpaceRef.current !== roomsResult.spaceId) {
|
||||
const newSpace = SpaceStore.instance.activeSpace;
|
||||
if (currentSpaceRef.current !== newSpace) {
|
||||
/*
|
||||
If the space has changed, we check if we can immediately set the active
|
||||
index to the last opened room in that space. Otherwise, we might see a
|
||||
flicker because of the delay between the space change event and
|
||||
active room change dispatch.
|
||||
*/
|
||||
newRoomId = SpaceStore.instance.getLastSelectedRoomIdForSpace(roomsResult.spaceId);
|
||||
newRoomId = SpaceStore.instance.getLastSelectedRoomIdForSpace(newSpace);
|
||||
isRoomChange = true;
|
||||
currentSpaceRef.current = roomsResult.spaceId;
|
||||
currentSpaceRef.current = newSpace;
|
||||
}
|
||||
updateRoomsAndIndex(newRoomId, isRoomChange);
|
||||
}, [roomsResult, updateRoomsAndIndex]);
|
||||
}, [rooms, updateRoomsAndIndex]);
|
||||
|
||||
return listState;
|
||||
return { activeIndex: listState.index, rooms: listState.roomsWithStickyRoom };
|
||||
}
|
||||
|
||||
@@ -10,8 +10,12 @@ import React from "react";
|
||||
import Modal from "../../../Modal.tsx";
|
||||
import InviteProgressBody from "./InviteProgressBody.tsx";
|
||||
|
||||
interface Props {
|
||||
onFinished: () => void;
|
||||
}
|
||||
|
||||
/** A Modal dialog that pops up while room invites are being sent. */
|
||||
const InviteProgressDialog: React.FC = (_) => {
|
||||
const InviteProgressDialog: React.FC<Props> = (props) => {
|
||||
return <InviteProgressBody />;
|
||||
};
|
||||
|
||||
|
||||
@@ -74,7 +74,6 @@ export default class MAudioBody extends React.PureComponent<IBodyProps, IState>
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
this.state.playback?.destroy();
|
||||
this.state.audioPlayerVm?.dispose();
|
||||
}
|
||||
|
||||
protected get showFileBody(): boolean {
|
||||
|
||||
@@ -12,9 +12,7 @@ import { type MatrixEvent, MsgType } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import DisambiguatedProfile from "./DisambiguatedProfile";
|
||||
import { useRoomMemberProfile } from "../../../hooks/room/useRoomMemberProfile";
|
||||
import ModuleApi from "../../../modules/Api";
|
||||
import { CustomComponentsApi } from "../../../modules/customComponentApi";
|
||||
import { MessageProfileComponentProps } from "@element-hq/element-web-module-api";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
onClick?(): void;
|
||||
@@ -26,25 +24,17 @@ export default function SenderProfile({ mxEvent, onClick, withTooltip }: IProps)
|
||||
userId: mxEvent.getSender(),
|
||||
member: mxEvent.sender,
|
||||
});
|
||||
|
||||
if (mxEvent.getContent().msgtype === MsgType.Emote) {
|
||||
return <></>;
|
||||
}
|
||||
const moduleRenderer = ModuleApi.customComponents.messageProfileRenderer;
|
||||
const renderFn = (moduleProps: MessageProfileComponentProps) => <DisambiguatedProfile
|
||||
fallbackName={moduleProps.mxEvent.sender ?? ""}
|
||||
onClick={moduleProps.onClick}
|
||||
member={moduleProps.member}
|
||||
colored={true}
|
||||
emphasizeDisplayName={true}
|
||||
withTooltip={withTooltip}
|
||||
/>;
|
||||
|
||||
const modProps = {
|
||||
onClick,
|
||||
mxEvent: CustomComponentsApi.getModuleMatrixEvent(mxEvent)!,
|
||||
member: member || undefined,
|
||||
};
|
||||
|
||||
return moduleRenderer ? moduleRenderer(modProps, renderFn) : renderFn(modProps);
|
||||
return mxEvent.getContent().msgtype !== MsgType.Emote ? (
|
||||
<DisambiguatedProfile
|
||||
fallbackName={mxEvent.getSender() ?? ""}
|
||||
onClick={onClick}
|
||||
member={member}
|
||||
colored={true}
|
||||
emphasizeDisplayName={true}
|
||||
withTooltip={withTooltip}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import { Flex } from "../../../../shared-components/utils/Flex";
|
||||
import PresenceLabel from "../../rooms/PresenceLabel";
|
||||
import CopyableText from "../../elements/CopyableText";
|
||||
import { UserInfoHeaderVerificationView } from "./UserInfoHeaderVerificationView";
|
||||
import ModuleApi from "../../../../modules/Api";
|
||||
|
||||
export interface UserInfoHeaderViewProps {
|
||||
member: Member;
|
||||
@@ -49,27 +48,6 @@ export const UserInfoHeaderView: React.FC<UserInfoHeaderViewProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
const moduleRenderer = ModuleApi.customComponents.userInfoRenderer;
|
||||
let usernameSection;
|
||||
if (moduleRenderer && vm.userIdentifier) {
|
||||
usernameSection = moduleRenderer(
|
||||
{
|
||||
userId: vm.userIdentifier,
|
||||
},
|
||||
(props) => <Text size="sm" weight="semibold" className="mx_UserInfo_profile_mxid">
|
||||
<CopyableText getTextToCopy={() => props.userId} border={false}>
|
||||
{props.userId}
|
||||
</CopyableText>
|
||||
</Text>,
|
||||
);
|
||||
} else {
|
||||
usernameSection = <Text size="sm" weight="semibold" className="mx_UserInfo_profile_mxid">
|
||||
<CopyableText getTextToCopy={() => vm.userIdentifier} border={false}>
|
||||
{vm.userIdentifier}
|
||||
</CopyableText>
|
||||
</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="mx_UserInfo_avatar">
|
||||
@@ -105,7 +83,11 @@ export const UserInfoHeaderView: React.FC<UserInfoHeaderViewProps> = ({
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
)}
|
||||
{usernameSection}
|
||||
<Text size="sm" weight="semibold" className="mx_UserInfo_profile_mxid">
|
||||
<CopyableText getTextToCopy={() => vm.userIdentifier} border={false}>
|
||||
{vm.userIdentifier}
|
||||
</CopyableText>
|
||||
</Text>
|
||||
</Flex>
|
||||
{!hideVerificationSection && <UserInfoHeaderVerificationView member={member} devices={devices} />}
|
||||
</Container>
|
||||
|
||||
@@ -28,7 +28,7 @@ interface IProps {
|
||||
|
||||
const MemberListView: React.FC<IProps> = (props: IProps) => {
|
||||
const vm = useMemberListViewModel(props.roomId);
|
||||
const { isPresenceEnabled, memberCount } = vm;
|
||||
const { isPresenceEnabled, onClickMember, memberCount } = vm;
|
||||
|
||||
const getItemKey = useCallback((item: MemberWithSeparator): string => {
|
||||
if (item === SEPARATOR) {
|
||||
@@ -80,6 +80,19 @@ const MemberListView: React.FC<IProps> = (props: IProps) => {
|
||||
[isPresenceEnabled, getItemKey, memberCount],
|
||||
);
|
||||
|
||||
const handleSelectItem = useCallback(
|
||||
(item: MemberWithSeparator): void => {
|
||||
if (item !== SEPARATOR) {
|
||||
if (item.member) {
|
||||
onClickMember(item.member);
|
||||
} else {
|
||||
onClickMember(item.threePidInvite);
|
||||
}
|
||||
}
|
||||
},
|
||||
[onClickMember],
|
||||
);
|
||||
|
||||
const isItemFocusable = useCallback((item: MemberWithSeparator): boolean => {
|
||||
return item !== SEPARATOR;
|
||||
}, []);
|
||||
@@ -99,6 +112,7 @@ const MemberListView: React.FC<IProps> = (props: IProps) => {
|
||||
</Form.Root>
|
||||
<ListView
|
||||
items={vm.members}
|
||||
onSelectItem={handleSelectItem}
|
||||
getItemComponent={getItemComponent}
|
||||
getItemKey={getItemKey}
|
||||
isItemFocusable={isItemFocusable}
|
||||
|
||||
@@ -5,16 +5,13 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useRef, type JSX } from "react";
|
||||
import { type Room } from "matrix-js-sdk/src/matrix";
|
||||
import { type ScrollIntoViewLocation } from "react-virtuoso";
|
||||
import { isEqual } from "lodash";
|
||||
import React, { useCallback, type JSX } from "react";
|
||||
import { AutoSizer, List, type ListRowProps } from "react-virtualized";
|
||||
|
||||
import { type RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { RoomListItemView } from "./RoomListItemView";
|
||||
import { type ListContext, ListView } from "../../../utils/ListView";
|
||||
import { type FilterKey } from "../../../../stores/room-list-v3/skip-list/filters";
|
||||
import { RovingTabIndexProvider } from "../../../../accessibility/RovingTabIndex";
|
||||
import { getKeyBindingsManager } from "../../../../KeyBindingsManager";
|
||||
import { KeyBindingAction } from "../../../../accessibility/KeyboardShortcuts";
|
||||
import { Landmark, LandmarkNavigation } from "../../../../accessibility/LandmarkNavigation";
|
||||
@@ -29,93 +26,55 @@ interface RoomListProps {
|
||||
/**
|
||||
* A virtualized list of rooms.
|
||||
*/
|
||||
export function RoomList({ vm: { roomsResult, activeIndex } }: RoomListProps): JSX.Element {
|
||||
const lastSpaceId = useRef<string | undefined>(undefined);
|
||||
const lastFilterKeys = useRef<FilterKey[] | undefined>(undefined);
|
||||
const roomCount = roomsResult.rooms.length;
|
||||
const getItemComponent = useCallback(
|
||||
(
|
||||
index: number,
|
||||
item: Room,
|
||||
context: ListContext<{
|
||||
spaceId: string;
|
||||
filterKeys: FilterKey[] | undefined;
|
||||
}>,
|
||||
onFocus: (e: React.FocusEvent) => void,
|
||||
): JSX.Element => {
|
||||
const itemKey = item.roomId;
|
||||
const isRovingItem = itemKey === context.tabIndexKey;
|
||||
const isFocused = isRovingItem && context.focused;
|
||||
const isSelected = activeIndex === index;
|
||||
return (
|
||||
<RoomListItemView
|
||||
room={item}
|
||||
key={itemKey}
|
||||
isSelected={isSelected}
|
||||
isFocused={isFocused}
|
||||
tabIndex={isRovingItem ? 0 : -1}
|
||||
roomIndex={index}
|
||||
roomCount={roomCount}
|
||||
onFocus={onFocus}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[activeIndex, roomCount],
|
||||
export function RoomList({ vm: { rooms, activeIndex } }: RoomListProps): JSX.Element {
|
||||
const roomRendererMemoized = useCallback(
|
||||
({ key, index, style }: ListRowProps) => (
|
||||
<RoomListItemView room={rooms[index]} key={key} style={style} isSelected={activeIndex === index} />
|
||||
),
|
||||
[rooms, activeIndex],
|
||||
);
|
||||
|
||||
const getItemKey = useCallback((item: Room): string => {
|
||||
return item.roomId;
|
||||
}, []);
|
||||
|
||||
const scrollIntoViewOnChange = useCallback(
|
||||
(params: {
|
||||
context: ListContext<{ spaceId: string; filterKeys: FilterKey[] | undefined }>;
|
||||
}): ScrollIntoViewLocation | null | undefined | false | void => {
|
||||
const { spaceId, filterKeys } = params.context.context;
|
||||
const shouldScrollIndexIntoView =
|
||||
lastSpaceId.current !== spaceId || !isEqual(lastFilterKeys.current, filterKeys);
|
||||
lastFilterKeys.current = filterKeys;
|
||||
lastSpaceId.current = spaceId;
|
||||
|
||||
if (shouldScrollIndexIntoView) {
|
||||
return {
|
||||
align: `start`,
|
||||
index: activeIndex || 0,
|
||||
behavior: "auto",
|
||||
};
|
||||
}
|
||||
return false;
|
||||
},
|
||||
[activeIndex],
|
||||
);
|
||||
|
||||
const keyDownCallback = useCallback((ev: React.KeyboardEvent) => {
|
||||
const navAction = getKeyBindingsManager().getNavigationAction(ev);
|
||||
if (navAction === KeyBindingAction.NextLandmark || navAction === KeyBindingAction.PreviousLandmark) {
|
||||
LandmarkNavigation.findAndFocusNextLandmark(
|
||||
Landmark.ROOM_LIST,
|
||||
navAction === KeyBindingAction.PreviousLandmark,
|
||||
);
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
return;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// The first div is needed to make the virtualized list take all the remaining space and scroll correctly
|
||||
return (
|
||||
<ListView
|
||||
context={{ spaceId: roomsResult.spaceId, filterKeys: roomsResult.filterKeys }}
|
||||
scrollIntoViewOnChange={scrollIntoViewOnChange}
|
||||
initialTopMostItemIndex={activeIndex}
|
||||
data-testid="room-list"
|
||||
role="listbox"
|
||||
aria-label={_t("room_list|list_title")}
|
||||
fixedItemHeight={48}
|
||||
items={roomsResult.rooms}
|
||||
getItemComponent={getItemComponent}
|
||||
getItemKey={getItemKey}
|
||||
isItemFocusable={() => true}
|
||||
onKeyDown={keyDownCallback}
|
||||
/>
|
||||
<RovingTabIndexProvider handleHomeEnd={true} handleUpDown={true}>
|
||||
{({ onKeyDownHandler }) => (
|
||||
<div
|
||||
className="mx_RoomList"
|
||||
data-testid="room-list"
|
||||
onKeyDown={(ev) => {
|
||||
const navAction = getKeyBindingsManager().getNavigationAction(ev);
|
||||
if (
|
||||
navAction === KeyBindingAction.NextLandmark ||
|
||||
navAction === KeyBindingAction.PreviousLandmark
|
||||
) {
|
||||
LandmarkNavigation.findAndFocusNextLandmark(
|
||||
Landmark.ROOM_LIST,
|
||||
navAction === KeyBindingAction.PreviousLandmark,
|
||||
);
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
return;
|
||||
}
|
||||
onKeyDownHandler(ev);
|
||||
}}
|
||||
>
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<List
|
||||
aria-label={_t("room_list|list_title")}
|
||||
className="mx_RoomList_List"
|
||||
rowRenderer={roomRendererMemoized}
|
||||
rowCount={rooms.length}
|
||||
rowHeight={48}
|
||||
height={height}
|
||||
width={width}
|
||||
scrollToIndex={activeIndex ?? 0}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
)}
|
||||
</RovingTabIndexProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -97,10 +97,7 @@ interface MoreOptionContentProps {
|
||||
|
||||
export function MoreOptionContent({ vm }: MoreOptionContentProps): JSX.Element {
|
||||
return (
|
||||
<div
|
||||
// We don't want keyboard navigation events to bubble up to the ListView changing the focused item
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<>
|
||||
{vm.canMarkAsRead && (
|
||||
<MenuItem
|
||||
Icon={MarkAsReadIcon}
|
||||
@@ -160,7 +157,7 @@ export function MoreOptionContent({ vm }: MoreOptionContentProps): JSX.Element {
|
||||
onClick={(evt) => evt.stopPropagation()}
|
||||
hideChevron={true}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -199,59 +196,54 @@ function NotificationMenu({ vm, setMenuOpen }: NotificationMenuProps): JSX.Eleme
|
||||
const checkComponent = <CheckIcon width="24px" height="24px" color="var(--cpd-color-icon-primary)" />;
|
||||
|
||||
return (
|
||||
<div
|
||||
// We don't want keyboard navigation events to bubble up to the ListView changing the focused item
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
<Menu
|
||||
open={open}
|
||||
onOpenChange={(isOpen) => {
|
||||
setOpen(isOpen);
|
||||
setMenuOpen(isOpen);
|
||||
}}
|
||||
title={_t("room_list|notification_options")}
|
||||
showTitle={false}
|
||||
align="start"
|
||||
trigger={<NotificationButton isRoomMuted={vm.isNotificationMute} size="24px" />}
|
||||
>
|
||||
<Menu
|
||||
open={open}
|
||||
onOpenChange={(isOpen) => {
|
||||
setOpen(isOpen);
|
||||
setMenuOpen(isOpen);
|
||||
}}
|
||||
title={_t("room_list|notification_options")}
|
||||
showTitle={false}
|
||||
align="start"
|
||||
trigger={<NotificationButton isRoomMuted={vm.isNotificationMute} size="24px" />}
|
||||
<MenuItem
|
||||
aria-selected={vm.isNotificationAllMessage}
|
||||
hideChevron={true}
|
||||
label={_t("notifications|default_settings")}
|
||||
onSelect={() => vm.setRoomNotifState(RoomNotifState.AllMessages)}
|
||||
onClick={(evt) => evt.stopPropagation()}
|
||||
>
|
||||
<MenuItem
|
||||
aria-selected={vm.isNotificationAllMessage}
|
||||
hideChevron={true}
|
||||
label={_t("notifications|default_settings")}
|
||||
onSelect={() => vm.setRoomNotifState(RoomNotifState.AllMessages)}
|
||||
onClick={(evt) => evt.stopPropagation()}
|
||||
>
|
||||
{vm.isNotificationAllMessage && checkComponent}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
aria-selected={vm.isNotificationAllMessageLoud}
|
||||
hideChevron={true}
|
||||
label={_t("notifications|all_messages")}
|
||||
onSelect={() => vm.setRoomNotifState(RoomNotifState.AllMessagesLoud)}
|
||||
onClick={(evt) => evt.stopPropagation()}
|
||||
>
|
||||
{vm.isNotificationAllMessageLoud && checkComponent}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
aria-selected={vm.isNotificationMentionOnly}
|
||||
hideChevron={true}
|
||||
label={_t("notifications|mentions_keywords")}
|
||||
onSelect={() => vm.setRoomNotifState(RoomNotifState.MentionsOnly)}
|
||||
onClick={(evt) => evt.stopPropagation()}
|
||||
>
|
||||
{vm.isNotificationMentionOnly && checkComponent}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
aria-selected={vm.isNotificationMute}
|
||||
hideChevron={true}
|
||||
label={_t("notifications|mute_room")}
|
||||
onSelect={() => vm.setRoomNotifState(RoomNotifState.Mute)}
|
||||
onClick={(evt) => evt.stopPropagation()}
|
||||
>
|
||||
{vm.isNotificationMute && checkComponent}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
{vm.isNotificationAllMessage && checkComponent}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
aria-selected={vm.isNotificationAllMessageLoud}
|
||||
hideChevron={true}
|
||||
label={_t("notifications|all_messages")}
|
||||
onSelect={() => vm.setRoomNotifState(RoomNotifState.AllMessagesLoud)}
|
||||
onClick={(evt) => evt.stopPropagation()}
|
||||
>
|
||||
{vm.isNotificationAllMessageLoud && checkComponent}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
aria-selected={vm.isNotificationMentionOnly}
|
||||
hideChevron={true}
|
||||
label={_t("notifications|mentions_keywords")}
|
||||
onSelect={() => vm.setRoomNotifState(RoomNotifState.MentionsOnly)}
|
||||
onClick={(evt) => evt.stopPropagation()}
|
||||
>
|
||||
{vm.isNotificationMentionOnly && checkComponent}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
aria-selected={vm.isNotificationMute}
|
||||
hideChevron={true}
|
||||
label={_t("notifications|mute_room")}
|
||||
onSelect={() => vm.setRoomNotifState(RoomNotifState.Mute)}
|
||||
onClick={(evt) => evt.stopPropagation()}
|
||||
>
|
||||
{vm.isNotificationMute && checkComponent}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type JSX, memo, useCallback, useEffect, useRef, useState } from "react";
|
||||
import React, { type JSX, memo, useCallback, useRef, useState } from "react";
|
||||
import { type Room } from "matrix-js-sdk/src/matrix";
|
||||
import classNames from "classnames";
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Flex } from "../../../../shared-components/utils/Flex";
|
||||
import { RoomListItemMenuView } from "./RoomListItemMenuView";
|
||||
import { NotificationDecoration } from "../NotificationDecoration";
|
||||
import { RoomAvatarView } from "../../avatars/RoomAvatarView";
|
||||
import { useRovingTabIndex } from "../../../../accessibility/RovingTabIndex";
|
||||
import { RoomListItemContextMenuView } from "./RoomListItemContextMenuView";
|
||||
|
||||
interface RoomListItemViewProps extends React.HTMLAttributes<HTMLButtonElement> {
|
||||
@@ -25,22 +26,6 @@ interface RoomListItemViewProps extends React.HTMLAttributes<HTMLButtonElement>
|
||||
* Whether the room is selected
|
||||
*/
|
||||
isSelected: boolean;
|
||||
/**
|
||||
* Whether the room is focused
|
||||
*/
|
||||
isFocused: boolean;
|
||||
/**
|
||||
* A callback that indicates the item has received focus
|
||||
*/
|
||||
onFocus: (e: React.FocusEvent) => void;
|
||||
/**
|
||||
* The index of the room in the list
|
||||
*/
|
||||
roomIndex: number;
|
||||
/**
|
||||
* The total number of rooms in the list
|
||||
*/
|
||||
roomCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,19 +34,18 @@ interface RoomListItemViewProps extends React.HTMLAttributes<HTMLButtonElement>
|
||||
export const RoomListItemView = memo(function RoomListItemView({
|
||||
room,
|
||||
isSelected,
|
||||
isFocused,
|
||||
onFocus,
|
||||
roomIndex: index,
|
||||
roomCount: count,
|
||||
...props
|
||||
}: RoomListItemViewProps): JSX.Element {
|
||||
const ref = useRef<HTMLButtonElement>(null);
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const [onFocus, isActive, ref] = useRovingTabIndex(buttonRef);
|
||||
|
||||
const vm = useRoomListItemViewModel(room);
|
||||
const [isHover, setHover] = useState(false);
|
||||
|
||||
const [isHover, setIsHoverWithDelay] = useIsHover();
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
// The compound menu in RoomListItemMenuView needs to be rendered when the hover menu is shown
|
||||
// Using display: none; and then display:flex when hovered in CSS causes the menu to be misaligned
|
||||
const showHoverDecoration = isMenuOpen || isFocused || isHover;
|
||||
const showHoverDecoration = isMenuOpen || isHover;
|
||||
const showHoverMenu = showHoverDecoration && vm.showHoverMenu;
|
||||
|
||||
const closeMenu = useCallback(() => {
|
||||
@@ -70,15 +54,8 @@ export const RoomListItemView = memo(function RoomListItemView({
|
||||
setTimeout(() => setIsMenuOpen(false), 10);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isFocused) {
|
||||
ref.current?.focus({ preventScroll: true, focusVisible: true });
|
||||
}
|
||||
}, [isFocused]);
|
||||
|
||||
const content = (
|
||||
<Flex
|
||||
as="button"
|
||||
<button
|
||||
ref={ref}
|
||||
className={classNames("mx_RoomListItemView", {
|
||||
mx_RoomListItemView_hover: showHoverDecoration,
|
||||
@@ -86,59 +63,63 @@ export const RoomListItemView = memo(function RoomListItemView({
|
||||
mx_RoomListItemView_selected: isSelected,
|
||||
mx_RoomListItemView_bold: vm.isBold,
|
||||
})}
|
||||
gap="var(--cpd-space-3x)"
|
||||
align="center"
|
||||
type="button"
|
||||
role="option"
|
||||
aria-posinset={index + 1}
|
||||
aria-setsize={count}
|
||||
aria-selected={isSelected}
|
||||
aria-label={vm.a11yLabel}
|
||||
onClick={() => vm.openRoom()}
|
||||
onFocus={onFocus}
|
||||
onMouseOver={() => setHover(true)}
|
||||
onMouseOut={() => setHover(false)}
|
||||
onBlur={() => setHover(false)}
|
||||
tabIndex={isFocused ? 0 : -1}
|
||||
onMouseOver={() => setIsHoverWithDelay(true)}
|
||||
onMouseOut={() => setIsHoverWithDelay(false)}
|
||||
onFocus={() => {
|
||||
setIsHoverWithDelay(true);
|
||||
onFocus();
|
||||
}}
|
||||
// Adding a timeout because when tabbing to go to the more options and notification menu, the focus moves out of the button
|
||||
// The blur makes the button lose the hover state and these menu are not shown
|
||||
// We delay the blur event to give time to the focus to move to the menu
|
||||
onBlur={() => setIsHoverWithDelay(false, 10)}
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
{...props}
|
||||
>
|
||||
<RoomAvatarView room={room} />
|
||||
<Flex
|
||||
className="mx_RoomListItemView_content"
|
||||
gap="var(--cpd-space-2x)"
|
||||
align="center"
|
||||
justify="space-between"
|
||||
>
|
||||
{/* We truncate the room name when too long. Title here is to show the full name on hover */}
|
||||
<div className="mx_RoomListItemView_text">
|
||||
<div className="mx_RoomListItemView_roomName" title={vm.name}>
|
||||
{vm.name}
|
||||
</div>
|
||||
{vm.messagePreview && (
|
||||
<div className="mx_RoomListItemView_messagePreview" title={vm.messagePreview}>
|
||||
{vm.messagePreview}
|
||||
{/* We need this extra div between the button and the content in order to add a padding which is not messing with the virtualized list */}
|
||||
<Flex className="mx_RoomListItemView_container" gap="var(--cpd-space-3x)" align="center">
|
||||
<RoomAvatarView room={room} />
|
||||
<Flex
|
||||
className="mx_RoomListItemView_content"
|
||||
gap="var(--cpd-space-2x)"
|
||||
align="center"
|
||||
justify="space-between"
|
||||
>
|
||||
{/* We truncate the room name when too long. Title here is to show the full name on hover */}
|
||||
<div className="mx_RoomListItemView_text">
|
||||
<div className="mx_RoomListItemView_roomName" title={vm.name}>
|
||||
{vm.name}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{showHoverMenu ? (
|
||||
<RoomListItemMenuView
|
||||
room={room}
|
||||
setMenuOpen={(isOpen) => (isOpen ? setIsMenuOpen(true) : closeMenu())}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{/* aria-hidden because we summarise the unread count/notification status in a11yLabel variable */}
|
||||
{vm.showNotificationDecoration && (
|
||||
<NotificationDecoration
|
||||
notificationState={vm.notificationState}
|
||||
aria-hidden={true}
|
||||
hasVideoCall={vm.hasParticipantInCall}
|
||||
/>
|
||||
{vm.messagePreview && (
|
||||
<div className="mx_RoomListItemView_messagePreview" title={vm.messagePreview}>
|
||||
{vm.messagePreview}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{showHoverMenu ? (
|
||||
<RoomListItemMenuView
|
||||
room={room}
|
||||
setMenuOpen={(isOpen) => (isOpen ? setIsMenuOpen(true) : closeMenu())}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{/* aria-hidden because we summarise the unread count/notification status in a11yLabel variable */}
|
||||
{vm.showNotificationDecoration && (
|
||||
<NotificationDecoration
|
||||
notificationState={vm.notificationState}
|
||||
aria-hidden={true}
|
||||
hasVideoCall={vm.hasParticipantInCall}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</button>
|
||||
);
|
||||
|
||||
if (!vm.showContextMenu) return content;
|
||||
@@ -159,3 +140,33 @@ export const RoomListItemView = memo(function RoomListItemView({
|
||||
</RoomListItemContextMenuView>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Custom hook to manage the hover state of the room list item
|
||||
* If the timeout is set, it will set the hover state after the timeout
|
||||
* If the timeout is not set, it will set the hover state immediately
|
||||
* When the set method is called, it will clear any existing timeout
|
||||
*
|
||||
* @returns {boolean} isHover - The hover state
|
||||
*/
|
||||
function useIsHover(): [boolean, (value: boolean, timeout?: number) => void] {
|
||||
const [isHover, setIsHover] = useState(false);
|
||||
// Store the timeout ID
|
||||
const timeoutRef = useRef<number | undefined>(undefined);
|
||||
|
||||
const setIsHoverWithDelay = useCallback((value: boolean, timeout?: number): void => {
|
||||
// Clear the timeout if it exists
|
||||
clearTimeout(timeoutRef.current);
|
||||
|
||||
// No delay, set the value immediately
|
||||
if (timeout === undefined) {
|
||||
setIsHover(value);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set a timeout to set the value after the delay
|
||||
timeoutRef.current = setTimeout(() => setIsHover(value), timeout);
|
||||
}, []);
|
||||
|
||||
return [isHover, setIsHoverWithDelay];
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import { RoomListPrimaryFilters } from "./RoomListPrimaryFilters";
|
||||
*/
|
||||
export function RoomListView(): JSX.Element {
|
||||
const vm = useRoomListViewModel();
|
||||
const isRoomListEmpty = vm.roomsResult.rooms.length === 0;
|
||||
const isRoomListEmpty = vm.rooms.length === 0;
|
||||
let listBody;
|
||||
if (vm.isLoadingRooms) {
|
||||
listBody = <div className="mx_RoomListSkeleton" />;
|
||||
|
||||
@@ -13,9 +13,9 @@ import {
|
||||
Field,
|
||||
IconButton,
|
||||
Label,
|
||||
PasswordControl,
|
||||
Root,
|
||||
Text,
|
||||
TextControl,
|
||||
} from "@vector-im/compound-web";
|
||||
import CopyIcon from "@vector-im/compound-design-tokens/assets/web/icons/copy";
|
||||
import KeyIcon from "@vector-im/compound-design-tokens/assets/web/icons/key-solid";
|
||||
@@ -350,11 +350,7 @@ function KeyForm({ onCancelClick, onSubmit, recoveryKey, submitButtonLabel }: Ke
|
||||
<Field name="recoveryKey" serverInvalid={isKeyInvalidAndFilled}>
|
||||
<Label>{_t("settings|encryption|recovery|enter_recovery_key")}</Label>
|
||||
|
||||
<PasswordControl
|
||||
required={true}
|
||||
title={_t("settings|encryption|recovery|enter_recovery_key")}
|
||||
className="mx_KeyForm_password mx_no_textinput"
|
||||
/>
|
||||
<TextControl required={true} />
|
||||
{isKeyInvalidAndFilled && (
|
||||
<ErrorMessage>{_t("settings|encryption|recovery|enter_key_error")}</ErrorMessage>
|
||||
)}
|
||||
|
||||
@@ -1921,7 +1921,6 @@
|
||||
"thread_list": {
|
||||
"context_menu_label": "Thread options"
|
||||
},
|
||||
"title": "Right panel",
|
||||
"video_room_chat": {
|
||||
"title": "Chat"
|
||||
}
|
||||
|
||||
@@ -1463,7 +1463,7 @@
|
||||
"toggle_hidden_events": "Lülita peidetud sündmuste näitamine sisse/välja",
|
||||
"toggle_microphone_mute": "Lülita mikrofoni summutamine sisse/välja",
|
||||
"toggle_right_panel": "Lülita parem paan sisse/välja",
|
||||
"toggle_space_panel": "Lülita kogukondade paan sisse/välja",
|
||||
"toggle_space_panel": "Lülita kogukondade riba sisse/välja",
|
||||
"toggle_top_left_menu": "Lülita ülemine vasak menüü sisse/välja",
|
||||
"toggle_webcam_mute": "Lülita veebikaamera sisse/välja",
|
||||
"upload_file": "Laadi fail üles"
|
||||
@@ -1917,7 +1917,6 @@
|
||||
"thread_list": {
|
||||
"context_menu_label": "Jutulõnga valikud"
|
||||
},
|
||||
"title": "Parem paan",
|
||||
"video_room_chat": {
|
||||
"title": "Vestle"
|
||||
}
|
||||
|
||||
@@ -1916,7 +1916,6 @@
|
||||
"thread_list": {
|
||||
"context_menu_label": "Options des fils de discussion"
|
||||
},
|
||||
"title": "Panneau droit",
|
||||
"video_room_chat": {
|
||||
"title": "Conversation privée"
|
||||
}
|
||||
|
||||
@@ -1904,7 +1904,6 @@
|
||||
"thread_list": {
|
||||
"context_menu_label": "Üzenetszál beállításai"
|
||||
},
|
||||
"title": "Jobb oldali panel",
|
||||
"video_room_chat": {
|
||||
"title": "Csevegés"
|
||||
}
|
||||
|
||||
@@ -1366,10 +1366,6 @@
|
||||
"name_email_mxid_share_space": "Inviter noen ved å bruke navn, e-postadresse, brukernavn (lik <userId/>) eller <a> del dette området</a>.",
|
||||
"name_mxid_share_room": "Inviter noen ved å bruke navnet, brukernavnet (som <userId/>) eller <a>del dette rommet</a>.",
|
||||
"name_mxid_share_space": "Inviter noen ved å bruke navnet sitt, brukernavnet (lik <userId/>) eller <a> dele dette området</a>.",
|
||||
"progress": {
|
||||
"dont_close": "Ikke lukk appen før den er ferdig.",
|
||||
"preparing": "Forbereder invitasjoner..."
|
||||
},
|
||||
"recents_section": "Nylige samtaler",
|
||||
"room_failed_partial": "Vi sendte de andre, men folkene nedenfor kunne ikke inviteres til <RoomName/>",
|
||||
"room_failed_partial_title": "Noen invitasjoner kunne ikke sendes",
|
||||
@@ -1921,7 +1917,6 @@
|
||||
"thread_list": {
|
||||
"context_menu_label": "Trådalternativer"
|
||||
},
|
||||
"title": "Høyre panel",
|
||||
"video_room_chat": {
|
||||
"title": "Chat"
|
||||
}
|
||||
|
||||
@@ -1380,10 +1380,6 @@
|
||||
"name_email_mxid_share_space": "Pozvite niekoho pomocou jeho mena, e-mailovej adresy, používateľského mena (napríklad <userId/>) alebo <a>zdieľajte tento priestor</a>.",
|
||||
"name_mxid_share_room": "Pozvite niekoho pomocou jeho mena, používateľského mena (napríklad <userId/>) alebo <a>zdieľate túto miestnosť</a>.",
|
||||
"name_mxid_share_space": "Pozvite niekoho pomocou jeho mena, používateľského mena (napríklad <userId/>) alebo <a>zdieľajte tento priestor</a>.",
|
||||
"progress": {
|
||||
"dont_close": "Nezatvárajte aplikáciu, kým nie je proces dokončený.",
|
||||
"preparing": "Príprava pozvánok..."
|
||||
},
|
||||
"recents_section": "Nedávne konverzácie",
|
||||
"room_failed_partial": "Ostatným sme pozvánky poslali, ale nižšie uvedené osoby nemohli byť pozvané do <RoomName/>",
|
||||
"room_failed_partial_title": "Niektoré pozvánky nebolo možné odoslať",
|
||||
@@ -1786,7 +1782,6 @@
|
||||
},
|
||||
"power_level": {
|
||||
"admin": "Správca",
|
||||
"creator": "Vlastník",
|
||||
"custom": "Vlastný (%(level)s)",
|
||||
"custom_level": "Vlastná úroveň",
|
||||
"default": "Predvolené",
|
||||
@@ -1945,7 +1940,6 @@
|
||||
"thread_list": {
|
||||
"context_menu_label": "Možnosti vlákna"
|
||||
},
|
||||
"title": "Pravý panel",
|
||||
"video_room_chat": {
|
||||
"title": "Konverzácia"
|
||||
}
|
||||
|
||||
@@ -1366,10 +1366,6 @@
|
||||
"name_email_mxid_share_space": "Bjud in någon med deras namn, e-postadress eller användarnamn (som <userId/>), eller <a>dela det här rummet</a>.",
|
||||
"name_mxid_share_room": "Bjud in någon med deras namn eller användarnamn (som <userId/>) eller <a>dela det här rummet</a>.",
|
||||
"name_mxid_share_space": "Bjud in någon med deras namn eller användarnamn (som <userId/>), eller <a>dela det här utrymmet</a>.",
|
||||
"progress": {
|
||||
"dont_close": "Stäng inte appen förrän det är klart.",
|
||||
"preparing": "Förbereder inbjudningar …"
|
||||
},
|
||||
"recents_section": "Senaste konversationerna",
|
||||
"room_failed_partial": "Vi skickade de andra, men personerna nedan kunde inte bjudas in till <RoomName/>",
|
||||
"room_failed_partial_title": "Vissa inbjudningar kunde inte skickas",
|
||||
@@ -1767,7 +1763,6 @@
|
||||
},
|
||||
"power_level": {
|
||||
"admin": "Administratör",
|
||||
"creator": "Ägare",
|
||||
"custom": "Anpassad (%(level)s)",
|
||||
"custom_level": "Anpassad nivå",
|
||||
"default": "Standard",
|
||||
@@ -1920,7 +1915,6 @@
|
||||
"thread_list": {
|
||||
"context_menu_label": "Trådalternativ"
|
||||
},
|
||||
"title": "Högerpanel",
|
||||
"video_room_chat": {
|
||||
"title": "Chatt"
|
||||
}
|
||||
|
||||
@@ -646,7 +646,7 @@
|
||||
"mode_plain": "Сховати форматування",
|
||||
"mode_rich_text": "Показати форматування",
|
||||
"no_perms_notice": "У вас немає дозволу писати в цій кімнаті",
|
||||
"placeholder": "Надіслати незашифроване повідомлення...",
|
||||
"placeholder": "Надіслати незашифроване message...",
|
||||
"placeholder_encrypted": "Надіслати повідомлення…",
|
||||
"placeholder_reply": "Надіслати незашифровану відповідь…",
|
||||
"placeholder_reply_encrypted": "Надіслати відповідь…",
|
||||
@@ -923,8 +923,7 @@
|
||||
},
|
||||
"privacy_warning": "Переконайтеся, що ніхто не бачить цей екран!",
|
||||
"restoring": "Відновлення ключів із резервної копії",
|
||||
"security_key_label": "Ключ відновлення",
|
||||
"security_key_title": "Введіть ключ відновлення"
|
||||
"security_key_title": "Ключ відновлення"
|
||||
},
|
||||
"bootstrap_title": "Налаштовування ключів",
|
||||
"confirm_encryption_setup_body": "Клацніть на кнопку внизу, щоб підтвердити налаштування шифрування.",
|
||||
@@ -1537,9 +1536,6 @@
|
||||
"render_reaction_images_description": "Іноді їх називають \"користувацькими емодзі\".",
|
||||
"report_to_moderators": "Поскаржитись модераторам",
|
||||
"report_to_moderators_description": "У кімнатах, які підтримують модерацію, кнопка «Поскаржитися» дає змогу повідомити про зловживання модераторам кімнати.",
|
||||
"share_history_on_invite": "Ділитися зашифрованою історією з новими учасниками",
|
||||
"share_history_on_invite_description": "Коли запрошуєте користувача до зашифрованої кімнати, в якій видимість історії встановлено як «спільна», поділіться зашифрованою історією з цим користувачем і прийміть зашифровану історію, коли вас запросять до такої кімнати.",
|
||||
"share_history_on_invite_warning": "Ця функція ЕКСПЕРИМЕНТАЛЬНА, і впроваджено не всі заходи безпеки. Не вмикайте її на робочих облікових записах.",
|
||||
"sliding_sync": "Режим ковзної синхронізації",
|
||||
"sliding_sync_description": "На стадії активної розробки, вимкнути не можна.",
|
||||
"sliding_sync_disabled_notice": "Вийдіть і знову увійдіть, щоб вимкнути",
|
||||
@@ -1662,7 +1658,6 @@
|
||||
"filter_placeholder": "Відфільтрувати учасників кімнати",
|
||||
"invite_button_no_perms_tooltip": "У вас немає дозволу запрошувати користувачів",
|
||||
"invited_label": "Запрошено",
|
||||
"list_title": "Список учасників",
|
||||
"no_matches": "Немає збігів"
|
||||
},
|
||||
"member_list_back_action_label": "Учасники кімнати",
|
||||
@@ -1767,7 +1762,6 @@
|
||||
},
|
||||
"power_level": {
|
||||
"admin": "Адміністратор",
|
||||
"creator": "Власник",
|
||||
"custom": "Власний (%(level)s)",
|
||||
"custom_level": "Власний рівень",
|
||||
"default": "Типовий",
|
||||
@@ -1921,7 +1915,6 @@
|
||||
"thread_list": {
|
||||
"context_menu_label": "Параметри гілки"
|
||||
},
|
||||
"title": "Права панель",
|
||||
"video_room_chat": {
|
||||
"title": "Бесіда"
|
||||
}
|
||||
@@ -3406,7 +3399,6 @@
|
||||
"unable_to_find": "Не вдалося знайти вказаної позиції в стрічці цієї кімнати."
|
||||
},
|
||||
"m.audio": {
|
||||
"audio_player": "Звуковий програвач",
|
||||
"error_downloading_audio": "Помилка завантаження аудіо",
|
||||
"error_processing_audio": "Помилка обробки аудіоповідомлення",
|
||||
"error_processing_voice_message": "Помилка обробки голосового повідомлення",
|
||||
|
||||
@@ -16,8 +16,6 @@ import type {
|
||||
CustomMessageRenderHints as ModuleCustomCustomMessageRenderHints,
|
||||
MatrixEvent as ModuleMatrixEvent,
|
||||
CustomRoomPreviewBarRenderFunction,
|
||||
MessageProfileRenderFunction,
|
||||
UserInfoRenderFunction,
|
||||
} from "@element-hq/element-web-module-api";
|
||||
import type React from "react";
|
||||
|
||||
@@ -44,7 +42,7 @@ export class CustomComponentsApi implements ICustomComponentsApi {
|
||||
* @param mxEvent
|
||||
* @returns An event object, or `null` if the event was not a message event.
|
||||
*/
|
||||
public static getModuleMatrixEvent(mxEvent: MatrixEvent): ModuleMatrixEvent | null {
|
||||
private static getModuleMatrixEvent(mxEvent: MatrixEvent): ModuleMatrixEvent | null {
|
||||
const eventId = mxEvent.getId();
|
||||
const roomId = mxEvent.getRoomId();
|
||||
const sender = mxEvent.sender;
|
||||
@@ -140,8 +138,6 @@ export class CustomComponentsApi implements ICustomComponentsApi {
|
||||
}
|
||||
|
||||
private _roomPreviewBarRenderer?: CustomRoomPreviewBarRenderFunction;
|
||||
private _userInfoRenderer?: UserInfoRenderFunction;
|
||||
private _messageProfileRenderer?: MessageProfileRenderFunction;
|
||||
|
||||
/**
|
||||
* Get the custom room preview bar renderer, if any has been registered.
|
||||
@@ -157,25 +153,4 @@ export class CustomComponentsApi implements ICustomComponentsApi {
|
||||
public registerRoomPreviewBar(renderer: CustomRoomPreviewBarRenderFunction): void {
|
||||
this._roomPreviewBarRenderer = renderer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the custom user info renderer, if any has been registered.
|
||||
*/
|
||||
public get messageProfileRenderer(): MessageProfileRenderFunction | undefined {
|
||||
return this._messageProfileRenderer;
|
||||
}
|
||||
|
||||
public registerMessageProfile(renderer: MessageProfileRenderFunction): void {
|
||||
this._messageProfileRenderer = renderer;
|
||||
}
|
||||
/**
|
||||
* Get the custom user info renderer, if any has been registered.
|
||||
*/
|
||||
public get userInfoRenderer(): UserInfoRenderFunction | undefined {
|
||||
return this._userInfoRenderer;
|
||||
}
|
||||
|
||||
public registerUserInfo(renderer: UserInfoRenderFunction): void {
|
||||
this._userInfoRenderer = renderer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import { AlphabeticSorter } from "./skip-list/sorters/AlphabeticSorter";
|
||||
import { readReceiptChangeIsFor } from "../../utils/read-receipts";
|
||||
import { EffectiveMembership, getEffectiveMembership, getEffectiveMembershipTag } from "../../utils/membership";
|
||||
import SpaceStore from "../spaces/SpaceStore";
|
||||
import { type SpaceKey, UPDATE_HOME_BEHAVIOUR, UPDATE_SELECTED_SPACE } from "../spaces";
|
||||
import { UPDATE_HOME_BEHAVIOUR, UPDATE_SELECTED_SPACE } from "../spaces";
|
||||
import { FavouriteFilter } from "./skip-list/filters/FavouriteFilter";
|
||||
import { UnreadFilter } from "./skip-list/filters/UnreadFilter";
|
||||
import { PeopleFilter } from "./skip-list/filters/PeopleFilter";
|
||||
@@ -56,16 +56,6 @@ export enum RoomListStoreV3Event {
|
||||
ListsLoaded = "lists_loaded",
|
||||
}
|
||||
|
||||
// The result object for returning rooms from the store
|
||||
export type RoomsResult = {
|
||||
// The ID of the active space queried
|
||||
spaceId: SpaceKey;
|
||||
// The filter queried
|
||||
filterKeys?: FilterKey[];
|
||||
// The resulting list of rooms
|
||||
rooms: Room[];
|
||||
};
|
||||
|
||||
export const LISTS_UPDATE_EVENT = RoomListStoreV3Event.ListsUpdate;
|
||||
export const LISTS_LOADED_EVENT = RoomListStoreV3Event.ListsLoaded;
|
||||
/**
|
||||
@@ -117,15 +107,9 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
|
||||
|
||||
* @param filterKeys Optional array of filters that the rooms must match against.
|
||||
*/
|
||||
public getSortedRoomsInActiveSpace(filterKeys?: FilterKey[]): RoomsResult {
|
||||
const spaceId = SpaceStore.instance.activeSpace;
|
||||
if (this.roomSkipList?.initialized)
|
||||
return {
|
||||
spaceId: spaceId,
|
||||
filterKeys,
|
||||
rooms: Array.from(this.roomSkipList.getRoomsInActiveSpace(filterKeys)),
|
||||
};
|
||||
else return { spaceId: spaceId, filterKeys, rooms: [] };
|
||||
public getSortedRoomsInActiveSpace(filterKeys?: FilterKey[]): Room[] {
|
||||
if (this.roomSkipList?.initialized) return Array.from(this.roomSkipList.getRoomsInActiveSpace(filterKeys));
|
||||
else return [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -144,7 +144,6 @@ export class StopGapWidgetDriver extends WidgetDriver {
|
||||
const clientDeviceId = MatrixClientPeg.safeGet().getDeviceId();
|
||||
if (clientDeviceId !== null) {
|
||||
// For the session membership type compliant with MSC4143
|
||||
// Note MSC4143 still uses the org.matrix.msc3401 unstable prefix
|
||||
this.allowedCapabilities.add(
|
||||
WidgetEventCapability.forStateEvent(
|
||||
EventDirection.Send,
|
||||
@@ -152,13 +151,6 @@ export class StopGapWidgetDriver extends WidgetDriver {
|
||||
`_${clientUserId}_${clientDeviceId}`,
|
||||
).raw,
|
||||
);
|
||||
this.allowedCapabilities.add(
|
||||
WidgetEventCapability.forStateEvent(
|
||||
EventDirection.Send,
|
||||
"org.matrix.msc3401.call.member",
|
||||
`_${clientUserId}_${clientDeviceId}_m.call`,
|
||||
).raw,
|
||||
);
|
||||
// Version with no leading underscore, for room versions whose auth rules allow it
|
||||
this.allowedCapabilities.add(
|
||||
WidgetEventCapability.forStateEvent(
|
||||
@@ -167,13 +159,6 @@ export class StopGapWidgetDriver extends WidgetDriver {
|
||||
`${clientUserId}_${clientDeviceId}`,
|
||||
).raw,
|
||||
);
|
||||
this.allowedCapabilities.add(
|
||||
WidgetEventCapability.forStateEvent(
|
||||
EventDirection.Send,
|
||||
"org.matrix.msc3401.call.member",
|
||||
`${clientUserId}_${clientDeviceId}_m.call`,
|
||||
).raw,
|
||||
);
|
||||
}
|
||||
this.allowedCapabilities.add(
|
||||
WidgetEventCapability.forStateEvent(EventDirection.Receive, "org.matrix.msc3401.call.member").raw,
|
||||
|
||||
@@ -77,9 +77,6 @@ export class AudioPlayerViewModel
|
||||
|
||||
public constructor(props: Props) {
|
||||
super(props, AudioPlayerViewModel.computeSnapshot(props.playback, props.mediaName));
|
||||
this.disposables.trackListener(props.playback, UPDATE_EVENT, this.setSnapshot);
|
||||
// There is no unsubscribe method in SimpleObservable
|
||||
this.props.playback.clockInfo.liveData.onUpdate(this.setSnapshot);
|
||||
|
||||
// Don't wait for the promise to complete - it will emit a progress update when it
|
||||
// is done, and it's not meant to take long anyhow.
|
||||
@@ -100,6 +97,15 @@ export class AudioPlayerViewModel
|
||||
}
|
||||
}
|
||||
|
||||
protected addDownstreamSubscription(): void {
|
||||
this.props.playback.on(UPDATE_EVENT, this.setSnapshot);
|
||||
// There is no unsubscribe method in SimpleObservable
|
||||
this.props.playback.clockInfo.liveData.onUpdate(this.setSnapshot);
|
||||
}
|
||||
protected removeDownstreamSubscription(): void {
|
||||
this.props.playback.off(UPDATE_EVENT, this.setSnapshot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the snapshot and emits an update to subscribers.
|
||||
*/
|
||||
|
||||
@@ -6,7 +6,6 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type ViewModel } from "../../shared-components/ViewModel";
|
||||
import { Disposables } from "./Disposables";
|
||||
import { Snapshot } from "./Snapshot";
|
||||
import { ViewModelSubscriptions } from "./ViewModelSubscriptions";
|
||||
|
||||
@@ -14,11 +13,13 @@ export abstract class BaseViewModel<T, P> implements ViewModel<T> {
|
||||
protected subs: ViewModelSubscriptions;
|
||||
protected snapshot: Snapshot<T>;
|
||||
protected props: P;
|
||||
protected disposables = new Disposables();
|
||||
|
||||
protected constructor(props: P, initialSnapshot: T) {
|
||||
this.props = props;
|
||||
this.subs = new ViewModelSubscriptions();
|
||||
this.subs = new ViewModelSubscriptions(
|
||||
this.addDownstreamSubscriptionWrapper,
|
||||
this.removeDownstreamSubscriptionWrapper,
|
||||
);
|
||||
this.snapshot = new Snapshot(initialSnapshot, () => {
|
||||
this.subs.emit();
|
||||
});
|
||||
@@ -28,24 +29,37 @@ export abstract class BaseViewModel<T, P> implements ViewModel<T> {
|
||||
return this.subs.add(listener);
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper around the abstract subscribe callback as we can't assume that the subclassed method
|
||||
* has a bound `this` context.
|
||||
*/
|
||||
private addDownstreamSubscriptionWrapper = (): void => {
|
||||
this.addDownstreamSubscription();
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper around the abstract unsubscribe callback as we can't call pass an abstract method directly
|
||||
* in the constructor.
|
||||
*/
|
||||
private removeDownstreamSubscriptionWrapper = (): void => {
|
||||
this.removeDownstreamSubscription();
|
||||
};
|
||||
|
||||
/**
|
||||
* Called when the first listener subscribes: the subclass should set up any necessary subscriptions
|
||||
* to call this.subs.emit() when the snapshot changes.
|
||||
*/
|
||||
protected abstract addDownstreamSubscription(): void;
|
||||
|
||||
/**
|
||||
* Called when the last listener unsubscribes: the subclass should clean up any subscriptions.
|
||||
*/
|
||||
protected abstract removeDownstreamSubscription(): void;
|
||||
|
||||
/**
|
||||
* Returns the current snapshot of the view model.
|
||||
*/
|
||||
public getSnapshot = (): T => {
|
||||
return this.snapshot.current;
|
||||
};
|
||||
|
||||
/**
|
||||
* Relinquish any resources held by this view-model.
|
||||
*/
|
||||
public dispose(): void {
|
||||
this.disposables.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this view-model has been disposed.
|
||||
*/
|
||||
public get isDisposed(): boolean {
|
||||
return this.disposables.isDisposed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
/*
|
||||
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 { EventEmitter } from "events";
|
||||
|
||||
/**
|
||||
* Something that needs to be eventually disposed. This can be:
|
||||
* - A function that does the disposing
|
||||
* - An object containing a dispose method which does the disposing
|
||||
*/
|
||||
export type DisposableItem = { dispose: () => void } | (() => void);
|
||||
|
||||
/**
|
||||
* This class provides a way for the view-model to track any resource
|
||||
* that it needs to eventually relinquish.
|
||||
*/
|
||||
export class Disposables {
|
||||
private readonly disposables: DisposableItem[] = [];
|
||||
private _isDisposed: boolean = false;
|
||||
|
||||
/**
|
||||
* Relinquish all tracked disposable values
|
||||
*/
|
||||
public dispose(): void {
|
||||
if (this.isDisposed) return;
|
||||
this._isDisposed = true;
|
||||
for (const disposable of this.disposables) {
|
||||
if (typeof disposable === "function") {
|
||||
disposable();
|
||||
} else {
|
||||
disposable.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Track a value that needs to be eventually relinquished
|
||||
*/
|
||||
public track<T extends DisposableItem>(disposable: T): T {
|
||||
this.throwIfDisposed();
|
||||
this.disposables.push(disposable);
|
||||
return disposable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an event listener that will be removed on dispose
|
||||
*/
|
||||
public trackListener(emitter: EventEmitter, event: string, callback: (...args: unknown[]) => void): void {
|
||||
this.throwIfDisposed();
|
||||
emitter.on(event, callback);
|
||||
this.track(() => {
|
||||
emitter.off(event, callback);
|
||||
});
|
||||
}
|
||||
|
||||
private throwIfDisposed(): void {
|
||||
if (this.isDisposed) throw new Error("Disposable is already disposed");
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this disposable has been disposed
|
||||
*/
|
||||
public get isDisposed(): boolean {
|
||||
return this._isDisposed;
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,20 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Utility class for view models to manage subscriptions to their updates
|
||||
* Utility class for view models to manage suscriptions to their updates
|
||||
*/
|
||||
export class ViewModelSubscriptions {
|
||||
private listeners = new Set<() => void>();
|
||||
|
||||
/**
|
||||
* @param subscribeCallback Called when the first listener subscribes.
|
||||
* @param unsubscribeCallback Called when the last listener unsubscribes.
|
||||
*/
|
||||
public constructor(
|
||||
private subscribeCallback: () => void,
|
||||
private unsubscribeCallback: () => void,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Subscribe to changes in the view model.
|
||||
* @param listener Will be called whenever the snapshot changes.
|
||||
@@ -18,8 +27,15 @@ export class ViewModelSubscriptions {
|
||||
*/
|
||||
public add = (listener: () => void): (() => void) => {
|
||||
this.listeners.add(listener);
|
||||
if (this.listeners.size === 1) {
|
||||
this.subscribeCallback();
|
||||
}
|
||||
|
||||
return () => {
|
||||
this.listeners.delete(listener);
|
||||
if (this.listeners.size === 0) {
|
||||
this.unsubscribeCallback();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -17,11 +17,18 @@ export class TextualEventViewModel extends BaseViewModel<TextualEventViewSnapsho
|
||||
public constructor(props: EventTileTypeProps) {
|
||||
super(props, { content: "" });
|
||||
this.setTextFromEvent();
|
||||
this.disposables.trackListener(this.props.mxEvent, MatrixEventEvent.SentinelUpdated, this.setTextFromEvent);
|
||||
}
|
||||
|
||||
private setTextFromEvent = (): void => {
|
||||
const content = textForEvent(this.props.mxEvent, MatrixClientPeg.safeGet(), true, this.props.showHiddenEvents);
|
||||
this.snapshot.set({ content });
|
||||
};
|
||||
|
||||
protected addDownstreamSubscription = (): void => {
|
||||
this.props.mxEvent.on(MatrixEventEvent.SentinelUpdated, this.setTextFromEvent);
|
||||
};
|
||||
|
||||
protected removeDownstreamSubscription = (): void => {
|
||||
this.props.mxEvent.off(MatrixEventEvent.SentinelUpdated, this.setTextFromEvent);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2189,11 +2189,9 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
|
||||
style="position: relative; user-select: auto; width: 420px; height: 100%; max-width: 50%; min-width: 320px; box-sizing: border-box; flex-shrink: 0;"
|
||||
>
|
||||
<aside
|
||||
aria-label="Right panel"
|
||||
class="mx_RightPanel"
|
||||
data-testid="right-panel"
|
||||
id="mx_RightPanel"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_BaseCard mx_ThreadPanel mx_TimelineCard"
|
||||
|
||||
@@ -209,7 +209,7 @@ describe("useRoomListHeaderViewModel", () => {
|
||||
const rooms = range(10).map((i) => mkStubRoom(`foo${i}:matrix.org`, `Foo ${i}`, undefined));
|
||||
const fn = jest
|
||||
.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace")
|
||||
.mockImplementation(() => ({ spaceId: "home", rooms: [...rooms] }));
|
||||
.mockImplementation(() => [...rooms]);
|
||||
return { rooms, fn };
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ describe("RoomListViewModel", () => {
|
||||
const rooms = range(10).map((i) => mkStubRoom(`foo${i}:matrix.org`, `Foo ${i}`, undefined));
|
||||
const fn = jest
|
||||
.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace")
|
||||
.mockImplementation(() => ({ spaceId: "home", rooms: [...rooms] }));
|
||||
.mockImplementation(() => [...rooms]);
|
||||
return { rooms, fn };
|
||||
}
|
||||
|
||||
@@ -42,9 +42,9 @@ describe("RoomListViewModel", () => {
|
||||
const { rooms } = mockAndCreateRooms();
|
||||
const { result: vm } = renderHook(() => useRoomListViewModel());
|
||||
|
||||
expect(vm.current.roomsResult.rooms).toHaveLength(10);
|
||||
expect(vm.current.rooms).toHaveLength(10);
|
||||
for (const room of rooms) {
|
||||
expect(vm.current.roomsResult.rooms).toContain(room);
|
||||
expect(vm.current.rooms).toContain(room);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -57,7 +57,7 @@ describe("RoomListViewModel", () => {
|
||||
await act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(vm.current.roomsResult.rooms).toContain(newRoom);
|
||||
expect(vm.current.rooms).toContain(newRoom);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -176,7 +176,7 @@ describe("RoomListViewModel", () => {
|
||||
describe("Sticky room and active index", () => {
|
||||
function expectActiveRoom(vm: ReturnType<typeof useRoomListViewModel>, i: number, roomId: string) {
|
||||
expect(vm.activeIndex).toEqual(i);
|
||||
expect(vm.roomsResult.rooms[i].roomId).toEqual(roomId);
|
||||
expect(vm.rooms[i].roomId).toEqual(roomId);
|
||||
}
|
||||
|
||||
it("active index is calculated with the last opened room in a space", () => {
|
||||
@@ -187,9 +187,9 @@ describe("RoomListViewModel", () => {
|
||||
|
||||
const rooms = range(10).map((i) => mkStubRoom(`foo${i}:matrix.org`, `Foo ${i}`, undefined));
|
||||
// Let's say all the rooms are in space1
|
||||
const roomsInSpace1 = { spaceId: currentSpace, rooms: [...rooms] };
|
||||
const roomsInSpace1 = [...rooms];
|
||||
// Let's say all rooms with even index are in space 2
|
||||
const roomsInSpace2 = { spaceId: "!space2:matrix.org", rooms: [...rooms].filter((_, i) => i % 2 === 0) };
|
||||
const roomsInSpace2 = [...rooms].filter((_, i) => i % 2 === 0);
|
||||
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockImplementation(() =>
|
||||
currentSpace === "!space1:matrix.org" ? roomsInSpace1 : roomsInSpace2,
|
||||
);
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
Copyright 2025 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.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
|
||||
@@ -3,11 +3,9 @@
|
||||
exports[`AppTile destroys non-persisted right panel widget on room change 1`] = `
|
||||
<DocumentFragment>
|
||||
<aside
|
||||
aria-label="Right panel"
|
||||
class="mx_RightPanel"
|
||||
data-testid="right-panel"
|
||||
id="mx_RightPanel"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_BaseCard mx_WidgetCard"
|
||||
|
||||
@@ -19,7 +19,7 @@ describe("<EmptyRoomList />", () => {
|
||||
beforeEach(() => {
|
||||
vm = {
|
||||
isLoadingRooms: false,
|
||||
roomsResult: { spaceId: "home", rooms: [] },
|
||||
rooms: [],
|
||||
primaryFilters: [],
|
||||
createRoom: jest.fn(),
|
||||
createChatRoom: jest.fn(),
|
||||
|
||||
@@ -9,25 +9,28 @@ import React from "react";
|
||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { render } from "jest-matrix-react";
|
||||
import { fireEvent } from "@testing-library/dom";
|
||||
import { VirtuosoMockContext } from "react-virtuoso";
|
||||
|
||||
import { mkRoom, stubClient, withClientContextRenderOptions } from "../../../../../test-utils";
|
||||
import { type RoomListViewState } from "../../../../../../src/components/viewmodels/roomlist/RoomListViewModel";
|
||||
import { RoomList } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomList";
|
||||
import DMRoomMap from "../../../../../../src/utils/DMRoomMap";
|
||||
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
|
||||
import { Landmark, LandmarkNavigation } from "../../../../../../src/accessibility/LandmarkNavigation";
|
||||
import { mkRoom, stubClient } from "../../../../../test-utils";
|
||||
|
||||
describe("<RoomList />", () => {
|
||||
let matrixClient: MatrixClient;
|
||||
let vm: RoomListViewState;
|
||||
|
||||
beforeEach(() => {
|
||||
// Needed to render the virtualized list in rtl tests
|
||||
// https://github.com/bvaughn/react-virtualized/issues/493#issuecomment-640084107
|
||||
jest.spyOn(HTMLElement.prototype, "offsetHeight", "get").mockReturnValue(1500);
|
||||
jest.spyOn(HTMLElement.prototype, "offsetWidth", "get").mockReturnValue(1500);
|
||||
|
||||
matrixClient = stubClient();
|
||||
const rooms = Array.from({ length: 10 }, (_, i) => mkRoom(matrixClient, `room${i}`));
|
||||
vm = {
|
||||
isLoadingRooms: false,
|
||||
roomsResult: { spaceId: "home", rooms },
|
||||
rooms,
|
||||
primaryFilters: [],
|
||||
createRoom: jest.fn(),
|
||||
createChatRoom: jest.fn(),
|
||||
@@ -41,18 +44,7 @@ describe("<RoomList />", () => {
|
||||
});
|
||||
|
||||
it("should render a room list", () => {
|
||||
const { asFragment } = render(<RoomList vm={vm} />, {
|
||||
wrapper: ({ children }) => (
|
||||
<MatrixClientContext.Provider value={matrixClient}>
|
||||
<VirtuosoMockContext.Provider value={{ viewportHeight: 600, itemHeight: 56 }}>
|
||||
<>{children}</>
|
||||
</VirtuosoMockContext.Provider>
|
||||
</MatrixClientContext.Provider>
|
||||
),
|
||||
});
|
||||
// At the moment the context prop on Virtuoso gets rendered in the dom as "[object Object]".
|
||||
// This is a general issue with the react-virtuoso library.
|
||||
// TODO: Update the snapshot when the following issue is resolved: https://github.com/petyosi/react-virtuoso/issues/1281
|
||||
const { asFragment } = render(<RoomList vm={vm} />, withClientContextRenderOptions(matrixClient));
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -61,15 +53,7 @@ describe("<RoomList />", () => {
|
||||
{ shortcut: { key: "F6", ctrlKey: true }, isPreviousLandmark: false, label: "NextLandmark" },
|
||||
])("should navigate to the landmark on NextLandmark.$label action", ({ shortcut, isPreviousLandmark }) => {
|
||||
const spyFindLandmark = jest.spyOn(LandmarkNavigation, "findAndFocusNextLandmark").mockReturnValue();
|
||||
const { getByTestId } = render(<RoomList vm={vm} />, {
|
||||
wrapper: ({ children }) => (
|
||||
<MatrixClientContext.Provider value={matrixClient}>
|
||||
<VirtuosoMockContext.Provider value={{ viewportHeight: 600, itemHeight: 56 }}>
|
||||
<>{children}</>
|
||||
</VirtuosoMockContext.Provider>
|
||||
</MatrixClientContext.Provider>
|
||||
),
|
||||
});
|
||||
const { getByTestId } = render(<RoomList vm={vm} />, withClientContextRenderOptions(matrixClient));
|
||||
const roomList = getByTestId("room-list");
|
||||
fireEvent.keyDown(roomList, shortcut);
|
||||
|
||||
|
||||
@@ -28,20 +28,6 @@ describe("<RoomListItemView />", () => {
|
||||
let defaultValue: RoomListItemViewState;
|
||||
let matrixClient: MatrixClient;
|
||||
let room: Room;
|
||||
|
||||
const renderRoomListItem = (props: Partial<React.ComponentProps<typeof RoomListItemView>> = {}) => {
|
||||
const defaultProps = {
|
||||
room,
|
||||
isSelected: false,
|
||||
isFocused: false,
|
||||
onFocus: jest.fn(),
|
||||
roomIndex: 0,
|
||||
roomCount: 1,
|
||||
};
|
||||
|
||||
return render(<RoomListItemView {...defaultProps} {...props} />, withClientContextRenderOptions(matrixClient));
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
matrixClient = stubClient();
|
||||
room = mkRoom(matrixClient, "room1");
|
||||
@@ -74,10 +60,7 @@ describe("<RoomListItemView />", () => {
|
||||
|
||||
test("should render a room item", () => {
|
||||
const onClick = jest.fn();
|
||||
const { asFragment } = renderRoomListItem({
|
||||
onClick,
|
||||
roomCount: 0,
|
||||
});
|
||||
const { asFragment } = render(<RoomListItemView room={room} onClick={onClick} isSelected={false} />);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -85,17 +68,15 @@ describe("<RoomListItemView />", () => {
|
||||
defaultValue.messagePreview = "The message looks like this";
|
||||
|
||||
const onClick = jest.fn();
|
||||
const { asFragment } = renderRoomListItem({
|
||||
onClick,
|
||||
});
|
||||
const { asFragment } = render(<RoomListItemView room={room} onClick={onClick} isSelected={false} />);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("should call openRoom when clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderRoomListItem();
|
||||
render(<RoomListItemView room={room} isSelected={false} />);
|
||||
|
||||
await user.click(screen.getByRole("option", { name: `Open room ${room.name}` }));
|
||||
await user.click(screen.getByRole("button", { name: `Open room ${room.name}` }));
|
||||
expect(defaultValue.openRoom).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -103,9 +84,8 @@ describe("<RoomListItemView />", () => {
|
||||
mocked(useRoomListItemViewModel).mockReturnValue({ ...defaultValue, showHoverMenu: true });
|
||||
|
||||
const user = userEvent.setup();
|
||||
renderRoomListItem();
|
||||
|
||||
const listItem = screen.getByRole("option", { name: `Open room ${room.name}` });
|
||||
render(<RoomListItemView room={room} isSelected={false} />, withClientContextRenderOptions(matrixClient));
|
||||
const listItem = screen.getByRole("button", { name: `Open room ${room.name}` });
|
||||
expect(screen.queryByRole("button", { name: "More Options" })).toBeNull();
|
||||
|
||||
await user.hover(listItem);
|
||||
@@ -113,33 +93,19 @@ describe("<RoomListItemView />", () => {
|
||||
});
|
||||
|
||||
test("should hover decoration if focused", async () => {
|
||||
const { rerender } = renderRoomListItem({
|
||||
isFocused: true,
|
||||
});
|
||||
const user = userEvent.setup();
|
||||
render(<RoomListItemView room={room} isSelected={false} />, withClientContextRenderOptions(matrixClient));
|
||||
const listItem = screen.getByRole("button", { name: `Open room ${room.name}` });
|
||||
await user.click(listItem);
|
||||
expect(listItem).toHaveClass("mx_RoomListItemView_hover");
|
||||
|
||||
const listItem = screen.getByRole("option", { name: `Open room ${room.name}` });
|
||||
expect(listItem).toHaveClass("flex mx_RoomListItemView mx_RoomListItemView_hover");
|
||||
|
||||
rerender(
|
||||
<RoomListItemView
|
||||
room={room}
|
||||
isSelected={false}
|
||||
isFocused={false}
|
||||
onFocus={jest.fn()}
|
||||
roomIndex={0}
|
||||
roomCount={1}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(listItem).not.toHaveClass("flex mx_RoomListItemView mx_RoomListItemView_hover"));
|
||||
await user.tab();
|
||||
await waitFor(() => expect(listItem).not.toHaveClass("mx_RoomListItemView_hover"));
|
||||
});
|
||||
|
||||
test("should be selected if isSelected=true", async () => {
|
||||
const { asFragment } = renderRoomListItem({
|
||||
isSelected: true,
|
||||
});
|
||||
|
||||
expect(screen.queryByRole("option", { name: `Open room ${room.name}` })).toHaveAttribute(
|
||||
const { asFragment } = render(<RoomListItemView room={room} isSelected={true} />);
|
||||
expect(screen.queryByRole("button", { name: `Open room ${room.name}` })).toHaveAttribute(
|
||||
"aria-selected",
|
||||
"true",
|
||||
);
|
||||
@@ -152,8 +118,7 @@ describe("<RoomListItemView />", () => {
|
||||
showNotificationDecoration: true,
|
||||
});
|
||||
|
||||
const { asFragment } = renderRoomListItem();
|
||||
|
||||
const { asFragment } = render(<RoomListItemView room={room} isSelected={false} />);
|
||||
expect(screen.getByTestId("notification-decoration")).toBeInTheDocument();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
@@ -166,9 +131,8 @@ describe("<RoomListItemView />", () => {
|
||||
showNotificationDecoration: true,
|
||||
});
|
||||
|
||||
renderRoomListItem();
|
||||
|
||||
const listItem = screen.getByRole("option", { name: `Open room ${room.name}` });
|
||||
render(<RoomListItemView room={room} isSelected={false} />);
|
||||
const listItem = screen.getByRole("button", { name: `Open room ${room.name}` });
|
||||
await user.hover(listItem);
|
||||
|
||||
expect(screen.queryByRole("notification-decoration")).toBeNull();
|
||||
@@ -182,9 +146,8 @@ describe("<RoomListItemView />", () => {
|
||||
showContextMenu: true,
|
||||
});
|
||||
|
||||
renderRoomListItem();
|
||||
|
||||
const button = screen.getByRole("option", { name: `Open room ${room.name}` });
|
||||
render(<RoomListItemView room={room} isSelected={false} />, withClientContextRenderOptions(matrixClient));
|
||||
const button = screen.getByRole("button", { name: `Open room ${room.name}` });
|
||||
await user.pointer([{ target: button }, { keys: "[MouseRight]", target: button }]);
|
||||
await waitFor(() => expect(screen.getByRole("menu")).toBeInTheDocument());
|
||||
// Menu should close
|
||||
|
||||
@@ -23,7 +23,7 @@ jest.mock("../../../../../../src/components/viewmodels/roomlist/RoomListViewMode
|
||||
describe("<RoomListView />", () => {
|
||||
const defaultValue: RoomListViewState = {
|
||||
isLoadingRooms: false,
|
||||
roomsResult: { spaceId: "home", rooms: [] },
|
||||
rooms: [],
|
||||
primaryFilters: [],
|
||||
createRoom: jest.fn(),
|
||||
createChatRoom: jest.fn(),
|
||||
@@ -56,10 +56,10 @@ describe("<RoomListView />", () => {
|
||||
it("should render a room list", () => {
|
||||
mocked(useRoomListViewModel).mockReturnValue({
|
||||
...defaultValue,
|
||||
roomsResult: { spaceId: "home", rooms: [mkRoom(matrixClient, "testing room")] },
|
||||
rooms: [mkRoom(matrixClient, "testing room")],
|
||||
});
|
||||
|
||||
render(<RoomListView />);
|
||||
expect(screen.getByRole("listbox", { name: "Room list" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("grid", { name: "Room list" })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,556 +3,531 @@
|
||||
exports[`<RoomList /> should render a room list 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
aria-label="Room list"
|
||||
context="[object Object]"
|
||||
class="mx_RoomList"
|
||||
data-testid="room-list"
|
||||
data-virtuoso-scroller="true"
|
||||
role="listbox"
|
||||
style="height: 100%; outline: none; overflow-y: auto; position: relative;"
|
||||
>
|
||||
<div
|
||||
data-viewport-type="element"
|
||||
style="height: 100%; position: absolute; top: 0px; width: 100%;"
|
||||
style="overflow: visible; height: 0px; width: 0px;"
|
||||
>
|
||||
<div
|
||||
data-testid="virtuoso-item-list"
|
||||
style="box-sizing: border-box; margin-top: 0px; padding-bottom: 0px; padding-top: 0px;"
|
||||
aria-label="Room list"
|
||||
aria-readonly="true"
|
||||
class="ReactVirtualized__Grid ReactVirtualized__List mx_RoomList_List"
|
||||
role="grid"
|
||||
style="box-sizing: border-box; direction: ltr; height: 1500px; position: relative; width: 1500px; will-change: transform; overflow-x: hidden; overflow-y: hidden;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
data-index="0"
|
||||
data-item-index="0"
|
||||
data-known-size="48"
|
||||
class="ReactVirtualized__Grid__innerScrollContainer"
|
||||
role="row"
|
||||
style="width: auto; height: 480px; max-width: 1500px; max-height: 480px; overflow: hidden; position: relative;"
|
||||
>
|
||||
<button
|
||||
aria-haspopup="menu"
|
||||
aria-label="Open room room0"
|
||||
aria-posinset="1"
|
||||
aria-selected="false"
|
||||
aria-setsize="10"
|
||||
class="flex mx_RoomListItemView"
|
||||
class="mx_RoomListItemView"
|
||||
data-state="closed"
|
||||
role="option"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
role="gridcell"
|
||||
style="height: 48px; left: 0px; position: absolute; top: 0px; width: 100%;"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="2"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
class="flex mx_RoomListItemView_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="2"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="mx_RoomListItemView_text"
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room0"
|
||||
class="mx_RoomListItemView_text"
|
||||
>
|
||||
room0
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room0"
|
||||
>
|
||||
room0
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
data-index="1"
|
||||
data-item-index="1"
|
||||
data-known-size="48"
|
||||
>
|
||||
<button
|
||||
aria-haspopup="menu"
|
||||
aria-label="Open room room1"
|
||||
aria-posinset="2"
|
||||
aria-selected="false"
|
||||
aria-setsize="10"
|
||||
class="flex mx_RoomListItemView"
|
||||
class="mx_RoomListItemView"
|
||||
data-state="closed"
|
||||
role="option"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
role="gridcell"
|
||||
style="height: 48px; left: 0px; position: absolute; top: 48px; width: 100%;"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
class="flex mx_RoomListItemView_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="mx_RoomListItemView_text"
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room1"
|
||||
class="mx_RoomListItemView_text"
|
||||
>
|
||||
room1
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room1"
|
||||
>
|
||||
room1
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
data-index="2"
|
||||
data-item-index="2"
|
||||
data-known-size="48"
|
||||
>
|
||||
<button
|
||||
aria-haspopup="menu"
|
||||
aria-label="Open room room2"
|
||||
aria-posinset="3"
|
||||
aria-selected="false"
|
||||
aria-setsize="10"
|
||||
class="flex mx_RoomListItemView"
|
||||
class="mx_RoomListItemView"
|
||||
data-state="closed"
|
||||
role="option"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
role="gridcell"
|
||||
style="height: 48px; left: 0px; position: absolute; top: 96px; width: 100%;"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="4"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
class="flex mx_RoomListItemView_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="4"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="mx_RoomListItemView_text"
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room2"
|
||||
class="mx_RoomListItemView_text"
|
||||
>
|
||||
room2
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room2"
|
||||
>
|
||||
room2
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
data-index="3"
|
||||
data-item-index="3"
|
||||
data-known-size="48"
|
||||
>
|
||||
<button
|
||||
aria-haspopup="menu"
|
||||
aria-label="Open room room3"
|
||||
aria-posinset="4"
|
||||
aria-selected="false"
|
||||
aria-setsize="10"
|
||||
class="flex mx_RoomListItemView"
|
||||
class="mx_RoomListItemView"
|
||||
data-state="closed"
|
||||
role="option"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
role="gridcell"
|
||||
style="height: 48px; left: 0px; position: absolute; top: 144px; width: 100%;"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="5"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
class="flex mx_RoomListItemView_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="5"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="mx_RoomListItemView_text"
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room3"
|
||||
class="mx_RoomListItemView_text"
|
||||
>
|
||||
room3
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room3"
|
||||
>
|
||||
room3
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
data-index="4"
|
||||
data-item-index="4"
|
||||
data-known-size="48"
|
||||
>
|
||||
<button
|
||||
aria-haspopup="menu"
|
||||
aria-label="Open room room4"
|
||||
aria-posinset="5"
|
||||
aria-selected="false"
|
||||
aria-setsize="10"
|
||||
class="flex mx_RoomListItemView"
|
||||
class="mx_RoomListItemView"
|
||||
data-state="closed"
|
||||
role="option"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
role="gridcell"
|
||||
style="height: 48px; left: 0px; position: absolute; top: 192px; width: 100%;"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="6"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
class="flex mx_RoomListItemView_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="6"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="mx_RoomListItemView_text"
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room4"
|
||||
class="mx_RoomListItemView_text"
|
||||
>
|
||||
room4
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room4"
|
||||
>
|
||||
room4
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
data-index="5"
|
||||
data-item-index="5"
|
||||
data-known-size="48"
|
||||
>
|
||||
<button
|
||||
aria-haspopup="menu"
|
||||
aria-label="Open room room5"
|
||||
aria-posinset="6"
|
||||
aria-selected="false"
|
||||
aria-setsize="10"
|
||||
class="flex mx_RoomListItemView"
|
||||
class="mx_RoomListItemView"
|
||||
data-state="closed"
|
||||
role="option"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
role="gridcell"
|
||||
style="height: 48px; left: 0px; position: absolute; top: 240px; width: 100%;"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="1"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
class="flex mx_RoomListItemView_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="1"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="mx_RoomListItemView_text"
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room5"
|
||||
class="mx_RoomListItemView_text"
|
||||
>
|
||||
room5
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room5"
|
||||
>
|
||||
room5
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
data-index="6"
|
||||
data-item-index="6"
|
||||
data-known-size="48"
|
||||
>
|
||||
<button
|
||||
aria-haspopup="menu"
|
||||
aria-label="Open room room6"
|
||||
aria-posinset="7"
|
||||
aria-selected="false"
|
||||
aria-setsize="10"
|
||||
class="flex mx_RoomListItemView"
|
||||
class="mx_RoomListItemView"
|
||||
data-state="closed"
|
||||
role="option"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
role="gridcell"
|
||||
style="height: 48px; left: 0px; position: absolute; top: 288px; width: 100%;"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="2"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
class="flex mx_RoomListItemView_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="2"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="mx_RoomListItemView_text"
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room6"
|
||||
class="mx_RoomListItemView_text"
|
||||
>
|
||||
room6
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room6"
|
||||
>
|
||||
room6
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
data-index="7"
|
||||
data-item-index="7"
|
||||
data-known-size="48"
|
||||
>
|
||||
<button
|
||||
aria-haspopup="menu"
|
||||
aria-label="Open room room7"
|
||||
aria-posinset="8"
|
||||
aria-selected="false"
|
||||
aria-setsize="10"
|
||||
class="flex mx_RoomListItemView"
|
||||
class="mx_RoomListItemView"
|
||||
data-state="closed"
|
||||
role="option"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
role="gridcell"
|
||||
style="height: 48px; left: 0px; position: absolute; top: 336px; width: 100%;"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
class="flex mx_RoomListItemView_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="mx_RoomListItemView_text"
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room7"
|
||||
class="mx_RoomListItemView_text"
|
||||
>
|
||||
room7
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room7"
|
||||
>
|
||||
room7
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
data-index="8"
|
||||
data-item-index="8"
|
||||
data-known-size="48"
|
||||
>
|
||||
<button
|
||||
aria-haspopup="menu"
|
||||
aria-label="Open room room8"
|
||||
aria-posinset="9"
|
||||
aria-selected="false"
|
||||
aria-setsize="10"
|
||||
class="flex mx_RoomListItemView"
|
||||
class="mx_RoomListItemView"
|
||||
data-state="closed"
|
||||
role="option"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
role="gridcell"
|
||||
style="height: 48px; left: 0px; position: absolute; top: 384px; width: 100%;"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="4"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
class="flex mx_RoomListItemView_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="4"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="mx_RoomListItemView_text"
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room8"
|
||||
class="mx_RoomListItemView_text"
|
||||
>
|
||||
room8
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room8"
|
||||
>
|
||||
room8
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
data-index="9"
|
||||
data-item-index="9"
|
||||
data-known-size="48"
|
||||
>
|
||||
<button
|
||||
aria-haspopup="menu"
|
||||
aria-label="Open room room9"
|
||||
aria-posinset="10"
|
||||
aria-selected="false"
|
||||
aria-setsize="10"
|
||||
class="flex mx_RoomListItemView"
|
||||
class="mx_RoomListItemView"
|
||||
data-state="closed"
|
||||
role="option"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
role="gridcell"
|
||||
style="height: 48px; left: 0px; position: absolute; top: 432px; width: 100%;"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="5"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
class="flex mx_RoomListItemView_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="5"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="mx_RoomListItemView_text"
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room9"
|
||||
class="mx_RoomListItemView_text"
|
||||
>
|
||||
room9
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room9"
|
||||
>
|
||||
room9
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -560,6 +535,20 @@ exports[`<RoomList /> should render a room list 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="resize-triggers"
|
||||
>
|
||||
<div
|
||||
class="expand-trigger"
|
||||
>
|
||||
<div
|
||||
style="width: 1501px; height: 1501px;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="contract-trigger"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
@@ -38,43 +38,41 @@ exports[`<RoomListItemMenuView /> should render the more options menu 1`] = `
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<div>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
aria-label="Notification options"
|
||||
aria-labelledby="«r9»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
data-state="closed"
|
||||
id="radix-«r7»"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 24px;"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
aria-label="Notification options"
|
||||
aria-labelledby="«r9»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
data-state="closed"
|
||||
id="radix-«r7»"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 24px;"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_zr2a0_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_zr2a0_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m4.917 2.083 17 17a1 1 0 0 1-1.414 1.414L19.006 19H4.414c-.89 0-1.337-1.077-.707-1.707L5 16v-6s0-2.034 1.096-3.91L3.504 3.498a1 1 0 0 1 1.414-1.414M19 13.35 9.136 3.484C9.93 3.181 10.874 3 12 3c7 0 7 7 7 7z"
|
||||
/>
|
||||
<path
|
||||
d="M10 20h4a2 2 0 0 1-4 0"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<path
|
||||
d="m4.917 2.083 17 17a1 1 0 0 1-1.414 1.414L19.006 19H4.414c-.89 0-1.337-1.077-.707-1.707L5 16v-6s0-2.034 1.096-3.91L3.504 3.498a1 1 0 0 1 1.414-1.414M19 13.35 9.136 3.484C9.93 3.181 10.874 3 12 3c7 0 7 7 7 7z"
|
||||
/>
|
||||
<path
|
||||
d="M10 20h4a2 2 0 0 1-4 0"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
@@ -117,43 +115,41 @@ exports[`<RoomListItemMenuView /> should render the notification options menu 1`
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<div>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
aria-label="Notification options"
|
||||
aria-labelledby="«rp»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
data-state="closed"
|
||||
id="radix-«rn»"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 24px;"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
aria-label="Notification options"
|
||||
aria-labelledby="«rp»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
data-state="closed"
|
||||
id="radix-«rn»"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 24px;"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_zr2a0_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_zr2a0_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m4.917 2.083 17 17a1 1 0 0 1-1.414 1.414L19.006 19H4.414c-.89 0-1.337-1.077-.707-1.707L5 16v-6s0-2.034 1.096-3.91L3.504 3.498a1 1 0 0 1 1.414-1.414M19 13.35 9.136 3.484C9.93 3.181 10.874 3 12 3c7 0 7 7 7 7z"
|
||||
/>
|
||||
<path
|
||||
d="M10 20h4a2 2 0 0 1-4 0"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<path
|
||||
d="m4.917 2.083 17 17a1 1 0 0 1-1.414 1.414L19.006 19H4.414c-.89 0-1.337-1.077-.707-1.707L5 16v-6s0-2.034 1.096-3.91L3.504 3.498a1 1 0 0 1 1.414-1.414M19 13.35 9.136 3.484C9.93 3.181 10.874 3 12 3c7 0 7 7 7 7z"
|
||||
/>
|
||||
<path
|
||||
d="M10 20h4a2 2 0 0 1-4 0"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
@@ -4,46 +4,47 @@ exports[`<RoomListItemView /> should be selected if isSelected=true 1`] = `
|
||||
<DocumentFragment>
|
||||
<button
|
||||
aria-label="Open room room1"
|
||||
aria-posinset="1"
|
||||
aria-selected="true"
|
||||
aria-setsize="1"
|
||||
class="flex mx_RoomListItemView mx_RoomListItemView_selected"
|
||||
role="option"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
class="mx_RoomListItemView mx_RoomListItemView_selected"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
class="flex mx_RoomListItemView_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="mx_RoomListItemView_text"
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room1"
|
||||
class="mx_RoomListItemView_text"
|
||||
>
|
||||
room1
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room1"
|
||||
>
|
||||
room1
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -55,59 +56,60 @@ exports[`<RoomListItemView /> should display notification decoration 1`] = `
|
||||
<DocumentFragment>
|
||||
<button
|
||||
aria-label="Open room room1"
|
||||
aria-posinset="1"
|
||||
aria-selected="false"
|
||||
aria-setsize="1"
|
||||
class="flex mx_RoomListItemView"
|
||||
role="option"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
class="mx_RoomListItemView"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
class="flex mx_RoomListItemView_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="mx_RoomListItemView_text"
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room1"
|
||||
class="mx_RoomListItemView_text"
|
||||
>
|
||||
room1
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room1"
|
||||
>
|
||||
room1
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="flex"
|
||||
data-testid="notification-decoration"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-1x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
class="_unread-counter_9mg0k_8"
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="flex"
|
||||
data-testid="notification-decoration"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-1x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
1
|
||||
</span>
|
||||
<span
|
||||
class="_unread-counter_9mg0k_8"
|
||||
>
|
||||
1
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
@@ -118,46 +120,47 @@ exports[`<RoomListItemView /> should render a room item 1`] = `
|
||||
<DocumentFragment>
|
||||
<button
|
||||
aria-label="Open room room1"
|
||||
aria-posinset="1"
|
||||
aria-selected="false"
|
||||
aria-setsize="0"
|
||||
class="flex mx_RoomListItemView"
|
||||
role="option"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
class="mx_RoomListItemView"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
class="flex mx_RoomListItemView_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="mx_RoomListItemView_text"
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room1"
|
||||
class="mx_RoomListItemView_text"
|
||||
>
|
||||
room1
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room1"
|
||||
>
|
||||
room1
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -169,52 +172,53 @@ exports[`<RoomListItemView /> should render a room item with a message preview 1
|
||||
<DocumentFragment>
|
||||
<button
|
||||
aria-label="Open room room1"
|
||||
aria-posinset="1"
|
||||
aria-selected="false"
|
||||
aria-setsize="1"
|
||||
class="flex mx_RoomListItemView"
|
||||
role="option"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
class="mx_RoomListItemView"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
class="flex mx_RoomListItemView_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_1qbcf_41"
|
||||
data-type="round"
|
||||
height="32px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="http://this.is.a.url/avatar.url/room.png"
|
||||
width="32px"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="mx_RoomListItemView_text"
|
||||
class="flex mx_RoomListItemView_content"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room1"
|
||||
class="mx_RoomListItemView_text"
|
||||
>
|
||||
room1
|
||||
</div>
|
||||
<div
|
||||
class="mx_RoomListItemView_messagePreview"
|
||||
title="The message looks like this"
|
||||
>
|
||||
The message looks like this
|
||||
<div
|
||||
class="mx_RoomListItemView_roomName"
|
||||
title="room1"
|
||||
>
|
||||
room1
|
||||
</div>
|
||||
<div
|
||||
class="mx_RoomListItemView_messagePreview"
|
||||
title="The message looks like this"
|
||||
>
|
||||
The message looks like this
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { act, fireEvent, screen, waitFor } from "jest-matrix-react";
|
||||
import { RoomMember, User, RoomEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import { mocked } from "jest-mock";
|
||||
import { type JSX } from "react";
|
||||
|
||||
import { shouldShowComponent } from "../../../../../../src/customisations/helpers/UIComponents";
|
||||
import defaultDispatcher from "../../../../../../src/dispatcher/dispatcher";
|
||||
@@ -20,6 +21,14 @@ jest.mock("../../../../../../src/customisations/helpers/UIComponents", () => ({
|
||||
shouldShowComponent: jest.fn(),
|
||||
}));
|
||||
|
||||
type Children = (args: { height: number; width: number }) => JSX.Element;
|
||||
jest.mock("react-virtualized", () => {
|
||||
const ReactVirtualized = jest.requireActual("react-virtualized");
|
||||
return {
|
||||
...ReactVirtualized,
|
||||
AutoSizer: ({ children }: { children: Children }) => children({ height: 1000, width: 1000 }),
|
||||
};
|
||||
});
|
||||
jest.spyOn(HTMLElement.prototype, "offsetHeight", "get").mockReturnValue(1500);
|
||||
jest.spyOn(HTMLElement.prototype, "offsetWidth", "get").mockReturnValue(1500);
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
import { act } from "react";
|
||||
import { waitFor, fireEvent } from "jest-matrix-react";
|
||||
import { type Room, type RoomMember, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { type JSX } from "react";
|
||||
|
||||
import { filterConsole } from "../../../../../test-utils";
|
||||
import { type Rendered, renderMemberList } from "./common";
|
||||
@@ -18,6 +19,14 @@ jest.mock("../../../../../../src/customisations/helpers/UIComponents", () => ({
|
||||
shouldShowComponent: jest.fn(),
|
||||
}));
|
||||
|
||||
type Children = (args: { height: number; width: number }) => JSX.Element;
|
||||
jest.mock("react-virtualized", () => {
|
||||
const ReactVirtualized = jest.requireActual("react-virtualized");
|
||||
return {
|
||||
...ReactVirtualized,
|
||||
AutoSizer: ({ children }: { children: Children }) => children({ height: 1000, width: 1000 }),
|
||||
};
|
||||
});
|
||||
jest.spyOn(HTMLElement.prototype, "offsetHeight", "get").mockReturnValue(1500);
|
||||
jest.spyOn(HTMLElement.prototype, "offsetWidth", "get").mockReturnValue(1500);
|
||||
|
||||
|
||||