Compare commits

..

8 Commits

Author SHA1 Message Date
Richard van der Hoff
2db2a77d56 Another go at updating screenshots 2025-08-15 12:39:01 +01:00
Richard van der Hoff
fc3a52cd4c Revert "update playwright screenshots"
This reverts commit b0a15d97f3.
2025-08-15 12:18:53 +01:00
Richard van der Hoff
b0a15d97f3 update playwright screenshots 2025-08-15 11:04:45 +01:00
Richard van der Hoff
a646f3575f Switch to compound CSS variables instead of old pcss vars 2025-08-14 14:45:34 +01:00
Richard van der Hoff
0541b7a961 Inhibit invite progress dialog when RoomUpgradeWarning dialog is kept open
... otherwise the `RoomUpgradeWarning` dialog disappears during the invites,
and the tests that assert that it is showing the correct thing fail.
 enter the commit message for your changes. Lines starting
2025-08-14 12:36:08 +01:00
Richard van der Hoff
58f1893997 Open a "progress" dialog while invites are being sent 2025-08-13 17:58:56 +01:00
Richard van der Hoff
a562b2928a MultiInviter-test: avoid building unhandled rejected promises
If we don't handle rejected promises, jest gets confused by them. Instead,
let's create them on-demand.
2025-08-13 17:58:56 +01:00
Richard van der Hoff
f43fadd9b8 InviteDialog: show some words and a spinner while invites are being sent 2025-08-13 17:58:56 +01:00
108 changed files with 1806 additions and 2305 deletions

3
.github/CODEOWNERS vendored
View File

@@ -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

View File

@@ -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:

View File

@@ -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: |

View File

@@ -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:

View File

@@ -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: |

View File

@@ -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

View File

@@ -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

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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:

View File

@@ -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') }}

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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",

View File

@@ -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/);
});
});

View File

@@ -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");
});

View File

@@ -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");
});

View File

@@ -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."),

View File

@@ -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);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -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}`);
}
}

View File

@@ -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,

View File

@@ -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;

View File

@@ -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

View File

@@ -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;
}
}
}
}

View File

@@ -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;
}

View File

@@ -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>
);

View File

@@ -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}

View File

@@ -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,
};

View File

@@ -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,

View File

@@ -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,
};
}

View File

@@ -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 };
}

View File

@@ -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 />;
};

View File

@@ -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 {

View File

@@ -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}
/>
) : (
<></>
);
}

View File

@@ -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>

View File

@@ -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}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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];
}

View File

@@ -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" />;

View File

@@ -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>
)}

View File

@@ -1921,7 +1921,6 @@
"thread_list": {
"context_menu_label": "Thread options"
},
"title": "Right panel",
"video_room_chat": {
"title": "Chat"
}

View File

@@ -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"
}

View File

@@ -1916,7 +1916,6 @@
"thread_list": {
"context_menu_label": "Options des fils de discussion"
},
"title": "Panneau droit",
"video_room_chat": {
"title": "Conversation privée"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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": "Помилка обробки голосового повідомлення",

View File

@@ -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;
}
}

View File

@@ -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 [];
}
/**

View File

@@ -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,

View File

@@ -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.
*/

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
};
};

View File

@@ -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);
};
}

View File

@@ -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"

View File

@@ -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 };
}

View File

@@ -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,
);

View File

@@ -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";

View File

@@ -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"

View File

@@ -19,7 +19,7 @@ describe("<EmptyRoomList />", () => {
beforeEach(() => {
vm = {
isLoadingRooms: false,
roomsResult: { spaceId: "home", rooms: [] },
rooms: [],
primaryFilters: [],
createRoom: jest.fn(),
createChatRoom: jest.fn(),

View File

@@ -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);

View File

@@ -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

View File

@@ -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();
});
});

View File

@@ -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>
`;

View File

@@ -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>
`;

View File

@@ -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>

View File

@@ -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);

View File

@@ -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);

Some files were not shown because too many files have changed in this diff Show More