diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index a6b54cabd5..d0c447f8c6 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -3,6 +3,7 @@ on: workflow_dispatch: {} push: tags: [v*] + pull_request: {} schedule: # This job can take a while, and we have usage limits, so just publish develop only twice a day - cron: "0 7/12 * * *" @@ -12,10 +13,12 @@ jobs: buildx: name: Docker Buildx runs-on: ubuntu-24.04 - environment: dockerhub + environment: ${{ github.event_name != 'pull_request' && 'dockerhub' || '' }} permissions: id-token: write # needed for signing the images with GitHub OIDC Token packages: write # needed for publishing packages to GHCR + env: + TEST_TAG: vectorim/element-web:test steps: - uses: actions/checkout@v4 with: @@ -23,6 +26,7 @@ jobs: - name: Install Cosign uses: sigstore/cosign-installer@c56c2d3e59e4281cc41dea2217323ba5694b171e # v3 + if: github.event_name != 'pull_request' - name: Set up QEMU uses: docker/setup-qemu-action@4574d27a4764455b42196d70a065bc6853246a25 # v3 @@ -34,20 +38,63 @@ jobs: - name: Login to Docker Hub uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3 + if: github.event_name != 'pull_request' with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3 + if: github.event_name != 'pull_request' with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and load + id: test-build + uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6 + with: + context: . + load: true + + - name: Test the image + env: + IMAGEID: ${{ steps.test-build.outputs.imageid }} + run: | + set -x + + # Make a fake module to test the image + MODULE_PATH="modules/module_name/index.js" + mkdir -p $(dirname $MODULE_PATH) + echo 'alert("Testing");' > $MODULE_PATH + + # Spin up a container of the image + ELEMENT_WEB_PORT=8181 + CONTAINER_ID=$( + docker run \ + --rm \ + -e "ELEMENT_WEB_PORT=$ELEMENT_WEB_PORT" \ + -dp "$ELEMENT_WEB_PORT:$ELEMENT_WEB_PORT" \ + -v $(pwd)/modules:/tmp/element-web-modules \ + "$IMAGEID" \ + ) + + # Run some smoke tests + wget --retry-connrefused --tries=5 -q --wait=3 --spider "http://localhost:$ELEMENT_WEB_PORT/modules/module_name/index.js" + MODULE_0=$(curl "http://localhost:$ELEMENT_WEB_PORT/config.json" | jq -r .modules[0]) + test "$MODULE_0" = "/${MODULE_PATH}" + + # Check healthcheck + test "$(docker inspect -f {{.State.Running}} $CONTAINER_ID)" == "true" + + # Clean up + docker stop "$CONTAINER_ID" + - name: Docker meta id: meta uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5 + if: github.event_name != 'pull_request' with: images: | vectorim/element-web @@ -61,6 +108,7 @@ jobs: - name: Build and push id: build-and-push uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6 + if: github.event_name != 'pull_request' with: context: . push: true @@ -72,6 +120,7 @@ jobs: env: DIGEST: ${{ steps.build-and-push.outputs.digest }} TAGS: ${{ steps.meta.outputs.tags }} + if: github.event_name != 'pull_request' run: | images="" for tag in ${TAGS}; do @@ -81,6 +130,7 @@ jobs: - name: Update repo description uses: peter-evans/dockerhub-description@e98e4d1628a5f3be2be7c231e50981aee98723ae # v4 + if: github.event_name != 'pull_request' continue-on-error: true with: username: ${{ secrets.DOCKERHUB_USERNAME }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 22739da21f..78383e8bf5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,6 +19,7 @@ jobs: contents: write issues: write pull-requests: read + id-token: write secrets: ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 2de3ce4d9d..324c7f7ad8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,33 @@ +Changes in [1.11.93](https://github.com/element-hq/element-web/releases/tag/v1.11.93) (2025-02-25) +================================================================================================== +## ✨ Features + +* [backport] Dynamically load Element Web modules in Docker entrypoint ([#29358](https://github.com/element-hq/element-web/pull/29358)). Contributed by @t3chguy. +* ChangeRecoveryKey: error handling ([#29262](https://github.com/element-hq/element-web/pull/29262)). Contributed by @richvdh. +* Dehydration: enable dehydrated device on "Set up recovery" ([#29265](https://github.com/element-hq/element-web/pull/29265)). Contributed by @richvdh. +* Render reason for invite rejection. ([#29257](https://github.com/element-hq/element-web/pull/29257)). Contributed by @Half-Shot. +* New room list: add search section ([#29251](https://github.com/element-hq/element-web/pull/29251)). Contributed by @florianduros. +* New room list: hide favourites and people meta spaces ([#29241](https://github.com/element-hq/element-web/pull/29241)). Contributed by @florianduros. +* New Room List: Create new labs flag ([#29239](https://github.com/element-hq/element-web/pull/29239)). Contributed by @MidhunSureshR. +* Stop URl preview from covering message box ([#29215](https://github.com/element-hq/element-web/pull/29215)). Contributed by @edent. +* Rename "security key" into "recovery key" ([#29217](https://github.com/element-hq/element-web/pull/29217)). Contributed by @florianduros. +* Add new verification section to user profile ([#29200](https://github.com/element-hq/element-web/pull/29200)). Contributed by @MidhunSureshR. +* Initial support for runtime modules ([#29104](https://github.com/element-hq/element-web/pull/29104)). Contributed by @t3chguy. +* Add `Forgot recovery key?` button to encryption tab ([#29202](https://github.com/element-hq/element-web/pull/29202)). Contributed by @florianduros. +* Add KeyIcon to key storage out of sync toast ([#29201](https://github.com/element-hq/element-web/pull/29201)). Contributed by @florianduros. +* Improve rendering of empty topics in the timeline ([#29152](https://github.com/element-hq/element-web/pull/29152)). Contributed by @Half-Shot. + +## 🐛 Bug Fixes + +* Fix font scaling in member list ([#29285](https://github.com/element-hq/element-web/pull/29285)). Contributed by @florianduros. +* Grow member list search field when resizing the right panel ([#29267](https://github.com/element-hq/element-web/pull/29267)). Contributed by @langleyd. +* Don't reload roomview on offline connectivity check ([#29243](https://github.com/element-hq/element-web/pull/29243)). Contributed by @dbkr. +* Respect user's 12/24 hour preference consistently ([#29237](https://github.com/element-hq/element-web/pull/29237)). Contributed by @t3chguy. +* Restore the accessibility role on call views ([#29225](https://github.com/element-hq/element-web/pull/29225)). Contributed by @robintown. +* Revert `GoToHome` keyboard shortcut to `Ctrl`–`Shift`–`H` on macOS ([#28577](https://github.com/element-hq/element-web/pull/28577)). Contributed by @gy-mate. +* Encryption tab: display correct encryption panel when user cancels the reset identity flow ([#29216](https://github.com/element-hq/element-web/pull/29216)). Contributed by @florianduros. + + Changes in [1.11.92](https://github.com/element-hq/element-web/releases/tag/v1.11.92) (2025-02-11) ================================================================================================== ## ✨ Features diff --git a/Dockerfile b/Dockerfile index 93d7c676d9..1103d7336f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,5 @@ +# syntax=docker.io/docker/dockerfile:1.7-labs + # Builder FROM --platform=$BUILDPLATFORM node:22-bullseye AS builder @@ -8,7 +10,7 @@ ARG JS_SDK_BRANCH="master" WORKDIR /src -COPY . /src +COPY --exclude=docker . /src RUN /src/scripts/docker-link-repos.sh RUN yarn --network-timeout=200000 install RUN /src/scripts/docker-package.sh @@ -19,11 +21,15 @@ RUN cp /src/config.sample.json /src/webapp/config.json # App FROM nginx:alpine-slim +# Install jq and moreutils for sponge, both used by our entrypoints +RUN apk add jq moreutils + COPY --from=builder /src/webapp /app # Override default nginx config. Templates in `/etc/nginx/templates` are passed # through `envsubst` by the nginx docker image entry point. COPY /docker/nginx-templates/* /etc/nginx/templates/ +COPY /docker/docker-entrypoint.d/* /docker-entrypoint.d/ # Tell nginx to put its pidfile elsewhere, so it can run as non-root RUN sed -i -e 's,/var/run/nginx.pid,/tmp/nginx.pid,' /etc/nginx/nginx.conf @@ -40,3 +46,5 @@ USER nginx # HTTP listen port ENV ELEMENT_WEB_PORT=80 + +HEALTHCHECK --start-period=5s CMD wget --retry-connrefused --tries=5 -q --wait=3 --spider http://localhost:$ELEMENT_WEB_PORT/config.json diff --git a/docker/docker-entrypoint.d/18-load-element-modules.sh b/docker/docker-entrypoint.d/18-load-element-modules.sh new file mode 100755 index 0000000000..15c0cb6086 --- /dev/null +++ b/docker/docker-entrypoint.d/18-load-element-modules.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +# Loads modules from `/tmp/element-web-modules` into config.json's `modules` field + +set -e + +entrypoint_log() { + if [ -z "${NGINX_ENTRYPOINT_QUIET_LOGS:-}" ]; then + echo "$@" + fi +} + +# Copy these config files as a base +mkdir /tmp/element-web-config +cp /app/config*.json /tmp/element-web-config/ + +# If there are modules to be loaded +if [ -d "/tmp/element-web-modules" ]; then + cd /tmp/element-web-modules + + for MODULE in * + do + # If the module has a package.json, use its main field as the entrypoint + ENTRYPOINT="index.js" + if [ -f "/tmp/element-web-modules/$MODULE/package.json" ]; then + ENTRYPOINT=$(jq -r '.main' "/tmp/element-web-modules/$MODULE/package.json") + fi + + entrypoint_log "Loading module $MODULE with entrypoint $ENTRYPOINT" + + # Append the module to the config + jq ".modules += [\"/modules/$MODULE/$ENTRYPOINT\"]" /tmp/element-web-config/config.json | sponge /tmp/element-web-config/config.json + done +fi diff --git a/docker/nginx-templates/default.conf.template b/docker/nginx-templates/default.conf.template index 06f33e08dd..53870038b6 100644 --- a/docker/nginx-templates/default.conf.template +++ b/docker/nginx-templates/default.conf.template @@ -18,8 +18,12 @@ server { } # covers config.json and config.hostname.json requests as it is prefix. location /config { + root /tmp/element-web-config; add_header Cache-Control "no-cache"; } + location /modules { + alias /tmp/element-web-modules; + } # redirect server error pages to the static page /50x.html # error_page 500 502 503 504 /50x.html; diff --git a/docs/install.md b/docs/install.md index f6bd98611c..743ae0a93a 100644 --- a/docs/install.md +++ b/docs/install.md @@ -66,6 +66,18 @@ on other runtimes may require root privileges. To resolve this, either run the image as root (`docker run --user 0`) or, better, change the port that nginx listens on via the `ELEMENT_WEB_PORT` environment variable. +[Element Web Modules](https://github.com/element-hq/element-modules/tree/main/packages/element-web-module-api) can be dynamically loaded +by being made available (e.g. via bind mount) in a directory within `/tmp/element-web-modules/`. +The default entrypoint will be index.js in that directory but can be overridden if a package.json file is found with a `main` directive. +These modules will be presented in a `/modules` subdirectory within the webroot, and automatically added to the config.json `modules` field. + +If you wish to use docker in read-only mode, +you should follow the [upstream instructions](https://hub.docker.com/_/nginx#:~:text=Running%20nginx%20in%20read%2Donly%20mode) +but additionally include the following directories: + +- /tmp/element-web-config/ +- /etc/nginx/conf.d/ + The behaviour of the docker image can be customised via the following environment variables: diff --git a/package.json b/package.json index a3390c11c0..262764ec41 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "element-web", - "version": "1.11.91", + "version": "1.11.93", "description": "Element: the future of secure communication", "author": "New Vector Ltd.", "repository": { @@ -91,8 +91,8 @@ "@sentry/browser": "^9.0.0", "@types/png-chunks-extract": "^1.0.2", "@types/react-virtualized": "^9.21.30", - "@vector-im/compound-design-tokens": "^3.0.0", - "@vector-im/compound-web": "^7.6.1", + "@vector-im/compound-design-tokens": "^4.0.0", + "@vector-im/compound-web": "^7.6.4", "@vector-im/matrix-wysiwyg": "2.38.0", "@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4", diff --git a/playwright/e2e/crypto/dehydration.spec.ts b/playwright/e2e/crypto/dehydration.spec.ts index 472ee39493..35b5fae1ad 100644 --- a/playwright/e2e/crypto/dehydration.spec.ts +++ b/playwright/e2e/crypto/dehydration.spec.ts @@ -116,6 +116,40 @@ test.describe("Dehydration", () => { expect(dehydratedDeviceIds.length).toBe(1); expect(dehydratedDeviceIds[0]).not.toEqual(initialDehydratedDeviceIds[0]); }); + + test("'Reset cryptographic identity' removes dehydrated device", async ({ page, homeserver, app, credentials }) => { + await logIntoElement(page, credentials); + + // Create a dehydrated device by setting up recovery (see "'Set up + // recovery' creates dehydrated device" test above) + const settingsDialogLocator = await app.settings.openUserSettings("Encryption"); + await settingsDialogLocator.getByRole("button", { name: "Set up recovery" }).click(); + + // First it displays an informative panel about the recovery key + await expect(settingsDialogLocator.getByRole("heading", { name: "Set up recovery" })).toBeVisible(); + await settingsDialogLocator.getByRole("button", { name: "Continue" }).click(); + + // Next, it displays the new recovery key. We click on the copy button. + await expect(settingsDialogLocator.getByText("Save your recovery key somewhere safe")).toBeVisible(); + await settingsDialogLocator.getByRole("button", { name: "Copy" }).click(); + const recoveryKey = await app.getClipboard(); + await settingsDialogLocator.getByRole("button", { name: "Continue" }).click(); + + await expect( + settingsDialogLocator.getByText("Enter your recovery key to confirm", { exact: true }), + ).toBeVisible(); + await settingsDialogLocator.getByRole("textbox").fill(recoveryKey); + await settingsDialogLocator.getByRole("button", { name: "Finish set up" }).click(); + + await expectDehydratedDeviceEnabled(app); + + // After recovery is set up, we reset our cryptographic identity, which + // should drop the dehydrated device. + await settingsDialogLocator.getByRole("button", { name: "Reset cryptographic identity" }).click(); + await settingsDialogLocator.getByRole("button", { name: "Continue" }).click(); + + await expectDehydratedDeviceDisabled(app); + }); }); async function getDehydratedDeviceIds(client: Client): Promise { @@ -144,3 +178,16 @@ async function expectDehydratedDeviceEnabled(app: ElementAppPage): Promise }) .toEqual(1); } + +/** Wait for our user to not have a dehydrated device */ +async function expectDehydratedDeviceDisabled(app: ElementAppPage): Promise { + // It might be nice to do this via the UI, but currently this info is not exposed via the UI. + // + // Note we might have to wait for the device list to be refreshed, so we wrap in `expect.poll`. + await expect + .poll(async () => { + const dehydratedDeviceIds = await getDehydratedDeviceIds(app.client); + return dehydratedDeviceIds.length; + }) + .toEqual(0); +} diff --git a/playwright/e2e/crypto/device-verification.spec.ts b/playwright/e2e/crypto/device-verification.spec.ts index ee710d1cfb..9be79452c4 100644 --- a/playwright/e2e/crypto/device-verification.spec.ts +++ b/playwright/e2e/crypto/device-verification.spec.ts @@ -21,6 +21,7 @@ import { waitForVerificationRequest, } from "./utils"; import { type Bot } from "../../pages/bot"; +import { Toasts } from "../../pages/toasts.ts"; test.describe("Device verification", { tag: "@no-webkit" }, () => { let aliceBotClient: Bot; @@ -72,6 +73,51 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => { await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, false); }); + // Regression test for https://github.com/element-hq/element-web/issues/29110 + test("No toast after verification, even if the secrets take a while to arrive", async ({ page, credentials }) => { + // Before we log in, the bot creates an encrypted room, so that we can test the toast behaviour that only happens + // when we are in an encrypted room. + await aliceBotClient.createRoom({ + initial_state: [ + { + type: "m.room.encryption", + state_key: "", + content: { algorithm: "m.megolm.v1.aes-sha2" }, + }, + ], + }); + + // In order to simulate a real environment more accurately, we need to slow down the arrival of the + // `m.secret.send` to-device messages. That's slightly tricky to do directly, so instead we delay the *outgoing* + // `m.secret.request` messages. + await page.route("**/_matrix/client/v3/sendToDevice/m.secret.request/**", async (route) => { + await route.fulfill({ json: {} }); + await new Promise((f) => setTimeout(f, 1000)); + await route.fetch(); + }); + + await logIntoElement(page, credentials); + + // Launch the verification request between alice and the bot + const verificationRequest = await initiateAliceVerificationRequest(page); + + // Handle emoji SAS verification + const infoDialog = page.locator(".mx_InfoDialog"); + // the bot chooses to do an emoji verification + const verifier = await verificationRequest.evaluateHandle((request) => request.startVerification("m.sas.v1")); + + // Handle emoji request and check that emojis are matching + await doTwoWaySasVerification(page, verifier); + + await infoDialog.getByRole("button", { name: "They match" }).click(); + await infoDialog.getByRole("button", { name: "Got it" }).click(); + + // There should be no toast (other than the notifications one) + const toasts = new Toasts(page); + await toasts.rejectToast("Notifications"); + await toasts.assertNoToasts(); + }); + test("Verify device with QR code during login", async ({ page, app, credentials, homeserver }) => { // A mode 0x02 verification: "self-verifying in which the current device does not yet trust the master key" await logIntoElement(page, credentials); diff --git a/playwright/e2e/left-panel/room-list-view/room-list-header.spec.ts b/playwright/e2e/left-panel/room-list-view/room-list-header.spec.ts index bda87b0462..daa8d3869f 100644 --- a/playwright/e2e/left-panel/room-list-view/room-list-header.spec.ts +++ b/playwright/e2e/left-panel/room-list-view/room-list-header.spec.ts @@ -47,12 +47,41 @@ test.describe("Header section of the room list", () => { await app.closeDialog(); }); - test("should render the header section for a space", async ({ page, app, user }) => { + test("should render the header section for a space", { tag: "@screenshot" }, async ({ page, app, user }) => { await app.client.createSpace({ name: "MySpace" }); await page.getByRole("button", { name: "MySpace" }).click(); const roomListHeader = getHeaderSection(page); + await expect(roomListHeader).toMatchScreenshot("room-list-space-header.png"); + await expect(roomListHeader.getByRole("heading", { name: "MySpace" })).toBeVisible(); - await expect(roomListHeader.getByRole("button", { name: "Add" })).not.toBeVisible(); + await expect(roomListHeader.getByRole("button", { name: "Add" })).toBeVisible(); + + const spaceMenu = roomListHeader.getByRole("button", { name: "Open space menu" }); + await spaceMenu.click(); + + await expect(page.getByRole("menu")).toMatchScreenshot("room-list-header-space-menu.png"); + + // It should open the space home + await page.getByRole("menuitem", { name: "Space home" }).click(); + await expect(page.getByRole("main").getByRole("heading", { name: "MySpace" })).toBeVisible(); + + // It should open the invite dialog + await spaceMenu.click(); + await page.getByRole("menuitem", { name: "Invite" }).click(); + await expect(page.getByRole("heading", { name: "Invite to MySpace" })).toBeVisible(); + await app.closeDialog(); + + // It should open the space preferences + await spaceMenu.click(); + await page.getByRole("menuitem", { name: "Preferences" }).click(); + await expect(page.getByRole("heading", { name: "Preferences" })).toBeVisible(); + await app.closeDialog(); + + // It should open the space settings + await spaceMenu.click(); + await page.getByRole("menuitem", { name: "Space Settings" }).click(); + await expect(page.getByRole("heading", { name: "Settings" })).toBeVisible(); + await app.closeDialog(); }); }); diff --git a/playwright/snapshots/left-panel/room-list-view/room-list-header.spec.ts/room-list-header-linux.png b/playwright/snapshots/left-panel/room-list-view/room-list-header.spec.ts/room-list-header-linux.png index c139730915..82636daa73 100644 Binary files a/playwright/snapshots/left-panel/room-list-view/room-list-header.spec.ts/room-list-header-linux.png and b/playwright/snapshots/left-panel/room-list-view/room-list-header.spec.ts/room-list-header-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-view/room-list-header.spec.ts/room-list-header-space-menu-linux.png b/playwright/snapshots/left-panel/room-list-view/room-list-header.spec.ts/room-list-header-space-menu-linux.png new file mode 100644 index 0000000000..d2934c2a76 Binary files /dev/null and b/playwright/snapshots/left-panel/room-list-view/room-list-header.spec.ts/room-list-header-space-menu-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-view/room-list-header.spec.ts/room-list-space-header-linux.png b/playwright/snapshots/left-panel/room-list-view/room-list-header.spec.ts/room-list-space-header-linux.png new file mode 100644 index 0000000000..bdb7f8fa2a Binary files /dev/null and b/playwright/snapshots/left-panel/room-list-view/room-list-header.spec.ts/room-list-space-header-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-view/room-list-view.spec.ts/room-list-view-linux.png b/playwright/snapshots/left-panel/room-list-view/room-list-view.spec.ts/room-list-view-linux.png index b1c7960e3b..378d5ef926 100644 Binary files a/playwright/snapshots/left-panel/room-list-view/room-list-view.spec.ts/room-list-view-linux.png and b/playwright/snapshots/left-panel/room-list-view/room-list-view.spec.ts/room-list-view-linux.png differ diff --git a/playwright/testcontainers/synapse.ts b/playwright/testcontainers/synapse.ts index 82d567185d..13250c86d9 100644 --- a/playwright/testcontainers/synapse.ts +++ b/playwright/testcontainers/synapse.ts @@ -25,7 +25,7 @@ import { type HomeserverContainer, type StartedHomeserverContainer } from "./Hom import { type StartedMatrixAuthenticationServiceContainer } from "./mas.ts"; import { Api, ClientServerApi, type Verb } from "../plugins/utils/api.ts"; -const TAG = "develop@sha256:cdf9ad343d8d9cae7ae37078dd3a07e8ec88fba30ba014e767bab1f752eb9752"; +const TAG = "develop@sha256:8d1c531cf6010b63142a04e1b138a60720946fa131ad404813232f02db4ce7ba"; const DEFAULT_CONFIG = { server_name: "localhost", diff --git a/res/css/structures/_SpaceHierarchy.pcss b/res/css/structures/_SpaceHierarchy.pcss index 31dad9413f..02f39a0b72 100644 --- a/res/css/structures/_SpaceHierarchy.pcss +++ b/res/css/structures/_SpaceHierarchy.pcss @@ -77,7 +77,7 @@ Please see LICENSE files in the repository root for full details. height: 16px; width: 16px; left: 0; - background-image: url("@vector-im/compound-design-tokens/icons/error.svg"); + background-image: url("@vector-im/compound-design-tokens/icons/error-solid.svg"); background-size: cover; background-repeat: no-repeat; } diff --git a/res/css/views/context_menus/_MessageContextMenu.pcss b/res/css/views/context_menus/_MessageContextMenu.pcss index f365c4a293..9fc454f328 100644 --- a/res/css/views/context_menus/_MessageContextMenu.pcss +++ b/res/css/views/context_menus/_MessageContextMenu.pcss @@ -29,7 +29,7 @@ Please see LICENSE files in the repository root for full details. } .mx_MessageContextMenu_iconReport::before { - mask-image: url("@vector-im/compound-design-tokens/icons/error.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/error-solid.svg"); } .mx_MessageContextMenu_iconLink::before { diff --git a/res/css/views/dialogs/security/_AccessSecretStorageDialog.pcss b/res/css/views/dialogs/security/_AccessSecretStorageDialog.pcss index da71b4462b..83b9fe96b4 100644 --- a/res/css/views/dialogs/security/_AccessSecretStorageDialog.pcss +++ b/res/css/views/dialogs/security/_AccessSecretStorageDialog.pcss @@ -21,7 +21,7 @@ Please see LICENSE files in the repository root for full details. &.mx_AccessSecretStorageDialog_resetBadge::before { /* The image isn't capable of masking, so we use a background instead. */ - background-image: url("@vector-im/compound-design-tokens/icons/error.svg"); + background-image: url("@vector-im/compound-design-tokens/icons/error-solid.svg"); background-size: 24px; background-color: transparent; } @@ -120,7 +120,7 @@ Please see LICENSE files in the repository root for full details. width: 16px; left: 0; top: 2px; /* alignment */ - background-image: url("@vector-im/compound-design-tokens/icons/error.svg"); + background-image: url("@vector-im/compound-design-tokens/icons/error-solid.svg"); background-size: contain; } diff --git a/res/css/views/elements/_InfoTooltip.pcss b/res/css/views/elements/_InfoTooltip.pcss index 5229b7d9f5..a214f0bf83 100644 --- a/res/css/views/elements/_InfoTooltip.pcss +++ b/res/css/views/elements/_InfoTooltip.pcss @@ -29,5 +29,5 @@ Please see LICENSE files in the repository root for full details. } .mx_InfoTooltip_icon_warning::before { - mask-image: url("@vector-im/compound-design-tokens/icons/error.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/error-solid.svg"); } diff --git a/res/css/views/rooms/RoomListView/_RoomListHeaderView.pcss b/res/css/views/rooms/RoomListView/_RoomListHeaderView.pcss index 0b80571062..6a88e61332 100644 --- a/res/css/views/rooms/RoomListView/_RoomListHeaderView.pcss +++ b/res/css/views/rooms/RoomListView/_RoomListHeaderView.pcss @@ -11,10 +11,22 @@ h1 { all: unset; - font: var(--cpd-font-body-lg-semibold); + font: var(--cpd-font-heading-sm-semibold); } button { color: var(--cpd-color-icon-secondary); } + + .mx_SpaceMenu_button { + svg { + transition: transform 0.1s linear; + } + } + + .mx_SpaceMenu_button[aria-expanded="true"] { + svg { + transform: rotate(180deg); + } + } } diff --git a/res/css/views/rooms/RoomListView/_RoomListSearch.pcss b/res/css/views/rooms/RoomListView/_RoomListSearch.pcss index 55b1def58a..f175ab3976 100644 --- a/res/css/views/rooms/RoomListView/_RoomListSearch.pcss +++ b/res/css/views/rooms/RoomListView/_RoomListSearch.pcss @@ -9,7 +9,7 @@ /* From figma, this should be aligned with the room header */ height: 64px; box-sizing: border-box; - border-bottom: 1px solid var(--cpd-color-bg-subtle-primary); + border-bottom: var(--cpd-border-width-1) solid var(--cpd-color-bg-subtle-primary); padding: 0 var(--cpd-space-3x); svg { @@ -31,7 +31,7 @@ } } - .mx_RoomListSearch_explore:hover { + .mx_RoomListSearch_button:hover { svg { fill: var(--cpd-color-icon-primary); } diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts index 5445082e25..cf46be41fa 100644 --- a/src/DeviceListener.ts +++ b/src/DeviceListener.ts @@ -15,9 +15,10 @@ import { type SyncState, ClientStoppedError, } from "matrix-js-sdk/src/matrix"; -import { logger as baseLogger } from "matrix-js-sdk/src/logger"; +import { logger as baseLogger, LogSpan } from "matrix-js-sdk/src/logger"; import { CryptoEvent, type KeyBackupInfo } from "matrix-js-sdk/src/crypto-api"; import { type CryptoSessionStateChange } from "@matrix-org/analytics-events/types/typescript/CryptoSessionStateChange"; +import { secureRandomString } from "matrix-js-sdk/src/randomstring"; import { PosthogAnalytics } from "./PosthogAnalytics"; import dis from "./dispatcher/dispatcher"; @@ -96,6 +97,7 @@ export default class DeviceListener { this.client.on(ClientEvent.AccountData, this.onAccountData); this.client.on(ClientEvent.Sync, this.onSync); this.client.on(RoomStateEvent.Events, this.onRoomStateEvents); + this.client.on(ClientEvent.ToDeviceEvent, this.onToDeviceEvent); this.shouldRecordClientInformation = SettingsStore.getValue("deviceClientInformationOptIn"); // only configurable in config, so we don't need to watch the value this.enableBulkUnverifiedSessionsReminder = SettingsStore.getValue(UIFeature.BulkUnverifiedSessionsReminder); @@ -118,6 +120,7 @@ export default class DeviceListener { this.client.removeListener(ClientEvent.AccountData, this.onAccountData); this.client.removeListener(ClientEvent.Sync, this.onSync); this.client.removeListener(RoomStateEvent.Events, this.onRoomStateEvents); + this.client.removeListener(ClientEvent.ToDeviceEvent, this.onToDeviceEvent); } SettingsStore.unwatchSetting(this.deviceClientInformationSettingWatcherRef); dis.unregister(this.dispatcherRef); @@ -225,6 +228,11 @@ export default class DeviceListener { this.updateClientInformation(); }; + private onToDeviceEvent = (event: MatrixEvent): void => { + // Receiving a 4S secret can mean we are in sync where we were not before. + if (event.getType() === EventType.SecretSend) this.recheck(); + }; + /** * Fetch the key backup information from the server. * @@ -273,18 +281,29 @@ export default class DeviceListener { private async doRecheck(): Promise { if (!this.running || !this.client) return; // we have been stopped + const logSpan = new LogSpan(logger, "check_" + secureRandomString(4)); + const cli = this.client; // cross-signing support was added to Matrix in MSC1756, which landed in spec v1.1 - if (!(await cli.isVersionSupported("v1.1"))) return; + if (!(await cli.isVersionSupported("v1.1"))) { + logSpan.debug("cross-signing not supported"); + return; + } const crypto = cli.getCrypto(); - if (!crypto) return; + if (!crypto) { + logSpan.debug("crypto not enabled"); + return; + } // don't recheck until the initial sync is complete: lots of account data events will fire // while the initial sync is processing and we don't need to recheck on each one of them // (we add a listener on sync to do once check after the initial sync is done) - if (!cli.isInitialSyncComplete()) return; + if (!cli.isInitialSyncComplete()) { + logSpan.debug("initial sync not yet complete"); + return; + } const crossSigningReady = await crypto.isCrossSigningReady(); const secretStorageReady = await crypto.isSecretStorageReady(); @@ -306,6 +325,7 @@ export default class DeviceListener { await this.reportCryptoSessionStateToAnalytics(cli); if (this.dismissedThisDeviceToast || allSystemsReady) { + logSpan.info("No toast needed"); hideSetupEncryptionToast(); this.checkKeyBackupStatus(); @@ -316,27 +336,33 @@ export default class DeviceListener { if (!crossSigningReady) { // This account is legacy and doesn't have cross-signing set up at all. // Prompt the user to set it up. - logger.info("Cross-signing not ready: showing SET_UP_ENCRYPTION toast"); + logSpan.info("Cross-signing not ready: showing SET_UP_ENCRYPTION toast"); showSetupEncryptionToast(SetupKind.SET_UP_ENCRYPTION); } else if (!isCurrentDeviceTrusted) { // cross signing is ready but the current device is not trusted: prompt the user to verify - logger.info("Current device not verified: showing VERIFY_THIS_SESSION toast"); + logSpan.info("Current device not verified: showing VERIFY_THIS_SESSION toast"); showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION); } else if (!allCrossSigningSecretsCached) { // cross signing ready & device trusted, but we are missing secrets from our local cache. // prompt the user to enter their recovery key. - logger.info("Some secrets not cached: showing KEY_STORAGE_OUT_OF_SYNC toast"); + logSpan.info( + "Some secrets not cached: showing KEY_STORAGE_OUT_OF_SYNC toast", + crossSigningStatus.privateKeysCachedLocally, + ); showSetupEncryptionToast(SetupKind.KEY_STORAGE_OUT_OF_SYNC); } else if (defaultKeyId === null) { // the user just hasn't set up 4S yet: prompt them to do so (unless they've explicitly said no to key storage) const disabledEvent = cli.getAccountData(BACKUP_DISABLED_ACCOUNT_DATA_KEY); if (!disabledEvent?.getContent().disabled) { + logSpan.info("No default 4S key: showing SET_UP_RECOVERY toast"); showSetupEncryptionToast(SetupKind.SET_UP_RECOVERY); + } else { + logSpan.info("No default 4S key but backup disabled: no toast needed"); } } else { // some other condition... yikes! Show the 'set up encryption' toast: this is what we previously did // in 'other' situations. Possibly we should consider prompting for a full reset in this case? - logger.warn("Couldn't match encryption state to a known case: showing 'setup encryption' prompt", { + logSpan.warn("Couldn't match encryption state to a known case: showing 'setup encryption' prompt", { crossSigningReady, secretStorageReady, allCrossSigningSecretsCached, @@ -345,6 +371,8 @@ export default class DeviceListener { }); showSetupEncryptionToast(SetupKind.SET_UP_ENCRYPTION); } + } else { + logSpan.info("Not yet ready, but shouldShowSetupEncryptionToast==false"); } // This needs to be done after awaiting on getUserDeviceInfo() above, so @@ -377,9 +405,9 @@ export default class DeviceListener { } } - logger.debug("Old unverified sessions: " + Array.from(oldUnverifiedDeviceIds).join(",")); - logger.debug("New unverified sessions: " + Array.from(newUnverifiedDeviceIds).join(",")); - logger.debug("Currently showing toasts for: " + Array.from(this.displayingToastsForDeviceIds).join(",")); + logSpan.debug("Old unverified sessions: " + Array.from(oldUnverifiedDeviceIds).join(",")); + logSpan.debug("New unverified sessions: " + Array.from(newUnverifiedDeviceIds).join(",")); + logSpan.debug("Currently showing toasts for: " + Array.from(this.displayingToastsForDeviceIds).join(",")); const isBulkUnverifiedSessionsReminderSnoozed = isBulkUnverifiedDeviceReminderSnoozed(); @@ -404,7 +432,7 @@ export default class DeviceListener { // ...and hide any we don't need any more for (const deviceId of this.displayingToastsForDeviceIds) { if (!newUnverifiedDeviceIds.has(deviceId)) { - logger.debug("Hiding unverified session toast for " + deviceId); + logSpan.debug("Hiding unverified session toast for " + deviceId); hideUnverifiedSessionsToast(deviceId); } } diff --git a/src/components/viewmodels/roomlist/RoomListHeaderViewModel.tsx b/src/components/viewmodels/roomlist/RoomListHeaderViewModel.tsx index 3284933a9b..fb822b8129 100644 --- a/src/components/viewmodels/roomlist/RoomListHeaderViewModel.tsx +++ b/src/components/viewmodels/roomlist/RoomListHeaderViewModel.tsx @@ -6,7 +6,7 @@ */ import { useCallback } from "react"; -import { type Room, RoomEvent, RoomType } from "matrix-js-sdk/src/matrix"; +import { JoinRule, type Room, RoomEvent, RoomType } from "matrix-js-sdk/src/matrix"; import { shouldShowComponent } from "../../../customisations/helpers/UIComponents"; import { UIComponent } from "../../../settings/UIFeature"; @@ -23,6 +23,15 @@ import { UPDATE_SELECTED_SPACE, } from "../../../stores/spaces"; import SpaceStore from "../../../stores/spaces/SpaceStore"; +import { + shouldShowSpaceSettings, + showCreateNewRoom, + showSpaceInvite, + showSpacePreferences, + showSpaceSettings, +} from "../../../utils/space"; +import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; +import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; /** * Hook to get the active space and its title. @@ -55,9 +64,14 @@ export interface RoomListHeaderViewState { title: string; /** * Whether to display the compose menu - * True if the user can create rooms and is not in a Space + * True if the user can create rooms */ displayComposeMenu: boolean; + /** + * Whether to display the space menu + * True if there is an active space + */ + displaySpaceMenu: boolean; /** * Whether the user can create rooms */ @@ -66,6 +80,14 @@ export interface RoomListHeaderViewState { * Whether the user can create video rooms */ canCreateVideoRoom: boolean; + /** + * Whether the user can invite in the active space + */ + canInviteInSpace: boolean; + /** + * Whether the user can access space settings + */ + canAccessSpaceSettings: boolean; /** * Create a chat room * @param e - The click event @@ -80,19 +102,39 @@ export interface RoomListHeaderViewState { * Create a video room */ createVideoRoom: () => void; + /** + * Open the active space home + */ + openSpaceHome: () => void; + /** + * Display the space invite dialog + */ + inviteInSpace: () => void; + /** + * Open the space preferences + */ + openSpacePreferences: () => void; + /** + * Open the space settings + */ + openSpaceSettings: () => void; } /** * View model for the RoomListHeader. - * The actions don't work when called in a space yet. */ export function useRoomListHeaderViewModel(): RoomListHeaderViewState { + const matrixClient = useMatrixClientContext(); const { activeSpace, title } = useSpace(); const canCreateRoom = shouldShowComponent(UIComponent.CreateRooms); const canCreateVideoRoom = useFeatureEnabled("feature_video_rooms"); - // Temporary: don't display the compose menu when in a Space - const displayComposeMenu = canCreateRoom && !activeSpace; + const displayComposeMenu = canCreateRoom; + const displaySpaceMenu = Boolean(activeSpace); + const canInviteInSpace = Boolean( + activeSpace?.getJoinRule() === JoinRule.Public || activeSpace?.canInvite(matrixClient.getSafeUserId()), + ); + const canAccessSpaceSettings = Boolean(activeSpace && shouldShowSpaceSettings(activeSpace)); /* Actions */ @@ -101,28 +143,73 @@ export function useRoomListHeaderViewModel(): RoomListHeaderViewState { PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateChatItem", e); }, []); - const createRoom = useCallback((e: Event) => { - defaultDispatcher.fire(Action.CreateRoom); - PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateRoomItem", e); - }, []); + const createRoom = useCallback( + (e: Event) => { + if (activeSpace) { + showCreateNewRoom(activeSpace); + } else { + defaultDispatcher.fire(Action.CreateRoom); + } + PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateRoomItem", e); + }, + [activeSpace], + ); const elementCallVideoRoomsEnabled = useFeatureEnabled("feature_element_call_video_rooms"); - const createVideoRoom = useCallback( - () => + const createVideoRoom = useCallback(() => { + const type = elementCallVideoRoomsEnabled ? RoomType.UnstableCall : RoomType.ElementVideo; + if (activeSpace) { + showCreateNewRoom(activeSpace, type); + } else { defaultDispatcher.dispatch({ action: Action.CreateRoom, - type: elementCallVideoRoomsEnabled ? RoomType.UnstableCall : RoomType.ElementVideo, - }), - [elementCallVideoRoomsEnabled], - ); + type, + }); + } + }, [activeSpace, elementCallVideoRoomsEnabled]); + + const openSpaceHome = useCallback(() => { + // openSpaceHome is only available when there is an active space + if (!activeSpace) return; + defaultDispatcher.dispatch({ + action: Action.ViewRoom, + room_id: activeSpace.roomId, + metricsTrigger: undefined, + }); + }, [activeSpace]); + + const inviteInSpace = useCallback(() => { + // inviteInSpace is only available when there is an active space + if (!activeSpace) return; + showSpaceInvite(activeSpace); + }, [activeSpace]); + + const openSpacePreferences = useCallback(() => { + // openSpacePreferences is only available when there is an active space + if (!activeSpace) return; + showSpacePreferences(activeSpace); + }, [activeSpace]); + + const openSpaceSettings = useCallback(() => { + // openSpaceSettings is only available when there is an active space + if (!activeSpace) return; + showSpaceSettings(activeSpace); + }, [activeSpace]); return { title, displayComposeMenu, + displaySpaceMenu, canCreateRoom, canCreateVideoRoom, + canInviteInSpace, + canAccessSpaceSettings, createChatRoom, createRoom, createVideoRoom, + openSpaceHome, + inviteInSpace, + openSpacePreferences, + openSpaceSettings, }; } diff --git a/src/components/views/auth/LoginWithQRFlow.tsx b/src/components/views/auth/LoginWithQRFlow.tsx index 588e599098..b246013421 100644 --- a/src/components/views/auth/LoginWithQRFlow.tsx +++ b/src/components/views/auth/LoginWithQRFlow.tsx @@ -10,7 +10,7 @@ import React, { createRef, type ReactNode } from "react"; import { ClientRendezvousFailureReason, MSC4108FailureReason } from "matrix-js-sdk/src/rendezvous"; import ChevronLeftIcon from "@vector-im/compound-design-tokens/assets/web/icons/chevron-left"; import CheckCircleSolidIcon from "@vector-im/compound-design-tokens/assets/web/icons/check-circle-solid"; -import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error"; +import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error-solid"; import { Heading, MFAInput, Text } from "@vector-im/compound-web"; import classNames from "classnames"; import { QrCodeIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; diff --git a/src/components/views/elements/AccessibleButton.tsx b/src/components/views/elements/AccessibleButton.tsx index efb6e5f939..a07e40c1d7 100644 --- a/src/components/views/elements/AccessibleButton.tsx +++ b/src/components/views/elements/AccessibleButton.tsx @@ -80,7 +80,7 @@ type Props = { /** * The tooltip to show on hover or focus. */ - title?: TooltipProps["label"]; + title?: string; /** * The caption is a secondary text displayed under the `title` of the tooltip. * Only valid when used in conjunction with `title`. diff --git a/src/components/views/right_panel/RoomSummaryCard.tsx b/src/components/views/right_panel/RoomSummaryCard.tsx index 8c32c21f0f..2d3453feba 100644 --- a/src/components/views/right_panel/RoomSummaryCard.tsx +++ b/src/components/views/right_panel/RoomSummaryCard.tsx @@ -35,7 +35,7 @@ import PinIcon from "@vector-im/compound-design-tokens/assets/web/icons/pin"; import LockIcon from "@vector-im/compound-design-tokens/assets/web/icons/lock-solid"; import LockOffIcon from "@vector-im/compound-design-tokens/assets/web/icons/lock-off"; import PublicIcon from "@vector-im/compound-design-tokens/assets/web/icons/public"; -import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error"; +import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error-solid"; import ChevronDownIcon from "@vector-im/compound-design-tokens/assets/web/icons/chevron-down"; import { EventType, JoinRule, type Room, RoomStateEvent } from "matrix-js-sdk/src/matrix"; diff --git a/src/components/views/rooms/MemberList/tiles/common/E2EIconView.tsx b/src/components/views/rooms/MemberList/tiles/common/E2EIconView.tsx index a1f37e5185..c25d40fc58 100644 --- a/src/components/views/rooms/MemberList/tiles/common/E2EIconView.tsx +++ b/src/components/views/rooms/MemberList/tiles/common/E2EIconView.tsx @@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details. import React from "react"; import { Tooltip } from "@vector-im/compound-web"; import VerifiedIcon from "@vector-im/compound-design-tokens/assets/web/icons/verified"; -import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error"; +import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error-solid"; import { _t } from "../../../../../../languageHandler"; import { E2EStatus } from "../../../../../../utils/ShieldUtils"; diff --git a/src/components/views/rooms/RoomHeader/RoomHeader.tsx b/src/components/views/rooms/RoomHeader/RoomHeader.tsx index 964295f0be..dbd8c4d073 100644 --- a/src/components/views/rooms/RoomHeader/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader/RoomHeader.tsx @@ -15,7 +15,7 @@ import ThreadsIcon from "@vector-im/compound-design-tokens/assets/web/icons/thre import RoomInfoIcon from "@vector-im/compound-design-tokens/assets/web/icons/info-solid"; import NotificationsIcon from "@vector-im/compound-design-tokens/assets/web/icons/notifications-solid"; import VerifiedIcon from "@vector-im/compound-design-tokens/assets/web/icons/verified"; -import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error"; +import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error-solid"; import PublicIcon from "@vector-im/compound-design-tokens/assets/web/icons/public"; import { JoinRule, type Room } from "matrix-js-sdk/src/matrix"; import { type ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle"; diff --git a/src/components/views/rooms/RoomListView/RoomListHeaderView.tsx b/src/components/views/rooms/RoomListView/RoomListHeaderView.tsx index 478b9b7e70..7c82dabc2a 100644 --- a/src/components/views/rooms/RoomListView/RoomListHeaderView.tsx +++ b/src/components/views/rooms/RoomListView/RoomListHeaderView.tsx @@ -8,7 +8,11 @@ import React, { type JSX, useState } from "react"; import { IconButton, Menu, MenuItem } from "@vector-im/compound-web"; import ComposeIcon from "@vector-im/compound-design-tokens/assets/web/icons/compose"; import UserAddIcon from "@vector-im/compound-design-tokens/assets/web/icons/user-add"; +import ChevronDownIcon from "@vector-im/compound-design-tokens/assets/web/icons/chevron-down"; import RoomIcon from "@vector-im/compound-design-tokens/assets/web/icons/room"; +import HomeIcon from "@vector-im/compound-design-tokens/assets/web/icons/home"; +import PreferencesIcon from "@vector-im/compound-design-tokens/assets/web/icons/preferences"; +import SettingsIcon from "@vector-im/compound-design-tokens/assets/web/icons/settings"; import VideoCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/video-call"; import { _t } from "../../../../languageHandler"; @@ -34,12 +38,57 @@ export function RoomListHeaderView(): JSX.Element { align="center" data-testid="room-list-header" > -

{vm.title}

+ +

{vm.title}

+ {vm.displaySpaceMenu && } +
{vm.displayComposeMenu && } ); } +interface SpaceMenuProps { + /** + * The view model for the room list header + */ + vm: RoomListHeaderViewState; +} + +/** + * The space menu for the room list header + */ +function SpaceMenu({ vm }: SpaceMenuProps): JSX.Element { + const [open, setOpen] = useState(false); + + return ( + + + + } + > + + {vm.canInviteInSpace && ( + + )} + + {vm.canAccessSpaceSettings && ( + + )} + + ); +} + interface ComposeMenuProps { /** * The view model for the room list header diff --git a/src/components/views/rooms/RoomListView/RoomListSearch.tsx b/src/components/views/rooms/RoomListView/RoomListSearch.tsx index 415e817ad9..6809841075 100644 --- a/src/components/views/rooms/RoomListView/RoomListSearch.tsx +++ b/src/components/views/rooms/RoomListView/RoomListSearch.tsx @@ -9,6 +9,7 @@ import React, { type JSX } from "react"; import { Button } from "@vector-im/compound-web"; import ExploreIcon from "@vector-im/compound-design-tokens/assets/web/icons/explore"; import SearchIcon from "@vector-im/compound-design-tokens/assets/web/icons/search"; +import DialPadIcon from "@vector-im/compound-design-tokens/assets/web/icons/dial-pad"; import { IS_MAC, Key } from "../../../../Keyboard"; import { _t } from "../../../../languageHandler"; @@ -20,6 +21,8 @@ import { Action } from "../../../../dispatcher/actions"; import PosthogTrackers from "../../../../PosthogTrackers"; import defaultDispatcher from "../../../../dispatcher/dispatcher"; import { Flex } from "../../../utils/Flex"; +import { useTypedEventEmitterState } from "../../../../hooks/useEventEmitter"; +import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../../../LegacyCallHandler"; type RoomListSearchProps = { /** @@ -35,6 +38,12 @@ type RoomListSearchProps = { */ export function RoomListSearch({ activeSpace }: RoomListSearchProps): JSX.Element { const displayExploreButton = activeSpace === MetaSpace.Home && shouldShowComponent(UIComponent.ExploreRooms); + // We only display the dial button if the user is can make PSTN calls + const displayDialButton = useTypedEventEmitterState( + LegacyCallHandler.instance, + LegacyCallHandlerEvent.ProtocolSupport, + () => LegacyCallHandler.instance.getSupportsPstnProtocol(), + ); return ( @@ -50,9 +59,22 @@ export function RoomListSearch({ activeSpace }: RoomListSearchProps): JSX.Elemen {IS_MAC ? "⌘ K" : _t(ALTERNATE_KEY_NAME[Key.CONTROL]) + " K"} + {displayDialButton && ( +
@@ -263,13 +263,13 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
@@ -563,13 +563,13 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
@@ -940,13 +940,13 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
@@ -1325,13 +1325,13 @@ exports[`RoomView should not display the timeline when the room encryption is lo
0
@@ -1497,7 +1497,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
0
@@ -1878,7 +1878,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
0
@@ -2025,7 +2025,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = ` class="mx_BaseCard_header_title" >

Chat @@ -2033,14 +2033,14 @@ exports[`RoomView video rooms should render joined video room view 1`] = `

@@ -41,7 +41,7 @@ exports[` renders sidebar correctly with beacons 1`] = ` class="mx_BeaconListItem" > renders sidebar correctly without beacons 1`] = ` xmlns="http://www.w3.org/2000/svg" >
diff --git a/test/unit-tests/components/views/dialogs/__snapshots__/ConfirmUserActionDialog-test.tsx.snap b/test/unit-tests/components/views/dialogs/__snapshots__/ConfirmUserActionDialog-test.tsx.snap index bf0fc31443..d48b62c57d 100644 --- a/test/unit-tests/components/views/dialogs/__snapshots__/ConfirmUserActionDialog-test.tsx.snap +++ b/test/unit-tests/components/views/dialogs/__snapshots__/ConfirmUserActionDialog-test.tsx.snap @@ -35,7 +35,7 @@ exports[`ConfirmUserActionDialog renders 1`] = ` class="mx_ConfirmUserActionDialog_avatar" > should list spaces which are not par
Make sure that you really want to remove all pinned messages. This action can’t be undone. @@ -32,7 +32,7 @@ exports[` should render 1`] = ` class="mx_UnpinAllDialog_buttons" >
@@ -161,7 +161,7 @@ exports[`AppTile for a pinned widget should render 1`] = ` xmlns="http://www.w3.org/2000/svg" >
@@ -182,7 +182,7 @@ exports[`AppTile for a pinned widget should render 1`] = ` xmlns="http://www.w3.org/2000/svg" >
@@ -213,7 +213,7 @@ exports[`AppTile for a pinned widget should render permission request 1`] = `
@@ -273,7 +273,7 @@ exports[`AppTile for a pinned widget should render permission request 1`] = ` xmlns="http://www.w3.org/2000/svg" >
@@ -294,7 +294,7 @@ exports[`AppTile for a pinned widget should render permission request 1`] = ` xmlns="http://www.w3.org/2000/svg" >
@@ -316,7 +316,7 @@ exports[`AppTile for a pinned widget should render permission request 1`] = `
@@ -404,7 +404,7 @@ exports[`AppTile preserves non-persisted widget on container move 1`] = `
@@ -464,7 +464,7 @@ exports[`AppTile preserves non-persisted widget on container move 1`] = ` xmlns="http://www.w3.org/2000/svg" >
@@ -485,7 +485,7 @@ exports[`AppTile preserves non-persisted widget on container move 1`] = ` xmlns="http://www.w3.org/2000/svg" >
diff --git a/test/unit-tests/components/views/elements/__snapshots__/FacePile-test.tsx.snap b/test/unit-tests/components/views/elements/__snapshots__/FacePile-test.tsx.snap index 2956bed066..2e923be862 100644 --- a/test/unit-tests/components/views/elements/__snapshots__/FacePile-test.tsx.snap +++ b/test/unit-tests/components/views/elements/__snapshots__/FacePile-test.tsx.snap @@ -9,10 +9,10 @@ exports[` renders with a tooltip 1`] = ` tabindex="0" >
renders dropdown options in menu 1`] = ` xmlns="http://www.w3.org/2000/svg" > should render the expected pill for @room 1`] = ` >
diff --git a/test/unit-tests/components/views/location/__snapshots__/MapError-test.tsx.snap b/test/unit-tests/components/views/location/__snapshots__/MapError-test.tsx.snap index 238097d995..f0c4749153 100644 --- a/test/unit-tests/components/views/location/__snapshots__/MapError-test.tsx.snap +++ b/test/unit-tests/components/views/location/__snapshots__/MapError-test.tsx.snap @@ -15,7 +15,7 @@ exports[` applies class when isMinimised is truthy 1`] = ` xmlns="http://www.w3.org/2000/svg" >

renders correctly for MapStyleUrlNotConfigured 1`] = ` xmlns="http://www.w3.org/2000/svg" >

renders correctly for MapStyleUrlNotReachable 1`] = ` xmlns="http://www.w3.org/2000/svg" >

renders with location icon when no room member 1`] = ` xmlns="http://www.w3.org/2000/svg" >

diff --git a/test/unit-tests/components/views/location/__snapshots__/SmartMarker-test.tsx.snap b/test/unit-tests/components/views/location/__snapshots__/SmartMarker-test.tsx.snap index f2b3e4cc8b..f1663bb751 100644 --- a/test/unit-tests/components/views/location/__snapshots__/SmartMarker-test.tsx.snap +++ b/test/unit-tests/components/views/location/__snapshots__/SmartMarker-test.tsx.snap @@ -18,7 +18,7 @@ exports[` creates a marker on mount 1`] = ` xmlns="http://www.w3.org/2000/svg" >
@@ -45,7 +45,7 @@ exports[` removes marker on unmount 1`] = ` xmlns="http://www.w3.org/2000/svg" >
diff --git a/test/unit-tests/components/views/location/__snapshots__/ZoomButtons-test.tsx.snap b/test/unit-tests/components/views/location/__snapshots__/ZoomButtons-test.tsx.snap index 4aa18b2f1e..02131a43d7 100644 --- a/test/unit-tests/components/views/location/__snapshots__/ZoomButtons-test.tsx.snap +++ b/test/unit-tests/components/views/location/__snapshots__/ZoomButtons-test.tsx.snap @@ -21,7 +21,7 @@ exports[` renders buttons 1`] = ` xmlns="http://www.w3.org/2000/svg" >
@@ -41,7 +41,7 @@ exports[` renders buttons 1`] = ` xmlns="http://www.w3.org/2000/svg" >
diff --git a/test/unit-tests/components/views/messages/TextualBody-test.tsx b/test/unit-tests/components/views/messages/TextualBody-test.tsx index a10e4bebe0..753534a93f 100644 --- a/test/unit-tests/components/views/messages/TextualBody-test.tsx +++ b/test/unit-tests/components/views/messages/TextualBody-test.tsx @@ -186,7 +186,7 @@ describe("", () => { const { container } = getComponent({ mxEvent: ev }); const content = container.querySelector(".mx_EventTile_body"); expect(content.innerHTML).toMatchInlineSnapshot( - `"Chat with Member"`, + `"Chat with Member"`, ); }); @@ -204,7 +204,7 @@ describe("", () => { const { container } = getComponent({ mxEvent: ev }); const content = container.querySelector(".mx_EventTile_body"); expect(content.innerHTML).toMatchInlineSnapshot( - `"Visit #room:example.com"`, + `"Visit #room:example.com"`, ); }); diff --git a/test/unit-tests/components/views/messages/__snapshots__/DecryptionFailureBody-test.tsx.snap b/test/unit-tests/components/views/messages/__snapshots__/DecryptionFailureBody-test.tsx.snap index a3b9fb205f..599a7719d5 100644 --- a/test/unit-tests/components/views/messages/__snapshots__/DecryptionFailureBody-test.tsx.snap +++ b/test/unit-tests/components/views/messages/__snapshots__/DecryptionFailureBody-test.tsx.snap @@ -35,7 +35,7 @@ exports[`DecryptionFailureBody should handle messages from users who change iden xmlns="http://www.w3.org/2000/svg" > Sender's verified identity has changed diff --git a/test/unit-tests/components/views/messages/__snapshots__/MBeaconBody-test.tsx.snap b/test/unit-tests/components/views/messages/__snapshots__/MBeaconBody-test.tsx.snap index de31628ec3..14007bd61a 100644 --- a/test/unit-tests/components/views/messages/__snapshots__/MBeaconBody-test.tsx.snap +++ b/test/unit-tests/components/views/messages/__snapshots__/MBeaconBody-test.tsx.snap @@ -14,7 +14,7 @@ exports[` when map display is not configured renders maps unavail xmlns="http://www.w3.org/2000/svg" >

should show a download button in file rendering type 1`] = class="mx_MFileBody_download" > should show a download button in file rendering type 1`] = xmlns="http://www.w3.org/2000/svg" > Download diff --git a/test/unit-tests/components/views/messages/__snapshots__/MLocationBody-test.tsx.snap b/test/unit-tests/components/views/messages/__snapshots__/MLocationBody-test.tsx.snap index 7b919b5326..48dc54241f 100644 --- a/test/unit-tests/components/views/messages/__snapshots__/MLocationBody-test.tsx.snap +++ b/test/unit-tests/components/views/messages/__snapshots__/MLocationBody-test.tsx.snap @@ -58,7 +58,7 @@ exports[`MLocationBody without error renders map correctly 1`] = xmlns="http://www.w3.org/2000/svg" > @@ -92,7 +92,7 @@ exports[`MLocationBody without error renders marker correctly fo class="mx_Marker_border" > Pinned message diff --git a/test/unit-tests/components/views/messages/__snapshots__/TextualBody-test.tsx.snap b/test/unit-tests/components/views/messages/__snapshots__/TextualBody-test.tsx.snap index 1ea245acf0..44672397ac 100644 --- a/test/unit-tests/components/views/messages/__snapshots__/TextualBody-test.tsx.snap +++ b/test/unit-tests/components/views/messages/__snapshots__/TextualBody-test.tsx.snap @@ -86,7 +86,7 @@ exports[` renders formatted m.text correctly pills appear for an Message from Member"`; +exports[` renders plain-text m.text correctly should pillify a permalink to a message in the same room with the label »Message from Member« 1`] = `"Visit Message from Member"`; -exports[` renders plain-text m.text correctly should pillify a permalink to an event in another room with the label »Message in Room 2« 1`] = `"Visit Message in Room 2"`; +exports[` renders plain-text m.text correctly should pillify a permalink to an event in another room with the label »Message in Room 2« 1`] = `"Visit Message in Room 2"`; exports[` renders plain-text m.text correctly should pillify a permalink to an unknown message in the same room with the label »Message« 1`] = `
renders plain-text m.text correctly should pillify a pe xmlns="http://www.w3.org/2000/svg" > Poll detail navigates back to poll list from detail vie xmlns="http://www.w3.org/2000/svg" > Active polls diff --git a/test/unit-tests/components/views/right_panel/__snapshots__/BaseCard-test.tsx.snap b/test/unit-tests/components/views/right_panel/__snapshots__/BaseCard-test.tsx.snap index 21ec64e9b3..dc85fba985 100644 --- a/test/unit-tests/components/views/right_panel/__snapshots__/BaseCard-test.tsx.snap +++ b/test/unit-tests/components/views/right_panel/__snapshots__/BaseCard-test.tsx.snap @@ -12,7 +12,7 @@ exports[` should close when clicking X button 1`] = ` class="mx_BaseCard_header_title" >

Heading text @@ -20,14 +20,14 @@ exports[` should close when clicking X button 1`] = `

message case AskToJoin renders the corresponding mes

message case AskToJoin renders the corresponding mes

with an invite with an invited email when client has

with an invite without an invited email for a dm roo

with an invite without an invited email for a non-dm

should render invite 1`] = ` class="mx_BaseCard_header_title" >

Profile @@ -20,14 +20,14 @@ exports[` should render invite 1`] = ` @@ -39,7 +39,7 @@ exports[`FormattingButtons renders in german 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -58,7 +58,7 @@ exports[`FormattingButtons renders in german 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -77,7 +77,7 @@ exports[`FormattingButtons renders in german 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -96,7 +96,7 @@ exports[`FormattingButtons renders in german 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -115,7 +115,7 @@ exports[`FormattingButtons renders in german 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -134,7 +134,7 @@ exports[`FormattingButtons renders in german 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -153,7 +153,7 @@ exports[`FormattingButtons renders in german 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -172,7 +172,7 @@ exports[`FormattingButtons renders in german 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -191,7 +191,7 @@ exports[`FormattingButtons renders in german 1`] = ` xmlns="http://www.w3.org/2000/svg" > diff --git a/test/unit-tests/components/views/settings/AddRemoveThreepids-test.tsx b/test/unit-tests/components/views/settings/AddRemoveThreepids-test.tsx index 5685f1eb8f..6d3d3de692 100644 --- a/test/unit-tests/components/views/settings/AddRemoveThreepids-test.tsx +++ b/test/unit-tests/components/views/settings/AddRemoveThreepids-test.tsx @@ -172,7 +172,7 @@ describe("AddRemoveThreepids", () => { const countryDropdown = await screen.findByRole("button", { name: /Country Dropdown/ }); await userEvent.click(countryDropdown); - const gbOption = screen.getByRole("option", { name: "🇬🇧 United Kingdom (+44)" }); + const gbOption = screen.getByText("United Kingdom (+44)"); await userEvent.click(gbOption); const input = screen.getByRole("textbox", { name: "Phone Number" }); @@ -511,7 +511,7 @@ describe("AddRemoveThreepids", () => { const countryDropdown = screen.getByRole("button", { name: /Country Dropdown/ }); await userEvent.click(countryDropdown); - const gbOption = screen.getByRole("option", { name: "🇬🇧 United Kingdom (+44)" }); + const gbOption = screen.getByText("United Kingdom (+44)"); await userEvent.click(gbOption); const input = screen.getByRole("textbox", { name: "Phone Number" }); diff --git a/test/unit-tests/components/views/settings/__snapshots__/LayoutSwitcher-test.tsx.snap b/test/unit-tests/components/views/settings/__snapshots__/LayoutSwitcher-test.tsx.snap index 2aa08adb94..e8ab3ccd2f 100644 --- a/test/unit-tests/components/views/settings/__snapshots__/LayoutSwitcher-test.tsx.snap +++ b/test/unit-tests/components/views/settings/__snapshots__/LayoutSwitcher-test.tsx.snap @@ -19,25 +19,25 @@ exports[` should render 1`] = ` class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi" >

  1. flow to change the recovery key should display th
  2. Change recovery key @@ -61,7 +61,7 @@ exports[` flow to change the recovery key should display th class="mx_EncryptionCard_header" >
    flow to change the recovery key should display th xmlns="http://www.w3.org/2000/svg" >

    Change recovery key?

    @@ -89,32 +89,32 @@ exports[` flow to change the recovery key should display th class="mx_KeyPanel" > Recovery key
    encoded private key Do not share this with anyone!
    1. flow to set up a recovery key should ask the user
    2. Set up recovery @@ -221,7 +221,7 @@ exports[` flow to set up a recovery key should ask the user class="mx_EncryptionCard_header" >
      flow to set up a recovery key should ask the user xmlns="http://www.w3.org/2000/svg" >

      Enter your recovery key to confirm

      @@ -246,19 +246,19 @@ exports[` flow to set up a recovery key should ask the user
After clicking continue, we’ll generate a recovery key for you. @@ -542,7 +542,7 @@ exports[` flow to set up a recovery key should display info class="mx_EncryptionCard_buttons" >
  1. flow to set up a recovery key should display the
  2. Set up recovery @@ -625,7 +625,7 @@ exports[` flow to set up a recovery key should display the class="mx_EncryptionCard_header" >
    flow to set up a recovery key should display the xmlns="http://www.w3.org/2000/svg" >

    Save your recovery key somewhere safe

    @@ -653,32 +653,32 @@ exports[` flow to set up a recovery key should display the class="mx_KeyPanel" > Recovery key
    encoded private key Do not share this with anyone!
  1. should display the 'forgot recovery key' variant
  2. Reset encryption @@ -61,7 +61,7 @@ exports[` should display the 'forgot recovery key' variant class="mx_EncryptionCard_header" >
    should display the 'forgot recovery key' variant xmlns="http://www.w3.org/2000/svg" >

    Forgot your recovery key? You’ll need to reset your identity.

    @@ -87,14 +87,14 @@ exports[` should display the 'forgot recovery key' variant style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: normal; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);" >