Merge branch 'develop' into t3chguy/fix/27078

This commit is contained in:
Michael Telatynski 2024-12-04 10:02:26 +00:00 committed by GitHub
commit 525cc6facd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
473 changed files with 6113 additions and 19397 deletions

View file

@ -10,24 +10,29 @@ inputs:
runs: runs:
using: composite using: composite
steps: steps:
- name: Download current version for its old bundles - name: Download release tarball
id: current_download
uses: robinraju/release-downloader@a96f54c1b5f5e09e47d9504526e96febd949d4c2 # v1 uses: robinraju/release-downloader@a96f54c1b5f5e09e47d9504526e96febd949d4c2 # v1
with: with:
tag: steps.current_version.outputs.version tag: ${{ inputs.tag }}
fileName: element-*.tar.gz* fileName: element-*.tar.gz*
out-file-path: ${{ runner.temp }}/download-verify-element-tarball out-file-path: ${{ runner.temp }}/download-verify-element-tarball
- name: Verify tarball - name: Verify tarball
shell: bash
run: gpg --verify element-*.tar.gz.asc element-*.tar.gz run: gpg --verify element-*.tar.gz.asc element-*.tar.gz
working-directory: ${{ runner.temp }}/download-verify-element-tarball working-directory: ${{ runner.temp }}/download-verify-element-tarball
- name: Extract tarball - name: Extract tarball
run: tar xvzf element-*.tar.gz -C webapp --strip-components=1 shell: bash
run: |
mkdir webapp
tar xvzf element-*.tar.gz -C webapp --strip-components=1
working-directory: ${{ runner.temp }}/download-verify-element-tarball working-directory: ${{ runner.temp }}/download-verify-element-tarball
- name: Move webapp to out-file-path - name: Move webapp to out-file-path
shell: bash
run: mv ${{ runner.temp }}/download-verify-element-tarball/webapp ${{ inputs.out-file-path }} run: mv ${{ runner.temp }}/download-verify-element-tarball/webapp ${{ inputs.out-file-path }}
- name: Clean up temp directory - name: Clean up temp directory
shell: bash
run: rm -R ${{ runner.temp }}/download-verify-element-tarball run: rm -R ${{ runner.temp }}/download-verify-element-tarball

View file

@ -1,7 +1,8 @@
# Manual deploy workflow for deploying to app.element.io & staging.element.io # Manual deploy workflow for deploying to app.element.io & staging.element.io
# Runs automatically for staging.element.io when an RC or Release is published # Runs automatically for staging.element.io when an RC or Release is published
# Note: Does *NOT* run automatically for app.element.io so that it gets tested on staging.element.io beforehand # Note: Does *NOT* run automatically for app.element.io so that it gets tested on staging.element.io beforehand
name: Build and Deploy ${{ inputs.site || 'staging.element.io' }} name: Deploy release
run-name: Deploy ${{ github.ref_name }} to ${{ inputs.site || 'staging.element.io' }}
on: on:
release: release:
types: [published] types: [published]
@ -28,37 +29,40 @@ jobs:
env: env:
SITE: ${{ inputs.site || 'staging.element.io' }} SITE: ${{ inputs.site || 'staging.element.io' }}
steps: steps:
- uses: actions/checkout@v4
- name: Load GPG key - name: Load GPG key
run: | run: |
curl https://packages.element.io/element-release-key.gpg | gpg --import curl https://packages.element.io/element-release-key.gpg | gpg --import
gpg -k "$GPG_FINGERPRINT" gpg -k "$GPG_FINGERPRINT"
env: env:
GPG_FINGERPRINT: ${{ secrets.GPG_FINGERPRINT }} GPG_FINGERPRINT: ${{ vars.GPG_FINGERPRINT }}
- name: Check current version on deployment - name: Check current version on deployment
id: current_version id: current_version
run: | run: |
echo "version=$(curl -s https://$SITE/version)" >> $GITHUB_OUTPUT version=$(curl -s https://$SITE/version)
echo "version=${version#v}" >> $GITHUB_OUTPUT
# The current version bundle melding dance is skipped if the version we're deploying is the same # The current version bundle melding dance is skipped if the version we're deploying is the same
# as then we're just doing a re-deploy of the same version with potentially different configs. # as then we're just doing a re-deploy of the same version with potentially different configs.
- name: Download current version for its old bundles - name: Download current version for its old bundles
id: current_download id: current_download
if: steps.current_version.outputs.version != github.ref_name if: steps.current_version.outputs.version != github.ref_name
uses: element-hq/element-web/.github/actions/download-verify-element-tarball@${{ github.ref_name }} uses: ./.github/actions/download-verify-element-tarball
with: with:
tag: steps.current_version.outputs.version tag: v${{ steps.current_version.outputs.version }}
out-file-path: current_version out-file-path: _current_version
- name: Download target version - name: Download target version
uses: element-hq/element-web/.github/actions/download-verify-element-tarball@${{ github.ref_name }} uses: ./.github/actions/download-verify-element-tarball
with: with:
tag: ${{ github.ref_name }} tag: ${{ github.ref_name }}
out-file-path: _deploy out-file-path: _deploy
- name: Merge current bundles into target - name: Merge current bundles into target
if: steps.current_download.outcome == 'success' if: steps.current_download.outcome == 'success'
run: cp -vnpr current_version/bundles/* _deploy/bundles/ run: cp -vnpr _current_version/bundles/* _deploy/bundles/
- name: Copy config - name: Copy config
run: cp element.io/app/config.json _deploy/config.json run: cp element.io/app/config.json _deploy/config.json
@ -73,7 +77,7 @@ jobs:
uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork
with: with:
ref: ${{ github.sha }} ref: ${{ github.sha }}
running-workflow-name: "Build and Deploy ${{ env.SITE }}" running-workflow-name: "Deploy to Cloudflare Pages"
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
wait-interval: 10 wait-interval: 10
check-regexp: ^((?!SonarCloud|SonarQube|issue|board|label|Release|prepare|GitHub Pages).)*$ check-regexp: ^((?!SonarCloud|SonarQube|issue|board|label|Release|prepare|GitHub Pages).)*$

View file

@ -39,7 +39,7 @@ jobs:
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5 uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5
with: with:
images: | images: |
vectorim/element-web vectorim/element-web
@ -51,7 +51,7 @@ jobs:
- name: Build and push - name: Build and push
id: build-and-push id: build-and-push
uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6 uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6
with: with:
context: . context: .
push: true push: true

View file

@ -9,6 +9,6 @@ jobs:
action: action:
uses: matrix-org/matrix-js-sdk/.github/workflows/pull_request.yaml@develop uses: matrix-org/matrix-js-sdk/.github/workflows/pull_request.yaml@develop
permissions: permissions:
pull-requests: read pull-requests: write
secrets: secrets:
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}

View file

@ -18,6 +18,7 @@ jobs:
permissions: permissions:
contents: write contents: write
issues: write issues: write
pull-requests: read
secrets: secrets:
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}

View file

@ -19,8 +19,23 @@ on:
default: true default: true
permissions: {} # Uses ELEMENT_BOT_TOKEN instead permissions: {} # Uses ELEMENT_BOT_TOKEN instead
jobs: jobs:
checks:
name: Sanity checks
strategy:
matrix:
repo:
- matrix-org/matrix-js-sdk
- element-hq/element-web
- element-hq/element-desktop
uses: matrix-org/matrix-js-sdk/.github/workflows/release-checks.yml@develop
secrets:
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
with:
repository: ${{ matrix.repo }}
prepare: prepare:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: checks
env: env:
# The order is specified bottom-up to avoid any races for allchange # The order is specified bottom-up to avoid any races for allchange
REPOS: matrix-js-sdk element-web element-desktop REPOS: matrix-js-sdk element-web element-desktop

View file

@ -104,7 +104,7 @@ jobs:
- name: Skip SonarCloud in merge queue - name: Skip SonarCloud in merge queue
if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true' if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true'
uses: guibranco/github-status-action-v2@1f26a0237cd1a57626fbb5a0eb2494c9b8797d07 uses: guibranco/github-status-action-v2@66088c44e212a906c32a047529a213d81809ec1c
with: with:
authToken: ${{ secrets.GITHUB_TOKEN }} authToken: ${{ secrets.GITHUB_TOKEN }}
state: success state: success

View file

@ -1,3 +1,33 @@
Changes in [1.11.87](https://github.com/element-hq/element-web/releases/tag/v1.11.87) (2024-12-03)
==================================================================================================
## ✨ Features
* Send and respect MSC4230 is\_animated flag ([#28513](https://github.com/element-hq/element-web/pull/28513)). Contributed by @t3chguy.
* Display a warning when an unverified user's identity changes ([#28211](https://github.com/element-hq/element-web/pull/28211)). Contributed by @uhoreg.
* Swap out Twitter link for Mastodon on auth footer ([#28508](https://github.com/element-hq/element-web/pull/28508)). Contributed by @t3chguy.
* Consider `org.matrix.msc3417.call` as video room in create room dialog ([#28497](https://github.com/element-hq/element-web/pull/28497)). Contributed by @t3chguy.
* Standardise icons using Compound Design Tokens ([#28217](https://github.com/element-hq/element-web/pull/28217)). Contributed by @t3chguy.
* Start sending stable `m.marked_unread` events ([#28478](https://github.com/element-hq/element-web/pull/28478)). Contributed by @tulir.
* Upgrade to compound-design-tokens v2 ([#28471](https://github.com/element-hq/element-web/pull/28471)). Contributed by @t3chguy.
* Standardise icons using Compound Design Tokens ([#28286](https://github.com/element-hq/element-web/pull/28286)). Contributed by @t3chguy.
* Remove reply fallbacks as per merged MSC2781 ([#28406](https://github.com/element-hq/element-web/pull/28406)). Contributed by @t3chguy.
* Use React Suspense when rendering async modals ([#28386](https://github.com/element-hq/element-web/pull/28386)). Contributed by @t3chguy.
## 🐛 Bug Fixes
* Add spinner when room encryption is loading in room settings ([#28535](https://github.com/element-hq/element-web/pull/28535)). Contributed by @florianduros.
* Fix getOidcCallbackUrl for Element Desktop ([#28521](https://github.com/element-hq/element-web/pull/28521)). Contributed by @t3chguy.
* Filter out redacted poll votes to avoid crashing the Poll widget ([#28498](https://github.com/element-hq/element-web/pull/28498)). Contributed by @t3chguy.
* Fix force tab complete not working since switching to React 18 createRoot API ([#28505](https://github.com/element-hq/element-web/pull/28505)). Contributed by @t3chguy.
* Fix media captions in bubble layout ([#28480](https://github.com/element-hq/element-web/pull/28480)). Contributed by @tulir.
* Reset cross-signing before backup when resetting both ([#28402](https://github.com/element-hq/element-web/pull/28402)). Contributed by @uhoreg.
* Listen to events so that encryption icon updates when status changes ([#28407](https://github.com/element-hq/element-web/pull/28407)). Contributed by @uhoreg.
* Check that the file the user chose has a MIME type of `image/*` ([#28467](https://github.com/element-hq/element-web/pull/28467)). Contributed by @t3chguy.
* Fix download button size in message action bar ([#28472](https://github.com/element-hq/element-web/pull/28472)). Contributed by @t3chguy.
* Allow tab completing users in brackets ([#28460](https://github.com/element-hq/element-web/pull/28460)). Contributed by @t3chguy.
* Fix React 18 strict mode breaking spotlight dialog ([#28452](https://github.com/element-hq/element-web/pull/28452)). Contributed by @MidhunSureshR.
Changes in [1.11.86](https://github.com/element-hq/element-web/releases/tag/v1.11.86) (2024-11-19) Changes in [1.11.86](https://github.com/element-hq/element-web/releases/tag/v1.11.86) (2024-11-19)
================================================================================================== ==================================================================================================
## ✨ Features ## ✨ Features

View file

@ -1,6 +0,0 @@
// Stub out FontManager for tests as it doesn't validate anything we don't already know given
// our fixed test environment and it requires the installation of node-canvas.
module.exports = {
fixupColorFonts: () => Promise.resolve(),
};

View file

@ -592,4 +592,3 @@ The following are undocumented or intended for developer use only.
2. `sync_timeline_limit` 2. `sync_timeline_limit`
3. `dangerously_allow_unsafe_and_insecure_passwords` 3. `dangerously_allow_unsafe_and_insecure_passwords`
4. `latex_maths_delims`: An optional setting to override the default delimiters used for maths parsing. See https://github.com/matrix-org/matrix-react-sdk/pull/5939 for details. Only used when `feature_latex_maths` is enabled. 4. `latex_maths_delims`: An optional setting to override the default delimiters used for maths parsing. See https://github.com/matrix-org/matrix-react-sdk/pull/5939 for details. Only used when `feature_latex_maths` is enabled.
5. `voice_broadcast.chunk_length`: Target chunk length in seconds for the Voice Broadcast feature currently under development.

View file

@ -32,7 +32,6 @@ const config: Config = {
"decoderWorker\\.min\\.wasm": "<rootDir>/__mocks__/empty.js", "decoderWorker\\.min\\.wasm": "<rootDir>/__mocks__/empty.js",
"waveWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js", "waveWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js",
"context-filter-polyfill": "<rootDir>/__mocks__/empty.js", "context-filter-polyfill": "<rootDir>/__mocks__/empty.js",
"FontManager.ts": "<rootDir>/__mocks__/FontManager.js",
"workers/(.+)Factory": "<rootDir>/__mocks__/workerFactoryMock.js", "workers/(.+)Factory": "<rootDir>/__mocks__/workerFactoryMock.js",
"^!!raw-loader!.*": "jest-raw-loader", "^!!raw-loader!.*": "jest-raw-loader",
"recorderWorkletFactory": "<rootDir>/__mocks__/empty.js", "recorderWorkletFactory": "<rootDir>/__mocks__/empty.js",

View file

@ -1,6 +1,6 @@
{ {
"name": "element-web", "name": "element-web",
"version": "1.11.86", "version": "1.11.87",
"description": "A feature-rich client for Matrix.org", "description": "A feature-rich client for Matrix.org",
"author": "New Vector Ltd.", "author": "New Vector Ltd.",
"repository": { "repository": {
@ -64,7 +64,7 @@
"test:playwright:open": "yarn test:playwright --ui", "test:playwright:open": "yarn test:playwright --ui",
"test:playwright:screenshots": "yarn test:playwright:screenshots:build && yarn test:playwright:screenshots:run", "test:playwright:screenshots": "yarn test:playwright:screenshots:build && yarn test:playwright:screenshots:run",
"test:playwright:screenshots:build": "docker build playwright -t element-web-playwright", "test:playwright:screenshots:build": "docker build playwright -t element-web-playwright",
"test:playwright:screenshots:run": "docker run --rm --network host -e BASE_URL -e CI -v $(pwd):/work/ -v $(node -e 'console.log(require(`path`).dirname(require.resolve(`matrix-js-sdk/package.json`)))'):/work/node_modules/matrix-js-sdk -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/:/tmp/ -it element-web-playwright", "test:playwright:screenshots:run": "docker run --rm --network host -e BASE_URL -e CI -v $(pwd):/work/ -v $(node -e 'console.log(require(`path`).dirname(require.resolve(`matrix-js-sdk/package.json`)))'):/work/node_modules/matrix-js-sdk -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/:/tmp/ -it element-web-playwright --grep @screenshot",
"coverage": "yarn test --coverage", "coverage": "yarn test --coverage",
"analyse:unused-exports": "ts-node ./scripts/analyse_unused_exports.ts", "analyse:unused-exports": "ts-node ./scripts/analyse_unused_exports.ts",
"analyse:webpack-bundles": "webpack-bundle-analyzer webpack-stats.json webapp", "analyse:webpack-bundles": "webpack-bundle-analyzer webpack-stats.json webapp",
@ -73,7 +73,7 @@
"resolutions": { "resolutions": {
"oidc-client-ts": "3.1.0", "oidc-client-ts": "3.1.0",
"jwt-decode": "4.0.0", "jwt-decode": "4.0.0",
"caniuse-lite": "1.0.30001679", "caniuse-lite": "1.0.30001684",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
"wrap-ansi": "npm:wrap-ansi@^7.0.0" "wrap-ansi": "npm:wrap-ansi@^7.0.0"
}, },
@ -114,10 +114,10 @@
"jsrsasign": "^11.0.0", "jsrsasign": "^11.0.0",
"jszip": "^3.7.0", "jszip": "^3.7.0",
"katex": "^0.16.0", "katex": "^0.16.0",
"linkify-element": "4.1.3", "linkify-element": "4.1.4",
"linkify-react": "4.1.3", "linkify-react": "4.1.4",
"linkify-string": "4.1.3", "linkify-string": "4.1.4",
"linkifyjs": "4.1.3", "linkifyjs": "4.1.4",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"maplibre-gl": "^4.0.0", "maplibre-gl": "^4.0.0",
"matrix-encrypt-attachment": "^1.0.3", "matrix-encrypt-attachment": "^1.0.3",
@ -268,11 +268,12 @@
"postcss-preset-env": "^10.0.0", "postcss-preset-env": "^10.0.0",
"postcss-scss": "^4.0.4", "postcss-scss": "^4.0.4",
"postcss-simple-vars": "^7.0.1", "postcss-simple-vars": "^7.0.1",
"prettier": "3.3.3", "prettier": "3.4.1",
"process": "^0.11.10", "process": "^0.11.10",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",
"rimraf": "^6.0.0", "rimraf": "^6.0.0",
"semver": "^7.5.2", "semver": "^7.5.2",
"source-map-loader": "^5.0.0",
"stylelint": "^16.1.0", "stylelint": "^16.1.0",
"stylelint-config-standard": "^36.0.0", "stylelint-config-standard": "^36.0.0",
"stylelint-scss": "^6.0.0", "stylelint-scss": "^6.0.0",

View file

@ -1,4 +1,4 @@
FROM mcr.microsoft.com/playwright:v1.48.2-jammy FROM mcr.microsoft.com/playwright:v1.49.0-jammy
WORKDIR /work WORKDIR /work

View file

@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
import { test, expect } from "../../element-web-test"; import { test, expect } from "../../element-web-test";
test(`shows error page if browser lacks Intl support`, async ({ page }) => { test(`shows error page if browser lacks Intl support`, { tag: "@screenshot" }, async ({ page }) => {
await page.addInitScript({ content: `delete window.Intl;` }); await page.addInitScript({ content: `delete window.Intl;` });
await page.goto("/"); await page.goto("/");
@ -21,7 +21,7 @@ test(`shows error page if browser lacks Intl support`, async ({ page }) => {
await expect(page).toMatchScreenshot("unsupported-browser.png"); await expect(page).toMatchScreenshot("unsupported-browser.png");
}); });
test(`shows error page if browser lacks WebAssembly support`, async ({ page }) => { test(`shows error page if browser lacks WebAssembly support`, { tag: "@screenshot" }, async ({ page }) => {
await page.addInitScript({ content: `delete window.WebAssembly;` }); await page.addInitScript({ content: `delete window.WebAssembly;` });
await page.goto("/"); await page.goto("/");

View file

@ -134,18 +134,22 @@ test.describe("Audio player", () => {
).toBeVisible(); ).toBeVisible();
}); });
test("should be correctly rendered - light theme", async ({ page, app }) => { test("should be correctly rendered - light theme", { tag: "@screenshot" }, async ({ page, app }) => {
await uploadFile(page, "playwright/sample-files/1sec-long-name-audio-file.ogg"); await uploadFile(page, "playwright/sample-files/1sec-long-name-audio-file.ogg");
await takeSnapshots(page, app, "Selected EventTile of audio player (light theme)"); await takeSnapshots(page, app, "Selected EventTile of audio player (light theme)");
}); });
test("should be correctly rendered - light theme with monospace font", async ({ page, app }) => { test(
"should be correctly rendered - light theme with monospace font",
{ tag: "@screenshot" },
async ({ page, app }) => {
await uploadFile(page, "playwright/sample-files/1sec-long-name-audio-file.ogg"); await uploadFile(page, "playwright/sample-files/1sec-long-name-audio-file.ogg");
await takeSnapshots(page, app, "Selected EventTile of audio player (light theme, monospace font)", true); // Enable monospace await takeSnapshots(page, app, "Selected EventTile of audio player (light theme, monospace font)", true); // Enable monospace
}); },
);
test("should be correctly rendered - high contrast theme", async ({ page, app }) => { test("should be correctly rendered - high contrast theme", { tag: "@screenshot" }, async ({ page, app }) => {
// Disable system theme in case ThemeWatcher enables the theme automatically, // Disable system theme in case ThemeWatcher enables the theme automatically,
// so that the high contrast theme can be enabled // so that the high contrast theme can be enabled
await app.settings.setValue("use_system_theme", null, SettingLevel.DEVICE, false); await app.settings.setValue("use_system_theme", null, SettingLevel.DEVICE, false);
@ -161,7 +165,7 @@ test.describe("Audio player", () => {
await takeSnapshots(page, app, "Selected EventTile of audio player (high contrast)"); await takeSnapshots(page, app, "Selected EventTile of audio player (high contrast)");
}); });
test("should be correctly rendered - dark theme", async ({ page, app }) => { test("should be correctly rendered - dark theme", { tag: "@screenshot" }, async ({ page, app }) => {
// Enable dark theme // Enable dark theme
await app.settings.setValue("theme", null, SettingLevel.ACCOUNT, "dark"); await app.settings.setValue("theme", null, SettingLevel.ACCOUNT, "dark");
@ -207,7 +211,10 @@ test.describe("Audio player", () => {
expect(download.suggestedFilename()).toBe("1sec.ogg"); expect(download.suggestedFilename()).toBe("1sec.ogg");
}); });
test("should support replying to audio file with another audio file", async ({ page, app }) => { test(
"should support replying to audio file with another audio file",
{ tag: "@screenshot" },
async ({ page, app }) => {
await uploadFile(page, "playwright/sample-files/1sec.ogg"); await uploadFile(page, "playwright/sample-files/1sec.ogg");
// Assert the audio player is rendered // Assert the audio player is rendered
@ -230,9 +237,13 @@ test.describe("Audio player", () => {
await expect(button.locator(".mx_MFileBody_info_filename")).toBeVisible(); await expect(button.locator(".mx_MFileBody_info_filename")).toBeVisible();
await takeSnapshots(page, app, "Selected EventTile of audio player with a reply"); await takeSnapshots(page, app, "Selected EventTile of audio player with a reply");
}); },
);
test("should support creating a reply chain with multiple audio files", async ({ page, app, user }) => { test(
"should support creating a reply chain with multiple audio files",
{ tag: "@screenshot" },
async ({ page, app, user }) => {
// Note: "mx_ReplyChain" element is used not only for replies which // Note: "mx_ReplyChain" element is used not only for replies which
// create a reply chain, but also for a single reply without a replied // create a reply chain, but also for a single reply without a replied
// message. This test checks whether a reply chain which consists of // message. This test checks whether a reply chain which consists of
@ -293,7 +304,8 @@ test.describe("Audio player", () => {
// Take snapshots // Take snapshots
await takeSnapshots(page, app, "Selected EventTile of audio player with a reply chain"); await takeSnapshots(page, app, "Selected EventTile of audio player with a reply chain");
}); },
);
test("should be rendered, play, and support replying on a thread", async ({ page, app }) => { test("should be rendered, play, and support replying on a thread", async ({ page, app }) => {
await uploadFile(page, "playwright/sample-files/1sec-long-name-audio-file.ogg"); await uploadFile(page, "playwright/sample-files/1sec-long-name-audio-file.ogg");

View file

@ -89,7 +89,10 @@ test.describe("HTML Export", () => {
}, },
}); });
test("should export html successfully and match screenshot", async ({ page, app, room }) => { test(
"should export html successfully and match screenshot",
{ tag: "@screenshot" },
async ({ page, app, room }) => {
// Set a fixed time rather than masking off the line with the time in it: we don't need to worry // Set a fixed time rather than masking off the line with the time in it: we don't need to worry
// about the width changing and we can actually test this line looks correct. // about the width changing and we can actually test this line looks correct.
page.clock.setSystemTime(new Date("2024-01-01T00:00:00Z")); page.clock.setSystemTime(new Date("2024-01-01T00:00:00Z"));
@ -127,5 +130,6 @@ test.describe("HTML Export", () => {
page.locator(".mx_MessageTimestamp"), page.locator(".mx_MessageTimestamp"),
], ],
}); });
}); },
);
}); });

View file

@ -204,12 +204,10 @@ test.describe("Cryptography", function () {
await expect(page.locator(".mx_Dialog")).toHaveCount(1); await expect(page.locator(".mx_Dialog")).toHaveCount(1);
}); });
test("creating a DM should work, being e2e-encrypted / user verification", async ({ test(
page, "creating a DM should work, being e2e-encrypted / user verification",
app, { tag: "@screenshot" },
bot: bob, async ({ page, app, bot: bob, user: aliceCredentials }) => {
user: aliceCredentials,
}) => {
await app.client.bootstrapCrossSigning(aliceCredentials); await app.client.bootstrapCrossSigning(aliceCredentials);
await startDMWithBob(page, bob); await startDMWithBob(page, bob);
// send first message // send first message
@ -227,7 +225,8 @@ test.describe("Cryptography", function () {
// Take a snapshot of RoomSummaryCard with a verified E2EE icon // Take a snapshot of RoomSummaryCard with a verified E2EE icon
await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("RoomSummaryCard-with-verified-e2ee.png"); await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("RoomSummaryCard-with-verified-e2ee.png");
}); },
);
test("should allow verification when there is no existing DM", async ({ test("should allow verification when there is no existing DM", async ({
page, page,

View file

@ -67,6 +67,9 @@ test.describe("Cryptography", function () {
await page.locator(".mx_AuthPage").getByRole("button", { name: "I'll verify later" }).click(); await page.locator(".mx_AuthPage").getByRole("button", { name: "I'll verify later" }).click();
await app.viewRoomByName("Test room"); await app.viewRoomByName("Test room");
// In this case, the call to cryptoApi.isEncryptionEnabledInRoom is taking a long time to resolve
await page.waitForTimeout(1000);
// There should be two historical events in the timeline // There should be two historical events in the timeline
const tiles = await page.locator(".mx_EventTile").all(); const tiles = await page.locator(".mx_EventTile").all();
expect(tiles.length).toBeGreaterThanOrEqual(2); expect(tiles.length).toBeGreaterThanOrEqual(2);

View file

@ -16,6 +16,7 @@ import {
logOutOfElement, logOutOfElement,
verify, verify,
} from "./utils"; } from "./utils";
import { bootstrapCrossSigningForClient } from "../../pages/client.ts";
test.describe("Cryptography", function () { test.describe("Cryptography", function () {
test.use({ test.use({
@ -307,5 +308,30 @@ test.describe("Cryptography", function () {
const penultimate = page.locator(".mx_EventTile").filter({ hasText: "test encrypted from verified" }); const penultimate = page.locator(".mx_EventTile").filter({ hasText: "test encrypted from verified" });
await expect(penultimate.locator(".mx_EventTile_e2eIcon")).not.toBeVisible(); await expect(penultimate.locator(".mx_EventTile_e2eIcon")).not.toBeVisible();
}); });
test("should show correct shields on events sent by users with changed identity", async ({
page,
app,
bot: bob,
homeserver,
}) => {
// Verify Bob
await verify(app, bob);
// Bob logs in a new device and resets cross-signing
const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob);
await bootstrapCrossSigningForClient(await bobSecondDevice.prepareClient(), bob.credentials, true);
/* should show an error for a message from a previously verified device */
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from user that was previously verified");
const last = page.locator(".mx_EventTile_last");
await expect(last).toContainText("test encrypted from user that was previously verified");
const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon");
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
await lastE2eIcon.focus();
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
"Sender's verified identity has changed",
);
});
}); });
}); });

View file

@ -66,7 +66,10 @@ test.describe("Editing", () => {
botCreateOpts: { displayName: "Bob" }, botCreateOpts: { displayName: "Bob" },
}); });
test("should render and interact with the message edit history dialog", async ({ page, user, app, room }) => { test(
"should render and interact with the message edit history dialog",
{ tag: "@screenshot" },
async ({ page, user, app, room }) => {
// Click the "Remove" button on the message edit history dialog // Click the "Remove" button on the message edit history dialog
const clickButtonRemove = async (locator: Locator) => { const clickButtonRemove = async (locator: Locator) => {
const eventTileLine = locator.locator(".mx_EventTile_line"); const eventTileLine = locator.locator(".mx_EventTile_line");
@ -185,7 +188,8 @@ test.describe("Editing", () => {
.locator(".mx_RoomView_MessageList") .locator(".mx_RoomView_MessageList")
.locator(".mx_EventTile_last .mx_RedactedBody", { hasText: "Message deleted" }), .locator(".mx_EventTile_last .mx_RedactedBody", { hasText: "Message deleted" }),
).toBeVisible(); ).toBeVisible();
}); },
);
test("should render 'View Source' button in developer mode on the message edit history dialog", async ({ test("should render 'View Source' button in developer mode on the message edit history dialog", async ({
page, page,

View file

@ -25,7 +25,7 @@ test.describe("Image Upload", () => {
).toBeVisible(); ).toBeVisible();
}); });
test("should show image preview when uploading an image", async ({ page, app }) => { test("should show image preview when uploading an image", { tag: "@screenshot" }, async ({ page, app }) => {
await page await page
.locator(".mx_MessageComposer_actions input[type='file']") .locator(".mx_MessageComposer_actions input[type='file']")
.setInputFiles("playwright/sample-files/riot.png"); .setInputFiles("playwright/sample-files/riot.png");

View file

@ -26,7 +26,7 @@ test.describe("Forgot Password", () => {
}), }),
}); });
test("renders properly", async ({ page, homeserver }) => { test("renders properly", { tag: "@screenshot" }, async ({ page, homeserver }) => {
await page.goto("/"); await page.goto("/");
await page.getByRole("link", { name: "Sign in" }).click(); await page.getByRole("link", { name: "Sign in" }).click();
@ -39,7 +39,7 @@ test.describe("Forgot Password", () => {
await expect(page.getByRole("main")).toMatchScreenshot("forgot-password.png"); await expect(page.getByRole("main")).toMatchScreenshot("forgot-password.png");
}); });
test("renders email verification dialog properly", async ({ page, homeserver }) => { test("renders email verification dialog properly", { tag: "@screenshot" }, async ({ page, homeserver }) => {
const user = await homeserver.registerUser(username, password); const user = await homeserver.registerUser(username, password);
await homeserver.setThreepid(user.userId, "email", email); await homeserver.setThreepid(user.userId, "email", email);

View file

@ -19,7 +19,7 @@ test.describe("Invite dialog", function () {
const botName = "BotAlice"; const botName = "BotAlice";
test("should support inviting a user to a room", async ({ page, app, user, bot }) => { test("should support inviting a user to a room", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
// Create and view a room // Create and view a room
await app.client.createRoom({ name: "Test Room" }); await app.client.createRoom({ name: "Test Room" });
await app.viewRoomByName("Test Room"); await app.viewRoomByName("Test Room");
@ -73,12 +73,17 @@ test.describe("Invite dialog", function () {
await expect(page.getByText(`${botName} joined the room`)).toBeVisible(); await expect(page.getByText(`${botName} joined the room`)).toBeVisible();
}); });
test("should support inviting a user to Direct Messages", async ({ page, app, user, bot }) => { test(
"should support inviting a user to Direct Messages",
{ tag: "@screenshot" },
async ({ page, app, user, bot }) => {
await page.locator(".mx_RoomList").getByRole("button", { name: "Start chat" }).click(); await page.locator(".mx_RoomList").getByRole("button", { name: "Start chat" }).click();
const other = page.locator(".mx_InviteDialog_other"); const other = page.locator(".mx_InviteDialog_other");
// Assert that the header is rendered // Assert that the header is rendered
await expect(other.locator(".mx_Dialog_header .mx_Dialog_title").getByText("Direct Messages")).toBeVisible(); await expect(
other.locator(".mx_Dialog_header .mx_Dialog_title").getByText("Direct Messages"),
).toBeVisible();
// Assert that the bar is rendered // Assert that the bar is rendered
await expect(other.locator(".mx_InviteDialog_addressBar")).toBeVisible(); await expect(other.locator(".mx_InviteDialog_addressBar")).toBeVisible();
@ -88,7 +93,9 @@ test.describe("Invite dialog", function () {
await other.getByTestId("invite-dialog-input").fill(bot.credentials.userId); await other.getByTestId("invite-dialog-input").fill(bot.credentials.userId);
await expect(other.locator(".mx_InviteDialog_tile_nameStack").getByText(bot.credentials.userId)).toBeVisible(); await expect(
other.locator(".mx_InviteDialog_tile_nameStack").getByText(bot.credentials.userId),
).toBeVisible();
await other.locator(".mx_InviteDialog_tile_nameStack").getByText(botName).click(); await other.locator(".mx_InviteDialog_tile_nameStack").getByText(botName).click();
await expect( await expect(
@ -108,7 +115,10 @@ test.describe("Invite dialog", function () {
// TODO: implement the test on room-header.spec.ts // TODO: implement the test on room-header.spec.ts
const roomHeader = page.locator(".mx_RoomHeader"); const roomHeader = page.locator(".mx_RoomHeader");
await roomHeader.locator(".mx_RoomHeader_heading").hover(); await roomHeader.locator(".mx_RoomHeader_heading").hover();
await expect(roomHeader.locator(".mx_RoomHeader_heading")).toHaveCSS("background-color", "rgba(0, 0, 0, 0)"); await expect(roomHeader.locator(".mx_RoomHeader_heading")).toHaveCSS(
"background-color",
"rgba(0, 0, 0, 0)",
);
// Send a message to invite the bots // Send a message to invite the bots
const composer = app.getComposer().locator("[contenteditable]"); const composer = app.getComposer().locator("[contenteditable]");
@ -120,5 +130,6 @@ test.describe("Invite dialog", function () {
// Assert that the message is displayed at the bottom // Assert that the message is displayed at the bottom
await expect(page.locator(".mx_EventTile_last").getByText("Hello")).toBeVisible(); await expect(page.locator(".mx_EventTile_last").getByText("Hello")).toBeVisible();
}); },
);
}); });

View file

@ -63,7 +63,7 @@ test.describe("Message rendering", () => {
{ direction: "ltr", displayName: "Quentin" }, { direction: "ltr", displayName: "Quentin" },
{ direction: "rtl", displayName: "كوينتين" }, { direction: "rtl", displayName: "كوينتين" },
].forEach(({ direction, displayName }) => { ].forEach(({ direction, displayName }) => {
test.describe(`with ${direction} display name`, () => { test.describe(`with ${direction} display name`, { tag: "@screenshot" }, () => {
test.use({ test.use({
displayName, displayName,
room: async ({ user, app }, use) => { room: async ({ user, app }, use) => {
@ -72,14 +72,18 @@ test.describe("Message rendering", () => {
}, },
}); });
test("should render a basic LTR text message", async ({ page, user, app, room }) => { test(
"should render a basic LTR text message",
{ tag: "@screenshot" },
async ({ page, user, app, room }) => {
await page.goto(`#/room/${room.roomId}`); await page.goto(`#/room/${room.roomId}`);
const msgTile = await sendMessage(page, "Hello, world!"); const msgTile = await sendMessage(page, "Hello, world!");
await expect(msgTile).toMatchScreenshot(`basic-message-ltr-${direction}displayname.png`, { await expect(msgTile).toMatchScreenshot(`basic-message-ltr-${direction}displayname.png`, {
mask: [page.locator(".mx_MessageTimestamp")], mask: [page.locator(".mx_MessageTimestamp")],
}); });
}); },
);
test("should render an LTR emote", async ({ page, user, app, room }) => { test("should render an LTR emote", async ({ page, user, app, room }) => {
await page.goto(`#/room/${room.roomId}`); await page.goto(`#/room/${room.roomId}`);

View file

@ -24,7 +24,7 @@ test.describe("permalinks", () => {
displayName: "Alice", displayName: "Alice",
}); });
test("shoud render permalinks as expected", async ({ page, app, user, homeserver }) => { test("shoud render permalinks as expected", { tag: "@screenshot" }, async ({ page, app, user, homeserver }) => {
const bob = new Bot(page, homeserver, { displayName: "Bob" }); const bob = new Bot(page, homeserver, { displayName: "Bob" });
const charlotte = new Bot(page, homeserver, { displayName: "Charlotte" }); const charlotte = new Bot(page, homeserver, { displayName: "Charlotte" });
await bob.prepareClient(); await bob.prepareClient();

View file

@ -10,20 +10,22 @@ import { test } from "./index";
import { expect } from "../../element-web-test"; import { expect } from "../../element-web-test";
test.describe("Pinned messages", () => { test.describe("Pinned messages", () => {
test("should show the empty state when there are no pinned messages", async ({ page, app, room1, util }) => { test(
"should show the empty state when there are no pinned messages",
{ tag: "@screenshot" },
async ({ page, app, room1, util }) => {
await util.goTo(room1); await util.goTo(room1);
await util.openRoomInfo(); await util.openRoomInfo();
await util.assertPinnedCountInRoomInfo(0); await util.assertPinnedCountInRoomInfo(0);
await util.openPinnedMessagesList(); await util.openPinnedMessagesList();
await util.assertEmptyPinnedMessagesList(); await util.assertEmptyPinnedMessagesList();
}); },
);
test("should pin one message and to have the pinned message badge in the timeline", async ({ test(
page, "should pin one message and to have the pinned message badge in the timeline",
app, { tag: "@screenshot" },
room1, async ({ page, app, room1, util }) => {
util,
}) => {
await util.goTo(room1); await util.goTo(room1);
await util.receiveMessages(room1, ["Msg1"]); await util.receiveMessages(room1, ["Msg1"]);
await util.pinMessages(["Msg1"]); await util.pinMessages(["Msg1"]);
@ -38,7 +40,8 @@ test.describe("Pinned messages", () => {
} }
`, `,
}); });
}); },
);
test("should pin messages and show them in the room info panel", async ({ page, app, room1, util }) => { test("should pin messages and show them in the room info panel", async ({ page, app, room1, util }) => {
await util.goTo(room1); await util.goTo(room1);
@ -73,7 +76,7 @@ test.describe("Pinned messages", () => {
await util.assertPinnedCountInRoomInfo(2); await util.assertPinnedCountInRoomInfo(2);
}); });
test("should unpin all messages", async ({ page, app, room1, util }) => { test("should unpin all messages", { tag: "@screenshot" }, async ({ page, app, room1, util }) => {
await util.goTo(room1); await util.goTo(room1);
await util.receiveMessages(room1, ["Msg1", "Msg2", "Msg3", "Msg4"]); await util.receiveMessages(room1, ["Msg1", "Msg2", "Msg3", "Msg4"]);
await util.pinMessages(["Msg1", "Msg2", "Msg4"]); await util.pinMessages(["Msg1", "Msg2", "Msg4"]);
@ -98,7 +101,7 @@ test.describe("Pinned messages", () => {
await util.assertPinnedCountInRoomInfo(0); await util.assertPinnedCountInRoomInfo(0);
}); });
test("should display one message in the banner", async ({ page, app, room1, util }) => { test("should display one message in the banner", { tag: "@screenshot" }, async ({ page, app, room1, util }) => {
await util.goTo(room1); await util.goTo(room1);
await util.receiveMessages(room1, ["Msg1"]); await util.receiveMessages(room1, ["Msg1"]);
await util.pinMessages(["Msg1"]); await util.pinMessages(["Msg1"]);
@ -106,7 +109,7 @@ test.describe("Pinned messages", () => {
await expect(util.getBanner()).toMatchScreenshot("pinned-message-banner-1-Msg1.png"); await expect(util.getBanner()).toMatchScreenshot("pinned-message-banner-1-Msg1.png");
}); });
test("should display 2 messages in the banner", async ({ page, app, room1, util }) => { test("should display 2 messages in the banner", { tag: "@screenshot" }, async ({ page, app, room1, util }) => {
await util.goTo(room1); await util.goTo(room1);
await util.receiveMessages(room1, ["Msg1", "Msg2"]); await util.receiveMessages(room1, ["Msg1", "Msg2"]);
await util.pinMessages(["Msg1", "Msg2"]); await util.pinMessages(["Msg1", "Msg2"]);
@ -123,7 +126,7 @@ test.describe("Pinned messages", () => {
await expect(util.getBanner()).toMatchScreenshot("pinned-message-banner-2-Msg2.png"); await expect(util.getBanner()).toMatchScreenshot("pinned-message-banner-2-Msg2.png");
}); });
test("should display 4 messages in the banner", async ({ page, app, room1, util }) => { test("should display 4 messages in the banner", { tag: "@screenshot" }, async ({ page, app, room1, util }) => {
await util.goTo(room1); await util.goTo(room1);
await util.receiveMessages(room1, ["Msg1", "Msg2", "Msg3", "Msg4"]); await util.receiveMessages(room1, ["Msg1", "Msg2", "Msg3", "Msg4"]);
await util.pinMessages(["Msg1", "Msg2", "Msg3", "Msg4"]); await util.pinMessages(["Msg1", "Msg2", "Msg3", "Msg4"]);

View file

@ -93,7 +93,7 @@ test.describe("Polls", () => {
}); });
}); });
test("should be creatable and votable", async ({ page, app, bot, user }) => { test("should be creatable and votable", { tag: "@screenshot" }, async ({ page, app, bot, user }) => {
const roomId: string = await app.client.createRoom({}); const roomId: string = await app.client.createRoom({});
await app.client.inviteUser(roomId, bot.credentials.userId); await app.client.inviteUser(roomId, bot.credentials.userId);
await page.goto("/#/room/" + roomId); await page.goto("/#/room/" + roomId);
@ -219,7 +219,10 @@ test.describe("Polls", () => {
await expect(page.locator(".mx_ErrorDialog")).toBeAttached(); await expect(page.locator(".mx_ErrorDialog")).toBeAttached();
}); });
test("should be displayed correctly in thread panel", async ({ page, app, user, bot, homeserver }) => { test(
"should be displayed correctly in thread panel",
{ tag: "@screenshot" },
async ({ page, app, user, bot, homeserver }) => {
const botCharlie = new Bot(page, homeserver, { displayName: "BotCharlie" }); const botCharlie = new Bot(page, homeserver, { displayName: "BotCharlie" });
await botCharlie.prepareClient(); await botCharlie.prepareClient();
@ -229,7 +232,9 @@ test.describe("Polls", () => {
await page.goto("/#/room/" + roomId); await page.goto("/#/room/" + roomId);
// wait until the bots joined // wait until the bots joined
await expect(page.getByText("BotBob and one other were invited and joined")).toBeAttached({ timeout: 10000 }); await expect(page.getByText("BotBob and one other were invited and joined")).toBeAttached({
timeout: 10000,
});
const locator = await app.openMessageComposerOptions(); const locator = await app.openMessageComposerOptions();
await locator.getByRole("menuitem", { name: "Poll" }).click(); await locator.getByRole("menuitem", { name: "Poll" }).click();
@ -274,22 +279,30 @@ test.describe("Polls", () => {
// and thread view // and thread view
await expect( await expect(
page.locator(".mx_ThreadView .mx_MPollBody_totalVotes").getByText("2 votes cast. Vote to see the results"), page
.locator(".mx_ThreadView .mx_MPollBody_totalVotes")
.getByText("2 votes cast. Vote to see the results"),
).toBeAttached(); ).toBeAttached();
// Take snapshots of poll on ThreadView // Take snapshots of poll on ThreadView
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble); await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble);
await expect(page.locator(".mx_ThreadView .mx_EventTile[data-layout='bubble']").first()).toBeVisible(); await expect(page.locator(".mx_ThreadView .mx_EventTile[data-layout='bubble']").first()).toBeVisible();
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot("ThreadView_with_a_poll_on_bubble_layout.png", { await expect(page.locator(".mx_ThreadView")).toMatchScreenshot(
"ThreadView_with_a_poll_on_bubble_layout.png",
{
mask: [page.locator(".mx_MessageTimestamp")], mask: [page.locator(".mx_MessageTimestamp")],
}); },
);
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group); await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group);
await expect(page.locator(".mx_ThreadView .mx_EventTile[data-layout='group']").first()).toBeVisible(); await expect(page.locator(".mx_ThreadView .mx_EventTile[data-layout='group']").first()).toBeVisible();
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot("ThreadView_with_a_poll_on_group_layout.png", { await expect(page.locator(".mx_ThreadView")).toMatchScreenshot(
"ThreadView_with_a_poll_on_group_layout.png",
{
mask: [page.locator(".mx_MessageTimestamp")], mask: [page.locator(".mx_MessageTimestamp")],
}); },
);
const roomViewLocator = page.locator(".mx_RoomView_body"); const roomViewLocator = page.locator(".mx_RoomView_body");
// vote 'Maybe' in the main timeline poll // vote 'Maybe' in the main timeline poll
@ -321,5 +334,6 @@ test.describe("Polls", () => {
// and in thread view tile // and in thread view tile
await expectVoteCounts(page.locator(".mx_ThreadView")); await expectVoteCounts(page.locator(".mx_ThreadView"));
}); },
);
}); });

View file

@ -38,12 +38,10 @@ test.describe("Email Registration", async () => {
await page.goto("/#/register"); await page.goto("/#/register");
}); });
test("registers an account and lands on the use case selection screen", async ({ test(
page, "registers an account and lands on the use case selection screen",
mailhog, { tag: "@screenshot" },
request, async ({ page, mailhog, request, checkA11y }) => {
checkA11y,
}) => {
await expect(page.getByRole("textbox", { name: "Username" })).toBeVisible(); await expect(page.getByRole("textbox", { name: "Username" })).toBeVisible();
// Hide the server text as it contains the randomly allocated Homeserver port // Hide the server text as it contains the randomly allocated Homeserver port
const screenshotOptions = { mask: [page.locator(".mx_ServerPicker_server")] }; const screenshotOptions = { mask: [page.locator(".mx_ServerPicker_server")] };
@ -67,5 +65,6 @@ test.describe("Email Registration", async () => {
await request.get(emailLink); // "Click" the link in the email await request.get(emailLink); // "Click" the link in the email
await expect(page.locator(".mx_UseCaseSelection_skip")).toBeVisible(); await expect(page.locator(".mx_UseCaseSelection_skip")).toBeVisible();
}); },
);
}); });

View file

@ -15,7 +15,10 @@ test.describe("Registration", () => {
await page.goto("/#/register"); await page.goto("/#/register");
}); });
test("registers an account and lands on the home screen", async ({ homeserver, page, checkA11y, crypto }) => { test(
"registers an account and lands on the home screen",
{ tag: "@screenshot" },
async ({ homeserver, page, checkA11y, crypto }) => {
await page.getByRole("button", { name: "Edit", exact: true }).click(); await page.getByRole("button", { name: "Edit", exact: true }).click();
await expect(page.getByRole("button", { name: "Continue", exact: true })).toBeVisible(); await expect(page.getByRole("button", { name: "Continue", exact: true })).toBeVisible();
@ -29,7 +32,10 @@ test.describe("Registration", () => {
await expect(page.getByRole("textbox", { name: "Username", exact: true })).toBeVisible(); await expect(page.getByRole("textbox", { name: "Username", exact: true })).toBeVisible();
// Hide the server text as it contains the randomly allocated Homeserver port // Hide the server text as it contains the randomly allocated Homeserver port
const screenshotOptions = { mask: [page.locator(".mx_ServerPicker_server")], includeDialogBackground: true }; const screenshotOptions = {
mask: [page.locator(".mx_ServerPicker_server")],
includeDialogBackground: true,
};
await expect(page).toMatchScreenshot("registration.png", screenshotOptions); await expect(page).toMatchScreenshot("registration.png", screenshotOptions);
await checkA11y(); await checkA11y();
@ -68,13 +74,14 @@ test.describe("Registration", () => {
await page.getByRole("button", { name: "User menu", exact: true }).click(); await page.getByRole("button", { name: "User menu", exact: true }).click();
await page.getByRole("menuitem", { name: "All settings", exact: true }).click(); await page.getByRole("menuitem", { name: "All settings", exact: true }).click();
await page.getByRole("tab", { name: "Sessions", exact: true }).click(); await page.getByRole("tab", { name: "Sessions", exact: true }).click();
await expect(page.getByTestId("current-session-section").getByTestId("device-metadata-isVerified")).toHaveText( await expect(
"Verified", page.getByTestId("current-session-section").getByTestId("device-metadata-isVerified"),
); ).toHaveText("Verified");
// check that cross-signing keys have been uploaded. // check that cross-signing keys have been uploaded.
await crypto.assertDeviceIsCrossSigned(); await crypto.assertDeviceIsCrossSigned();
}); },
);
test("should require username to fulfil requirements and be available", async ({ homeserver, page }) => { test("should require username to fulfil requirements and be available", async ({ homeserver, page }) => {
await page.getByRole("button", { name: "Edit", exact: true }).click(); await page.getByRole("button", { name: "Edit", exact: true }).click();

View file

@ -18,7 +18,7 @@ test.describe("Release announcement", () => {
labsFlags: ["threadsActivityCentre"], labsFlags: ["threadsActivityCentre"],
}); });
test("should display the release announcement process", async ({ page, app, util }) => { test("should display the release announcement process", { tag: "@screenshot" }, async ({ page, app, util }) => {
// The TAC release announcement should be displayed // The TAC release announcement should be displayed
await util.assertReleaseAnnouncementIsVisible("Threads Activity Centre"); await util.assertReleaseAnnouncementIsVisible("Threads Activity Centre");
// Hide the release announcement // Hide the release announcement

View file

@ -40,7 +40,7 @@ test.describe("FilePanel", () => {
}); });
test.describe("render", () => { test.describe("render", () => {
test("should render empty state", async ({ page }) => { test("should render empty state", { tag: "@screenshot" }, async ({ page }) => {
// Wait until the information about the empty state is rendered // Wait until the information about the empty state is rendered
await expect(page.locator(".mx_EmptyState")).toBeVisible(); await expect(page.locator(".mx_EmptyState")).toBeVisible();
@ -48,7 +48,7 @@ test.describe("FilePanel", () => {
await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("empty.png"); await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("empty.png");
}); });
test("should list tiles on the panel", async ({ page }) => { test("should list tiles on the panel", { tag: "@screenshot" }, async ({ page }) => {
// Upload multiple files // Upload multiple files
await uploadFile(page, "playwright/sample-files/riot.png"); // Image await uploadFile(page, "playwright/sample-files/riot.png"); // Image
await uploadFile(page, "playwright/sample-files/1sec.ogg"); // Audio await uploadFile(page, "playwright/sample-files/1sec.ogg"); // Audio

View file

@ -21,7 +21,7 @@ test.describe("NotificationPanel", () => {
await app.client.createRoom({ name: ROOM_NAME }); await app.client.createRoom({ name: ROOM_NAME });
}); });
test("should render empty state", async ({ page, app }) => { test("should render empty state", { tag: "@screenshot" }, async ({ page, app }) => {
await app.viewRoomByName(ROOM_NAME); await app.viewRoomByName(ROOM_NAME);
await page.getByRole("button", { name: "Notifications" }).click(); await page.getByRole("button", { name: "Notifications" }).click();

View file

@ -38,7 +38,7 @@ test.describe("RightPanel", () => {
}); });
test.describe("in rooms", () => { test.describe("in rooms", () => {
test("should handle long room address and long room name", async ({ page, app }) => { test("should handle long room address and long room name", { tag: "@screenshot" }, async ({ page, app }) => {
await app.client.createRoom({ name: ROOM_NAME_LONG }); await app.client.createRoom({ name: ROOM_NAME_LONG });
await viewRoomSummaryByName(page, app, ROOM_NAME_LONG); await viewRoomSummaryByName(page, app, ROOM_NAME_LONG);

View file

@ -47,7 +47,10 @@ test.describe("Room Directory", () => {
expect(resp.chunk[0].room_id).toEqual(roomId); expect(resp.chunk[0].room_id).toEqual(roomId);
}); });
test("should allow finding published rooms in directory", async ({ page, app, user, bot }) => { test(
"should allow finding published rooms in directory",
{ tag: "@screenshot" },
async ({ page, app, user, bot }) => {
const name = "This is a public room"; const name = "This is a public room";
await bot.createRoom({ await bot.createRoom({
visibility: "public" as Visibility, visibility: "public" as Visibility,
@ -60,7 +63,9 @@ test.describe("Room Directory", () => {
const dialog = page.locator(".mx_SpotlightDialog"); const dialog = page.locator(".mx_SpotlightDialog");
await dialog.getByRole("textbox", { name: "Search" }).fill("Unknown Room"); await dialog.getByRole("textbox", { name: "Search" }).fill("Unknown Room");
await expect( await expect(
dialog.getByText("If you can't find the room you're looking for, ask for an invite or create a new room."), dialog.getByText(
"If you can't find the room you're looking for, ask for an invite or create a new room.",
),
).toHaveClass("mx_SpotlightDialog_otherSearches_messageSearchText"); ).toHaveClass("mx_SpotlightDialog_otherSearches_messageSearchText");
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("filtered-no-results.png"); await expect(page.locator(".mx_Dialog")).toMatchScreenshot("filtered-no-results.png");
@ -76,5 +81,6 @@ test.describe("Room Directory", () => {
.click(); .click();
await expect(page).toHaveURL("/#/room/#test1234:localhost"); await expect(page).toHaveURL("/#/room/#test1234:localhost");
}); },
);
}); });

View file

@ -20,7 +20,7 @@ test.describe("Room Header", () => {
test.use({ test.use({
labsFlags: ["feature_notifications"], labsFlags: ["feature_notifications"],
}); });
test("should render default buttons properly", async ({ page, app, user }) => { test("should render default buttons properly", { tag: "@screenshot" }, async ({ page, app, user }) => {
await app.client.createRoom({ name: "Test Room" }); await app.client.createRoom({ name: "Test Room" });
await app.viewRoomByName("Test Room"); await app.viewRoomByName("Test Room");
@ -51,7 +51,10 @@ test.describe("Room Header", () => {
await expect(header).toMatchScreenshot("room-header.png"); await expect(header).toMatchScreenshot("room-header.png");
}); });
test("should render a very long room name without collapsing the buttons", async ({ page, app, user }) => { test(
"should render a very long room name without collapsing the buttons",
{ tag: "@screenshot" },
async ({ page, app, user }) => {
const LONG_ROOM_NAME = const LONG_ROOM_NAME =
"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore " + "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore " +
"et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut " + "et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut " +
@ -78,7 +81,8 @@ test.describe("Room Header", () => {
} }
await expect(header).toMatchScreenshot("room-header-long-name.png"); await expect(header).toMatchScreenshot("room-header-long-name.png");
}); },
);
}); });
test.describe("with a video room", () => { test.describe("with a video room", () => {
@ -99,7 +103,10 @@ test.describe("Room Header", () => {
test.describe("and with feature_notifications enabled", () => { test.describe("and with feature_notifications enabled", () => {
test.use({ labsFlags: ["feature_video_rooms", "feature_notifications"] }); test.use({ labsFlags: ["feature_video_rooms", "feature_notifications"] });
test("should render buttons for chat, room info, threads and facepile", async ({ page, app, user }) => { test(
"should render buttons for chat, room info, threads and facepile",
{ tag: "@screenshot" },
async ({ page, app, user }) => {
await createVideoRoom(page, app); await createVideoRoom(page, app);
const header = page.locator(".mx_RoomHeader"); const header = page.locator(".mx_RoomHeader");
@ -122,7 +129,8 @@ test.describe("Room Header", () => {
await expect(header.getByRole("button")).toHaveCount(7); await expect(header.getByRole("button")).toHaveCount(7);
await expect(header).toMatchScreenshot("room-header-video-room.png"); await expect(header).toMatchScreenshot("room-header-video-room.png");
}); },
);
}); });
test("should render a working chat button which opens the timeline on a right panel", async ({ test("should render a working chat button which opens the timeline on a right panel", async ({

View file

@ -23,7 +23,7 @@ test.describe("Account user settings tab", () => {
}, },
}); });
test("should be rendered properly", async ({ uut, user }) => { test("should be rendered properly", { tag: "@screenshot" }, async ({ uut, user }) => {
await expect(uut).toMatchScreenshot("account.png"); await expect(uut).toMatchScreenshot("account.png");
// Assert that the top heading is rendered // Assert that the top heading is rendered
@ -71,7 +71,7 @@ test.describe("Account user settings tab", () => {
); );
}); });
test("should respond to small screen sizes", async ({ page, uut }) => { test("should respond to small screen sizes", { tag: "@screenshot" }, async ({ page, uut }) => {
await page.setViewportSize({ width: 700, height: 600 }); await page.setViewportSize({ width: 700, height: 600 });
await expect(uut).toMatchScreenshot("account-smallscreen.png"); await expect(uut).toMatchScreenshot("account-smallscreen.png");
}); });

View file

@ -13,7 +13,7 @@ test.describe("Appearance user settings tab", () => {
displayName: "Hanako", displayName: "Hanako",
}); });
test("should be rendered properly", async ({ page, user, app }) => { test("should be rendered properly", { tag: "@screenshot" }, async ({ page, user, app }) => {
const tab = await app.settings.openUserSettings("Appearance"); const tab = await app.settings.openUserSettings("Appearance");
// Click "Show advanced" link button // Click "Show advanced" link button
@ -25,7 +25,10 @@ test.describe("Appearance user settings tab", () => {
await expect(tab).toMatchScreenshot("appearance-tab.png"); await expect(tab).toMatchScreenshot("appearance-tab.png");
}); });
test("should support changing font size by using the font size dropdown", async ({ page, app, user }) => { test(
"should support changing font size by using the font size dropdown",
{ tag: "@screenshot" },
async ({ page, app, user }) => {
await app.settings.openUserSettings("Appearance"); await app.settings.openUserSettings("Appearance");
const tab = page.getByTestId("mx_AppearanceUserSettingsTab"); const tab = page.getByTestId("mx_AppearanceUserSettingsTab");
@ -37,7 +40,8 @@ test.describe("Appearance user settings tab", () => {
await fontDropdown.getByLabel("Font size").selectOption({ value: "-4" }); await fontDropdown.getByLabel("Font size").selectOption({ value: "-4" });
await expect(page).toMatchScreenshot("window-12px.png", { includeDialogBackground: true }); await expect(page).toMatchScreenshot("window-12px.png", { includeDialogBackground: true });
}); },
);
test("should support enabling system font", async ({ page, app, user }) => { test("should support enabling system font", async ({ page, app, user }) => {
await app.settings.openUserSettings("Appearance"); await app.settings.openUserSettings("Appearance");

View file

@ -20,7 +20,10 @@ test.describe("Appearance user settings tab", () => {
await util.openAppearanceTab(); await util.openAppearanceTab();
}); });
test("should change the message layout from modern to bubble", async ({ page, app, user, util }) => { test(
"should change the message layout from modern to bubble",
{ tag: "@screenshot" },
async ({ page, app, user, util }) => {
await util.assertScreenshot(util.getMessageLayoutPanel(), "message-layout-panel-modern.png"); await util.assertScreenshot(util.getMessageLayoutPanel(), "message-layout-panel-modern.png");
await util.getBubbleLayout().click(); await util.getBubbleLayout().click();
@ -33,7 +36,8 @@ test.describe("Appearance user settings tab", () => {
// Assert that the room layout is set to bubble layout // Assert that the room layout is set to bubble layout
await util.assertBubbleLayout(); await util.assertBubbleLayout();
await util.assertScreenshot(util.getMessageLayoutPanel(), "message-layout-panel-bubble.png"); await util.assertScreenshot(util.getMessageLayoutPanel(), "message-layout-panel-bubble.png");
}); },
);
test("should enable compact layout when the modern layout is selected", async ({ page, app, user, util }) => { test("should enable compact layout when the modern layout is selected", async ({ page, app, user, util }) => {
await expect(util.getCompactLayoutCheckbox()).not.toBeChecked(); await expect(util.getCompactLayoutCheckbox()).not.toBeChecked();

View file

@ -20,7 +20,10 @@ test.describe("Appearance user settings tab", () => {
await util.openAppearanceTab(); await util.openAppearanceTab();
}); });
test("should be rendered with the light theme selected", async ({ page, app, util }) => { test(
"should be rendered with the light theme selected",
{ tag: "@screenshot" },
async ({ page, app, util }) => {
// Assert that 'Match system theme' is not checked // Assert that 'Match system theme' is not checked
await expect(util.getMatchSystemThemeCheckbox()).not.toBeChecked(); await expect(util.getMatchSystemThemeCheckbox()).not.toBeChecked();
@ -31,9 +34,13 @@ test.describe("Appearance user settings tab", () => {
await expect(util.getHighContrastTheme()).not.toBeChecked(); await expect(util.getHighContrastTheme()).not.toBeChecked();
await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-light.png"); await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-light.png");
}); },
);
test("should disable the themes when the system theme is clicked", async ({ page, app, util }) => { test(
"should disable the themes when the system theme is clicked",
{ tag: "@screenshot" },
async ({ page, app, util }) => {
await util.getMatchSystemThemeCheckbox().click(); await util.getMatchSystemThemeCheckbox().click();
// Assert that the themes are disabled // Assert that the themes are disabled
@ -42,9 +49,10 @@ test.describe("Appearance user settings tab", () => {
await expect(util.getHighContrastTheme()).toBeDisabled(); await expect(util.getHighContrastTheme()).toBeDisabled();
await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-match-system-enabled.png"); await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-match-system-enabled.png");
}); },
);
test("should change the theme to dark", async ({ page, app, util }) => { test("should change the theme to dark", { tag: "@screenshot" }, async ({ page, app, util }) => {
// Assert that the light theme is selected // Assert that the light theme is selected
await expect(util.getLightTheme()).toBeChecked(); await expect(util.getLightTheme()).toBeChecked();
@ -63,11 +71,14 @@ test.describe("Appearance user settings tab", () => {
labsFlags: ["feature_custom_themes"], labsFlags: ["feature_custom_themes"],
}); });
test("should render the custom theme section", async ({ page, app, util }) => { test("should render the custom theme section", { tag: "@screenshot" }, async ({ page, app, util }) => {
await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme.png"); await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme.png");
}); });
test("should be able to add and remove a custom theme", async ({ page, app, util }) => { test(
"should be able to add and remove a custom theme",
{ tag: "@screenshot" },
async ({ page, app, util }) => {
await util.addCustomTheme(); await util.addCustomTheme();
await expect(util.getCustomTheme()).not.toBeChecked(); await expect(util.getCustomTheme()).not.toBeChecked();
@ -75,7 +86,8 @@ test.describe("Appearance user settings tab", () => {
await util.removeCustomTheme(); await util.removeCustomTheme();
await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme-removed.png"); await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme-removed.png");
}); },
);
}); });
}); });
}); });

View file

@ -20,7 +20,7 @@ test.describe("General room settings tab", () => {
await app.viewRoomByName(roomName); await app.viewRoomByName(roomName);
}); });
test("should be rendered properly", async ({ page, app }) => { test("should be rendered properly", { tag: "@screenshot" }, async ({ page, app }) => {
const settings = await app.settings.openRoomSettings("General"); const settings = await app.settings.openRoomSettings("General");
// Assert that "Show less" details element is rendered // Assert that "Show less" details element is rendered

View file

@ -23,7 +23,7 @@ test.describe("Preferences user settings tab", () => {
}, },
}); });
test("should be rendered properly", async ({ app, page, user }) => { test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page, user }) => {
page.setViewportSize({ width: 1024, height: 3300 }); page.setViewportSize({ width: 1024, height: 3300 });
const tab = await app.settings.openUserSettings("Preferences"); const tab = await app.settings.openUserSettings("Preferences");
// Assert that the top heading is rendered // Assert that the top heading is rendered

View file

@ -36,7 +36,7 @@ test.describe("Security user settings tab", () => {
}); });
test.describe("AnalyticsLearnMoreDialog", () => { test.describe("AnalyticsLearnMoreDialog", () => {
test("should be rendered properly", async ({ app, page }) => { test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page }) => {
const tab = await app.settings.openUserSettings("Security"); const tab = await app.settings.openUserSettings("Security");
await tab.getByRole("button", { name: "Learn more" }).click(); await tab.getByRole("button", { name: "Learn more" }).click();
await expect(page.locator(".mx_AnalyticsLearnMoreDialog_wrapper .mx_Dialog")).toMatchScreenshot( await expect(page.locator(".mx_AnalyticsLearnMoreDialog_wrapper .mx_Dialog")).toMatchScreenshot(

View file

@ -0,0 +1,67 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
* Please see LICENSE files in the repository root for full details.
*/
import { test, expect } from "../../element-web-test";
test.describe("Share dialog", () => {
test.use({
displayName: "Alice",
room: async ({ app, user, bot }, use) => {
const roomId = await app.client.createRoom({ name: "Alice room" });
await use({ roomId });
},
});
test("should share a room", { tag: "@screenshot" }, async ({ page, app, room }) => {
await app.viewRoomById(room.roomId);
await app.toggleRoomInfoPanel();
await page.getByRole("menuitem", { name: "Copy link" }).click();
const dialog = page.getByRole("dialog", { name: "Share room" });
await expect(dialog.getByText(`https://matrix.to/#/${room.roomId}`)).toBeVisible();
expect(dialog).toMatchScreenshot("share-dialog-room.png", {
// QRCode and url changes at every run
mask: [page.locator(".mx_QRCode"), page.locator(".mx_ShareDialog_top > span")],
});
});
test("should share a room member", { tag: "@screenshot" }, async ({ page, app, room, user }) => {
await app.viewRoomById(room.roomId);
await app.client.sendMessage(room.roomId, { body: "hello", msgtype: "m.text" });
const rightPanel = await app.toggleRoomInfoPanel();
await rightPanel.getByRole("menuitem", { name: "People" }).click();
await rightPanel.getByRole("button", { name: `${user.userId} (power 100)` }).click();
await rightPanel.getByRole("button", { name: "Share profile" }).click();
const dialog = page.getByRole("dialog", { name: "Share User" });
await expect(dialog.getByText(`https://matrix.to/#/${user.userId}`)).toBeVisible();
expect(dialog).toMatchScreenshot("share-dialog-user.png", {
// QRCode changes at every run
mask: [page.locator(".mx_QRCode")],
});
});
test("should share an event", { tag: "@screenshot" }, async ({ page, app, room }) => {
await app.viewRoomById(room.roomId);
await app.client.sendMessage(room.roomId, { body: "hello", msgtype: "m.text" });
const timelineMessage = page.locator(".mx_MTextBody", { hasText: "hello" });
await timelineMessage.hover();
await page.getByRole("button", { name: "Options", exact: true }).click();
await page.getByRole("menuitem", { name: "Share" }).click();
const dialog = page.getByRole("dialog", { name: "Share Room Message" });
await expect(dialog.getByRole("checkbox", { name: "Link to selected message" })).toBeChecked();
expect(dialog).toMatchScreenshot("share-dialog-event.png", {
// QRCode and url changes at every run
mask: [page.locator(".mx_QRCode"), page.locator(".mx_ShareDialog_top > span")],
});
await dialog.getByRole("checkbox", { name: "Link to selected message" }).click();
await expect(dialog.getByRole("checkbox", { name: "Link to selected message" })).not.toBeChecked();
});
});

View file

@ -55,7 +55,7 @@ test.describe("Spaces", () => {
botCreateOpts: { displayName: "BotBob" }, botCreateOpts: { displayName: "BotBob" },
}); });
test("should allow user to create public space", async ({ page, app, user }) => { test("should allow user to create public space", { tag: "@screenshot" }, async ({ page, app, user }) => {
const contextMenu = await openSpaceCreateMenu(page); const contextMenu = await openSpaceCreateMenu(page);
await expect(contextMenu).toMatchScreenshot("space-create-menu.png"); await expect(contextMenu).toMatchScreenshot("space-create-menu.png");
@ -88,7 +88,7 @@ test.describe("Spaces", () => {
await expect(page.getByRole("treeitem", { name: "Jokes" })).toBeVisible(); await expect(page.getByRole("treeitem", { name: "Jokes" })).toBeVisible();
}); });
test("should allow user to create private space", async ({ page, app, user }) => { test("should allow user to create private space", { tag: "@screenshot" }, async ({ page, app, user }) => {
const menu = await openSpaceCreateMenu(page); const menu = await openSpaceCreateMenu(page);
await menu.getByRole("button", { name: "Private" }).click(); await menu.getByRole("button", { name: "Private" }).click();
@ -216,13 +216,10 @@ test.describe("Spaces", () => {
await expect(hierarchyList.getByRole("treeitem", { name: "Gaming" }).getByRole("button")).toBeVisible(); await expect(hierarchyList.getByRole("treeitem", { name: "Gaming" }).getByRole("button")).toBeVisible();
}); });
test("should render subspaces in the space panel only when expanded", async ({ test(
page, "should render subspaces in the space panel only when expanded",
app, { tag: "@screenshot" },
user, async ({ page, app, user, axe, checkA11y }) => {
axe,
checkA11y,
}) => {
axe.disableRules([ axe.disableRules([
// Disable this check as it triggers on nested roving tab index elements which are in practice fine // Disable this check as it triggers on nested roving tab index elements which are in practice fine
"nested-interactive", "nested-interactive",
@ -258,7 +255,8 @@ test.describe("Spaces", () => {
await checkA11y(); await checkA11y();
await expect(page.locator(".mx_SpacePanel")).toMatchScreenshot("space-panel-expanded.png"); await expect(page.locator(".mx_SpacePanel")).toMatchScreenshot("space-panel-expanded.png");
}); },
);
test("should not soft crash when joining a room from space hierarchy which has a link in its topic", async ({ test("should not soft crash when joining a room from space hierarchy which has a link in its topic", async ({
page, page,

View file

@ -276,7 +276,7 @@ export class Helpers {
* Assert that the threads activity centre button has no indicator * Assert that the threads activity centre button has no indicator
*/ */
async assertNoTacIndicator() { async assertNoTacIndicator() {
// Assert by checkng neither of the known indicators are visible first. This will wait // Assert by checking neither of the known indicators are visible first. This will wait
// if it takes a little time to disappear, but the screenshot comparison won't. // if it takes a little time to disappear, but the screenshot comparison won't.
await expect(this.getTacButton().locator("[data-indicator='success']")).not.toBeVisible(); await expect(this.getTacButton().locator("[data-indicator='success']")).not.toBeVisible();
await expect(this.getTacButton().locator("[data-indicator='critical']")).not.toBeVisible(); await expect(this.getTacButton().locator("[data-indicator='critical']")).not.toBeVisible();
@ -376,7 +376,7 @@ export class Helpers {
* Clicks the button to mark all threads as read in the current room * Clicks the button to mark all threads as read in the current room
*/ */
clickMarkAllThreadsRead() { clickMarkAllThreadsRead() {
return this.page.getByLabel("Mark all as read").click(); return this.page.locator("#thread-panel").getByRole("button", { name: "Mark all as read" }).click();
} }
} }

View file

@ -16,16 +16,18 @@ test.describe("Threads Activity Centre", () => {
labsFlags: ["threadsActivityCentre"], labsFlags: ["threadsActivityCentre"],
}); });
test("should have the button correctly aligned and displayed in the space panel when expanded", async ({ test(
util, "should have the button correctly aligned and displayed in the space panel when expanded",
}) => { { tag: "@screenshot" },
async ({ util }) => {
// Open the space panel // Open the space panel
await util.expandSpacePanel(); await util.expandSpacePanel();
// The buttons in the space panel should be aligned when expanded // The buttons in the space panel should be aligned when expanded
await expect(util.getSpacePanel()).toMatchScreenshot("tac-button-expanded.png"); await expect(util.getSpacePanel()).toMatchScreenshot("tac-button-expanded.png");
}); },
);
test("should not show indicator when there is no thread", async ({ room1, util }) => { test("should not show indicator when there is no thread", { tag: "@screenshot" }, async ({ room1, util }) => {
// No indicator should be shown // No indicator should be shown
await util.assertNoTacIndicator(); await util.assertNoTacIndicator();
@ -62,7 +64,7 @@ test.describe("Threads Activity Centre", () => {
await util.assertHighlightIndicator(); await util.assertHighlightIndicator();
}); });
test("should show the rooms with unread threads", async ({ room1, room2, util, msg }) => { test("should show the rooms with unread threads", { tag: "@screenshot" }, async ({ room1, room2, util, msg }) => {
await util.goTo(room2); await util.goTo(room2);
await util.populateThreads(room1, room2, msg); await util.populateThreads(room1, room2, msg);
// The indicator should be shown // The indicator should be shown
@ -79,7 +81,7 @@ test.describe("Threads Activity Centre", () => {
await expect(util.getTacPanel()).toMatchScreenshot("tac-panel-mix-unread.png"); await expect(util.getTacPanel()).toMatchScreenshot("tac-panel-mix-unread.png");
}); });
test("should update with a thread is read", async ({ room1, room2, util, msg }) => { test("should update with a thread is read", { tag: "@screenshot" }, async ({ room1, room2, util, msg }) => {
await util.goTo(room2); await util.goTo(room2);
await util.populateThreads(room1, room2, msg); await util.populateThreads(room1, room2, msg);
@ -128,7 +130,7 @@ test.describe("Threads Activity Centre", () => {
await expect(page.locator(".mx_SpotlightDialog")).not.toBeVisible(); await expect(page.locator(".mx_SpotlightDialog")).not.toBeVisible();
}); });
test("should have the correct hover state", async ({ util, page }) => { test("should have the correct hover state", { tag: "@screenshot" }, async ({ util, page }) => {
await util.hoverTacButton(); await util.hoverTacButton();
await expect(util.getSpacePanel()).toMatchScreenshot("tac-hovered.png"); await expect(util.getSpacePanel()).toMatchScreenshot("tac-hovered.png");
@ -138,7 +140,7 @@ test.describe("Threads Activity Centre", () => {
await expect(util.getSpacePanel()).toMatchScreenshot("tac-hovered-expanded.png"); await expect(util.getSpacePanel()).toMatchScreenshot("tac-hovered-expanded.png");
}); });
test("should mark all threads as read", async ({ room1, room2, util, msg, page }) => { test("should mark all threads as read", { tag: "@screenshot" }, async ({ room1, room2, util, msg, page }) => {
await util.receiveMessages(room1, ["Msg1", msg.threadedOff("Msg1", "Resp1")]); await util.receiveMessages(room1, ["Msg1", msg.threadedOff("Msg1", "Resp1")]);
await util.assertNotificationTac(); await util.assertNotificationTac();
@ -146,7 +148,7 @@ test.describe("Threads Activity Centre", () => {
await util.openTac(); await util.openTac();
await util.clickRoomInTac(room1.name); await util.clickRoomInTac(room1.name);
util.clickMarkAllThreadsRead(); await util.clickMarkAllThreadsRead();
await util.assertNoTacIndicator(); await util.assertNoTacIndicator();
}); });

View file

@ -25,7 +25,7 @@ test.describe("Threads", () => {
}); });
// Flaky: https://github.com/vector-im/element-web/issues/26452 // Flaky: https://github.com/vector-im/element-web/issues/26452
test.skip("should be usable for a conversation", async ({ page, app, bot }) => { test.skip("should be usable for a conversation", { tag: "@screenshot" }, async ({ page, app, bot }) => {
const roomId = await app.client.createRoom({}); const roomId = await app.client.createRoom({});
await app.client.inviteUser(roomId, bot.credentials.userId); await app.client.inviteUser(roomId, bot.credentials.userId);
await bot.joinRoom(roomId); await bot.joinRoom(roomId);
@ -150,7 +150,7 @@ test.describe("Threads", () => {
).toHaveCSS("padding-inline-start", ThreadViewGroupSpacingStart); ).toHaveCSS("padding-inline-start", ThreadViewGroupSpacingStart);
// Take snapshot of group layout (IRC layout is not available on ThreadView) // Take snapshot of group layout (IRC layout is not available on ThreadView)
expect(page.locator(".mx_ThreadView")).toMatchScreenshot( await expect(page.locator(".mx_ThreadView")).toMatchScreenshot(
"ThreadView_with_reaction_and_a_hidden_event_on_group_layout.png", "ThreadView_with_reaction_and_a_hidden_event_on_group_layout.png",
{ {
mask: mask, mask: mask,
@ -174,7 +174,7 @@ test.describe("Threads", () => {
.toHaveCSS("margin-inline-start", "0px"); .toHaveCSS("margin-inline-start", "0px");
// Take snapshot of bubble layout // Take snapshot of bubble layout
expect(page.locator(".mx_ThreadView")).toMatchScreenshot( await expect(page.locator(".mx_ThreadView")).toMatchScreenshot(
"ThreadView_with_reaction_and_a_hidden_event_on_bubble_layout.png", "ThreadView_with_reaction_and_a_hidden_event_on_bubble_layout.png",
{ {
mask: mask, mask: mask,
@ -351,7 +351,10 @@ test.describe("Threads", () => {
}); });
}); });
test("should send location and reply to the location on ThreadView", async ({ page, app, bot }) => { test(
"should send location and reply to the location on ThreadView",
{ tag: "@screenshot" },
async ({ page, app, bot }) => {
const roomId = await app.client.createRoom({}); const roomId = await app.client.createRoom({});
await app.client.inviteUser(roomId, bot.credentials.userId); await app.client.inviteUser(roomId, bot.credentials.userId);
await bot.joinRoom(roomId); await bot.joinRoom(roomId);
@ -401,7 +404,8 @@ test.describe("Threads", () => {
// Take a snapshot of reply to the shared location // Take a snapshot of reply to the shared location
await page.addStyleTag({ content: css }); await page.addStyleTag({ content: css });
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot("Reply_to_the_location_on_ThreadView.png"); await expect(page.locator(".mx_ThreadView")).toMatchScreenshot("Reply_to_the_location_on_ThreadView.png");
}); },
);
test("right panel behaves correctly", async ({ page, app, user }) => { test("right panel behaves correctly", async ({ page, app, user }) => {
// Create room // Create room

View file

@ -137,7 +137,10 @@ test.describe("Timeline", () => {
}); });
test.describe("configure room", () => { test.describe("configure room", () => {
test("should create and configure a room on IRC layout", async ({ page, app, room }) => { test(
"should create and configure a room on IRC layout",
{ tag: "@screenshot" },
async ({ page, app, room }) => {
await page.goto(`/#/room/${room.roomId}`); await page.goto(`/#/room/${room.roomId}`);
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC); await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
await expect( await expect(
@ -151,9 +154,13 @@ test.describe("Timeline", () => {
await expect(page.locator(".mx_TimelineSeparator")).toHaveText("today"); await expect(page.locator(".mx_TimelineSeparator")).toHaveText("today");
await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("configured-room-irc-layout.png"); await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("configured-room-irc-layout.png");
}); },
);
test("should have an expanded generic event list summary (GELS) on IRC layout", async ({ page, app, room }) => { test(
"should have an expanded generic event list summary (GELS) on IRC layout",
{ tag: "@screenshot" },
async ({ page, app, room }) => {
await page.goto(`/#/room/${room.roomId}`); await page.goto(`/#/room/${room.roomId}`);
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC); await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
@ -179,13 +186,13 @@ test.describe("Timeline", () => {
} }
`, `,
}); });
}); },
);
test("should have an expanded generic event list summary (GELS) on compact modern/group layout", async ({ test(
page, "should have an expanded generic event list summary (GELS) on compact modern/group layout",
app, { tag: "@screenshot" },
room, async ({ page, app, room }) => {
}) => {
await page.goto(`/#/room/${room.roomId}`); await page.goto(`/#/room/${room.roomId}`);
// Set compact modern layout // Set compact modern layout
@ -213,13 +220,13 @@ test.describe("Timeline", () => {
} }
`, `,
}); });
}); },
);
test("should click 'collapse' on the first hovered info event line inside GELS on bubble layout", async ({ test(
page, "should click 'collapse' on the first hovered info event line inside GELS on bubble layout",
app, { tag: "@screenshot" },
room, async ({ page, app, room }) => {
}) => {
// This test checks clickability of the "Collapse" link button, which had been covered with // This test checks clickability of the "Collapse" link button, which had been covered with
// MessageActionBar's safe area - https://github.com/vector-im/element-web/issues/22864 // MessageActionBar's safe area - https://github.com/vector-im/element-web/issues/22864
@ -250,7 +257,9 @@ test.describe("Timeline", () => {
}); });
// Click "collapse" link button on the first hovered info event line // Click "collapse" link button on the first hovered info event line
const firstTile = gels.locator(".mx_GenericEventListSummary_unstyledList .mx_EventTile_info:first-of-type"); const firstTile = gels.locator(
".mx_GenericEventListSummary_unstyledList .mx_EventTile_info:first-of-type",
);
await firstTile.hover(); await firstTile.hover();
await expect(firstTile.getByRole("toolbar", { name: "Message Actions" })).toBeVisible(); await expect(firstTile.getByRole("toolbar", { name: "Message Actions" })).toBeVisible();
await gels.getByRole("button", { name: "Collapse" }).click(); await gels.getByRole("button", { name: "Collapse" }).click();
@ -262,15 +271,13 @@ test.describe("Timeline", () => {
await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("collapsed-gels-bubble-layout.png", { await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("collapsed-gels-bubble-layout.png", {
mask: [page.locator(".mx_MessageTimestamp")], mask: [page.locator(".mx_MessageTimestamp")],
}); });
}); },
);
test("should add inline start margin to an event line on IRC layout", async ({ test(
page, "should add inline start margin to an event line on IRC layout",
app, { tag: "@screenshot" },
room, async ({ page, app, room, axe, checkA11y }) => {
axe,
checkA11y,
}) => {
axe.disableRules("color-contrast"); axe.disableRules("color-contrast");
await page.goto(`/#/room/${room.roomId}`); await page.goto(`/#/room/${room.roomId}`);
@ -312,7 +319,8 @@ test.describe("Timeline", () => {
}, },
); );
await checkA11y(); await checkA11y();
}); },
);
}); });
test.describe("message displaying", () => { test.describe("message displaying", () => {
@ -332,11 +340,10 @@ test.describe("Timeline", () => {
).toBeVisible(); ).toBeVisible();
}; };
test("should align generic event list summary with messages and emote on IRC layout", async ({ test(
page, "should align generic event list summary with messages and emote on IRC layout",
app, { tag: "@screenshot" },
room, async ({ page, app, room }) => {
}) => {
// This test aims to check: // This test aims to check:
// 1. Alignment of collapsed GELS (generic event list summary) and messages // 1. Alignment of collapsed GELS (generic event list summary) and messages
// 2. Alignment of expanded GELS and messages // 2. Alignment of expanded GELS and messages
@ -372,10 +379,9 @@ test.describe("Timeline", () => {
// = var(--name-width) + var(--icon-width) + var(--MessageTimestamp-width) + 2 * var(--right-padding) // = var(--name-width) + var(--icon-width) + var(--MessageTimestamp-width) + 2 * var(--right-padding)
// = 80 + 14 + 46 + 2 * 5 // = 80 + 14 + 46 + 2 * 5
// = 150px // = 150px
await expect(page.locator(".mx_GenericEventListSummary[data-layout=irc] > .mx_EventTile_line")).toHaveCSS( await expect(
"padding-inline-start", page.locator(".mx_GenericEventListSummary[data-layout=irc] > .mx_EventTile_line"),
"150px", ).toHaveCSS("padding-inline-start", "150px");
);
// Check width and spacing values of elements in .mx_EventTile, which should be equal to 150px // Check width and spacing values of elements in .mx_EventTile, which should be equal to 150px
// --right-padding should be applied // --right-padding should be applied
for (const locator of await page.locator(".mx_EventTile > a").all()) { for (const locator of await page.locator(".mx_EventTile > a").all()) {
@ -416,10 +422,13 @@ test.describe("Timeline", () => {
page.locator(".mx_EventTile[data-layout=irc].mx_EventTile_info:first-of-type .mx_EventTile_line"), page.locator(".mx_EventTile[data-layout=irc].mx_EventTile_info:first-of-type .mx_EventTile_line"),
).toHaveCSS("margin-inline-start", "99px"); ).toHaveCSS("margin-inline-start", "99px");
// Record alignment of expanded GELS and messages on messagePanel // Record alignment of expanded GELS and messages on messagePanel
await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-and-messages-irc-layout.png", { await expect(page.locator(".mx_MainSplit")).toMatchScreenshot(
"expanded-gels-and-messages-irc-layout.png",
{
// Exclude timestamp from snapshot of mx_MainSplit // Exclude timestamp from snapshot of mx_MainSplit
mask: [page.locator(".mx_MessageTimestamp")], mask: [page.locator(".mx_MessageTimestamp")],
}); },
);
// 3. Alignment of expanded GELS and placeholder of deleted message // 3. Alignment of expanded GELS and placeholder of deleted message
// Delete the second (last) message // Delete the second (last) message
@ -431,15 +440,20 @@ test.describe("Timeline", () => {
await page.locator(".mx_Dialog_buttons").getByRole("button", { name: "Remove" }).click(); await page.locator(".mx_Dialog_buttons").getByRole("button", { name: "Remove" }).click();
// Make sure the dialog was closed and the second (last) message was redacted // Make sure the dialog was closed and the second (last) message was redacted
await expect(page.locator(".mx_Dialog")).not.toBeVisible(); await expect(page.locator(".mx_Dialog")).not.toBeVisible();
await expect(page.locator(".mx_GenericEventListSummary .mx_EventTile_last .mx_RedactedBody")).toBeVisible(); await expect(
page.locator(".mx_GenericEventListSummary .mx_EventTile_last .mx_RedactedBody"),
).toBeVisible();
await expect( await expect(
page.locator(".mx_GenericEventListSummary .mx_EventTile_last .mx_EventTile_receiptSent"), page.locator(".mx_GenericEventListSummary .mx_EventTile_last .mx_EventTile_receiptSent"),
).toBeVisible(); ).toBeVisible();
// Record alignment of expanded GELS and placeholder of deleted message on messagePanel // Record alignment of expanded GELS and placeholder of deleted message on messagePanel
await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-redaction-placeholder.png", { await expect(page.locator(".mx_MainSplit")).toMatchScreenshot(
"expanded-gels-redaction-placeholder.png",
{
// Exclude timestamp from snapshot of mx_MainSplit // Exclude timestamp from snapshot of mx_MainSplit
mask: [page.locator(".mx_MessageTimestamp")], mask: [page.locator(".mx_MessageTimestamp")],
}); },
);
// 4. Alignment of expanded GELS, placeholder of deleted message, and emote // 4. Alignment of expanded GELS, placeholder of deleted message, and emote
// Send a emote // Send a emote
@ -447,7 +461,10 @@ test.describe("Timeline", () => {
.locator(".mx_RoomView_body") .locator(".mx_RoomView_body")
.getByRole("textbox", { name: "Send a message…" }) .getByRole("textbox", { name: "Send a message…" })
.fill("/me says hello to Mr. Bot"); .fill("/me says hello to Mr. Bot");
await page.locator(".mx_RoomView_body").getByRole("textbox", { name: "Send a message…" }).press("Enter"); await page
.locator(".mx_RoomView_body")
.getByRole("textbox", { name: "Send a message…" })
.press("Enter");
// Check inline start margin of its avatar // Check inline start margin of its avatar
// Here --right-padding is for the avatar on the message line // Here --right-padding is for the avatar on the message line
// See: _IRCLayout.pcss // See: _IRCLayout.pcss
@ -456,15 +473,21 @@ test.describe("Timeline", () => {
// = 80 + 14 + 1 * 5 // = 80 + 14 + 1 * 5
await expect(page.locator(".mx_EventTile_emote .mx_EventTile_avatar")).toHaveCSS("margin-left", "99px"); await expect(page.locator(".mx_EventTile_emote .mx_EventTile_avatar")).toHaveCSS("margin-left", "99px");
// Make sure emote was sent // Make sure emote was sent
await expect(page.locator(".mx_EventTile_last.mx_EventTile_emote .mx_EventTile_receiptSent")).toBeVisible(); await expect(
page.locator(".mx_EventTile_last.mx_EventTile_emote .mx_EventTile_receiptSent"),
).toBeVisible();
// Record alignment of expanded GELS, placeholder of deleted message, and emote // Record alignment of expanded GELS, placeholder of deleted message, and emote
await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-emote-irc-layout.png", { await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-emote-irc-layout.png", {
// Exclude timestamp from snapshot of mx_MainSplit // Exclude timestamp from snapshot of mx_MainSplit
mask: [page.locator(".mx_MessageTimestamp")], mask: [page.locator(".mx_MessageTimestamp")],
}); });
}); },
);
test("should render EventTiles on IRC, modern (group), and bubble layout", async ({ page, app, room }) => { test(
"should render EventTiles on IRC, modern (group), and bubble layout",
{ tag: "@screenshot" },
async ({ page, app, room }) => {
const screenshotOptions = { const screenshotOptions = {
// Hide because flaky - See https://github.com/vector-im/element-web/issues/24957 // Hide because flaky - See https://github.com/vector-im/element-web/issues/24957
mask: [page.locator(".mx_MessageTimestamp")], mask: [page.locator(".mx_MessageTimestamp")],
@ -491,7 +514,9 @@ test.describe("Timeline", () => {
await composer.fill("This message has an inline emoji 👒"); await composer.fill("This message has an inline emoji 👒");
await composer.press("Enter"); await composer.press("Enter");
await expect(page.locator(".mx_RoomView").getByText("This message has an inline emoji 👒")).toBeVisible(); await expect(
page.locator(".mx_RoomView").getByText("This message has an inline emoji 👒"),
).toBeVisible();
//////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////
// IRC layout // IRC layout
@ -558,9 +583,13 @@ test.describe("Timeline", () => {
"event-tiles-bubble-layout.png", "event-tiles-bubble-layout.png",
screenshotOptions, screenshotOptions,
); );
}); },
);
test("should set inline start padding to a hidden event line", async ({ page, app, room }) => { test(
"should set inline start padding to a hidden event line",
{ tag: "@screenshot" },
async ({ page, app, room }) => {
test.skip( test.skip(
true, true,
"Disabled due to screenshot test being flaky - https://github.com/element-hq/element-web/issues/26890", "Disabled due to screenshot test being flaky - https://github.com/element-hq/element-web/issues/26890",
@ -612,9 +641,10 @@ test.describe("Timeline", () => {
"hidden-event-line-padding-modern-layout.png", "hidden-event-line-padding-modern-layout.png",
screenshotOptions, screenshotOptions,
); );
}); },
);
test("should click view source event toggle", async ({ page, app, room }) => { test("should click view source event toggle", { tag: "@screenshot" }, async ({ page, app, room }) => {
// This test checks: // This test checks:
// 1. clickability of top left of view source event toggle // 1. clickability of top left of view source event toggle
// 2. clickability of view source toggle on IRC layout // 2. clickability of view source toggle on IRC layout
@ -712,7 +742,10 @@ test.describe("Timeline", () => {
).toBeVisible(); ).toBeVisible();
}); });
test("should render url previews", async ({ page, app, room, axe, checkA11y, context }) => { test(
"should render url previews",
{ tag: "@screenshot" },
async ({ page, app, room, axe, checkA11y, context }) => {
axe.disableRules("color-contrast"); axe.disableRules("color-contrast");
// Element Web uses a Service Worker to rewrite unauthenticated media requests to authenticated ones, but // Element Web uses a Service Worker to rewrite unauthenticated media requests to authenticated ones, but
@ -769,10 +802,14 @@ test.describe("Timeline", () => {
} }
`, `,
}); });
}); },
);
test.describe("on search results panel", () => { test.describe("on search results panel", () => {
test("should highlight search result words regardless of formatting", async ({ page, app, room }) => { test(
"should highlight search result words regardless of formatting",
{ tag: "@screenshot" },
async ({ page, app, room }) => {
await sendEvent(app.client, room.roomId); await sendEvent(app.client, room.roomId);
await sendEvent(app.client, room.roomId, true); await sendEvent(app.client, room.roomId, true);
await page.goto(`/#/room/${room.roomId}`); await page.goto(`/#/room/${room.roomId}`);
@ -792,9 +829,10 @@ test.describe("Timeline", () => {
await expect(page.locator(".mx_RoomView_searchResultsPanel")).toMatchScreenshot( await expect(page.locator(".mx_RoomView_searchResultsPanel")).toMatchScreenshot(
"highlighted-search-results.png", "highlighted-search-results.png",
); );
}); },
);
test("should render a fully opaque textual event", async ({ page, app, room }) => { test("should render a fully opaque textual event", { tag: "@screenshot" }, async ({ page, app, room }) => {
const stringToSearch = "Message"; // Same with string sent with sendEvent() const stringToSearch = "Message"; // Same with string sent with sendEvent()
await sendEvent(app.client, room.roomId); await sendEvent(app.client, room.roomId);
@ -918,7 +956,7 @@ test.describe("Timeline", () => {
).toHaveCount(4); ).toHaveCount(4);
}); });
test("should display a reply chain", async ({ page, app, room, homeserver }) => { test("should display a reply chain", { tag: "@screenshot" }, async ({ page, app, room, homeserver }) => {
const reply2 = "Reply again"; const reply2 = "Reply again";
await page.goto(`/#/room/${room.roomId}`); await page.goto(`/#/room/${room.roomId}`);
@ -1031,12 +1069,10 @@ test.describe("Timeline", () => {
); );
}); });
test("should send, reply, and display long strings without overflowing", async ({ test(
page, "should send, reply, and display long strings without overflowing",
app, { tag: "@screenshot" },
room, async ({ page, app, room, homeserver }) => {
homeserver,
}) => {
// Max 256 characters for display name // Max 256 characters for display name
const LONG_STRING = const LONG_STRING =
"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut " + "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut " +
@ -1146,7 +1182,8 @@ test.describe("Timeline", () => {
"long-strings-with-reply-bubble-layout.png", "long-strings-with-reply-bubble-layout.png",
screenshotOptions, screenshotOptions,
); );
}); },
);
async function testImageRendering(page: Page, app: ElementAppPage, room: { roomId: string }) { async function testImageRendering(page: Page, app: ElementAppPage, room: { roomId: string }) {
await app.viewRoomById(room.roomId); await app.viewRoomById(room.roomId);
@ -1176,7 +1213,7 @@ test.describe("Timeline", () => {
); );
} }
test("should render images in the timeline", async ({ page, app, room, context }) => { test("should render images in the timeline", { tag: "@screenshot" }, async ({ page, app, room, context }) => {
await testImageRendering(page, app, room); await testImageRendering(page, app, room);
}); });
@ -1188,7 +1225,10 @@ test.describe("Timeline", () => {
// In practice, this means this test will *always* succeed because it ends up relying on fallback behaviour tested // In practice, this means this test will *always* succeed because it ends up relying on fallback behaviour tested
// above (unless of course the above tests are also broken). // above (unless of course the above tests are also broken).
test.describe("MSC3916 - Authenticated Media", () => { test.describe("MSC3916 - Authenticated Media", () => {
test("should render authenticated images in the timeline", async ({ page, app, room, context }) => { test(
"should render authenticated images in the timeline",
{ tag: "@screenshot" },
async ({ page, app, room, context }) => {
// Note: we have to use `context` instead of `page` for routing, otherwise we'll miss Service Worker events. // Note: we have to use `context` instead of `page` for routing, otherwise we'll miss Service Worker events.
// See https://playwright.dev/docs/service-workers-experimental#network-events-and-routing // See https://playwright.dev/docs/service-workers-experimental#network-events-and-routing
@ -1231,7 +1271,8 @@ test.describe("Timeline", () => {
// We check the same screenshot because there should be no user-visible impact to using authentication. // We check the same screenshot because there should be no user-visible impact to using authentication.
await testImageRendering(page, app, room); await testImageRendering(page, app, room);
}); },
);
}); });
}); });
}); });

View file

@ -11,7 +11,7 @@ import { test, expect } from "../../element-web-test";
test.describe("User Menu", () => { test.describe("User Menu", () => {
test.use({ displayName: "Jeff" }); test.use({ displayName: "Jeff" });
test("should contain our name & userId", async ({ page, user }) => { test("should contain our name & userId", { tag: "@screenshot" }, async ({ page, user }) => {
await page.getByRole("button", { name: "User menu", exact: true }).click(); await page.getByRole("button", { name: "User menu", exact: true }).click();
const menu = page.getByRole("menu"); const menu = page.getByRole("menu");

View file

@ -26,7 +26,7 @@ test.describe("User Onboarding (new user)", () => {
await expect(page.locator(".mx_UserOnboardingList")).toBeVisible(); await expect(page.locator(".mx_UserOnboardingList")).toBeVisible();
}); });
test("page is shown and preference exists", async ({ page, app }) => { test("page is shown and preference exists", { tag: "@screenshot" }, async ({ page, app }) => {
await expect(page.locator(".mx_UserOnboardingPage")).toMatchScreenshot( await expect(page.locator(".mx_UserOnboardingPage")).toMatchScreenshot(
"User-Onboarding-new-user-page-is-shown-and-preference-exists-1.png", "User-Onboarding-new-user-page-is-shown-and-preference-exists-1.png",
); );
@ -34,7 +34,7 @@ test.describe("User Onboarding (new user)", () => {
await expect(page.getByText("Show shortcut to welcome checklist above the room list")).toBeVisible(); await expect(page.getByText("Show shortcut to welcome checklist above the room list")).toBeVisible();
}); });
test("app download dialog", async ({ page }) => { test("app download dialog", { tag: "@screenshot" }, async ({ page }) => {
await page.getByRole("button", { name: "Download apps" }).click(); await page.getByRole("button", { name: "Download apps" }).click();
await expect( await expect(
page.getByRole("dialog").getByRole("heading", { level: 1, name: "Download Element" }), page.getByRole("dialog").getByRole("heading", { level: 1, name: "Download Element" }),

View file

@ -14,7 +14,7 @@ test.describe("UserView", () => {
botCreateOpts: { displayName: "Usman" }, botCreateOpts: { displayName: "Usman" },
}); });
test("should render the user view as expected", async ({ page, homeserver, user, bot }) => { test("should render the user view as expected", { tag: "@screenshot" }, async ({ page, homeserver, user, bot }) => {
await page.goto(`/#/user/${bot.credentials.userId}`); await page.goto(`/#/user/${bot.credentials.userId}`);
const rightPanel = page.locator("#mx_RightPanel"); const rightPanel = page.locator("#mx_RightPanel");

View file

@ -70,7 +70,7 @@ test.describe("Widget Layout", () => {
await app.viewRoomByName(ROOM_NAME); await app.viewRoomByName(ROOM_NAME);
}); });
test("should be set properly", async ({ page }) => { test("should be set properly", { tag: "@screenshot" }, async ({ page }) => {
await expect(page.locator(".mx_AppsDrawer")).toMatchScreenshot("apps-drawer.png"); await expect(page.locator(".mx_AppsDrawer")).toMatchScreenshot("apps-drawer.png");
}); });

View file

@ -314,6 +314,10 @@ export const expect = baseExpect.extend({
const testInfo = test.info(); const testInfo = test.info();
if (!testInfo) throw new Error(`toMatchScreenshot() must be called during the test`); if (!testInfo) throw new Error(`toMatchScreenshot() must be called during the test`);
if (!testInfo.tags.includes("@screenshot")) {
throw new Error("toMatchScreenshot() must be used in a test tagged with @screenshot");
}
const page = "page" in receiver ? receiver.page() : receiver; const page = "page" in receiver ? receiver.page() : receiver;
let css = ` let css = `

View file

@ -20,7 +20,7 @@ import { randB64Bytes } from "../../utils/rand";
// Docker tag to use for synapse docker image. // Docker tag to use for synapse docker image.
// We target a specific digest as every now and then a Synapse update will break our CI. // We target a specific digest as every now and then a Synapse update will break our CI.
// This digest is updated by the playwright-image-updates.yaml workflow periodically. // This digest is updated by the playwright-image-updates.yaml workflow periodically.
const DOCKER_TAG = "develop@sha256:34da08a44994e0ad2def7ed5f28c3cc7a2f7ead9722f4ae87b23030f59384ea5"; const DOCKER_TAG = "develop@sha256:7e5aa044c65d67382f29acd424fc5eae447a951114be8b650e322145ba65d75f";
async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise<Omit<HomeserverConfig, "dockerUrl">> { async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise<Omit<HomeserverConfig, "dockerUrl">> {
const templateDir = path.join(__dirname, "templates", opts.template); const templateDir = path.join(__dirname, "templates", opts.template);

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -596,7 +596,7 @@ legend {
.mx_Dialog .mx_Dialog
button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not(
.mx_UserProfileSettings button .mx_UserProfileSettings button
):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button), ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button),
.mx_Dialog input[type="submit"], .mx_Dialog input[type="submit"],
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton), .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton),
.mx_Dialog_buttons input[type="submit"] { .mx_Dialog_buttons input[type="submit"] {
@ -616,14 +616,16 @@ legend {
.mx_Dialog .mx_Dialog
button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not(
.mx_UserProfileSettings button .mx_UserProfileSettings button
):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):last-child { ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(
.mx_ShareDialog button
):last-child {
margin-right: 0px; margin-right: 0px;
} }
.mx_Dialog .mx_Dialog
button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not(
.mx_UserProfileSettings button .mx_UserProfileSettings button
):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):focus, ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button):focus,
.mx_Dialog input[type="submit"]:focus, .mx_Dialog input[type="submit"]:focus,
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):focus, .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):focus,
.mx_Dialog_buttons input[type="submit"]:focus { .mx_Dialog_buttons input[type="submit"]:focus {
@ -635,7 +637,7 @@ legend {
.mx_Dialog_buttons .mx_Dialog_buttons
button.mx_Dialog_primary:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not( button.mx_Dialog_primary:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not(
.mx_UserProfileSettings button .mx_UserProfileSettings button
):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button), ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button),
.mx_Dialog_buttons input[type="submit"].mx_Dialog_primary { .mx_Dialog_buttons input[type="submit"].mx_Dialog_primary {
color: var(--cpd-color-text-on-solid-primary); color: var(--cpd-color-text-on-solid-primary);
background-color: var(--cpd-color-bg-action-primary-rest); background-color: var(--cpd-color-bg-action-primary-rest);
@ -648,7 +650,7 @@ legend {
.mx_Dialog_buttons .mx_Dialog_buttons
button.danger:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not(.mx_UserProfileSettings button):not( button.danger:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not(.mx_UserProfileSettings button):not(
.mx_ThemeChoicePanel_CustomTheme button .mx_ThemeChoicePanel_CustomTheme button
):not(.mx_UnpinAllDialog button), ):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button),
.mx_Dialog_buttons input[type="submit"].danger { .mx_Dialog_buttons input[type="submit"].danger {
background-color: var(--cpd-color-bg-critical-primary); background-color: var(--cpd-color-bg-critical-primary);
border: solid 1px var(--cpd-color-bg-critical-primary); border: solid 1px var(--cpd-color-bg-critical-primary);
@ -664,7 +666,7 @@ legend {
.mx_Dialog .mx_Dialog
button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not(
.mx_UserProfileSettings button .mx_UserProfileSettings button
):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):disabled, ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button):disabled,
.mx_Dialog input[type="submit"]:disabled, .mx_Dialog input[type="submit"]:disabled,
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):disabled, .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):disabled,
.mx_Dialog_buttons input[type="submit"]:disabled { .mx_Dialog_buttons input[type="submit"]:disabled {

View file

@ -393,9 +393,3 @@
@import "./views/voip/_LegacyCallViewHeader.pcss"; @import "./views/voip/_LegacyCallViewHeader.pcss";
@import "./views/voip/_LegacyCallViewSidebar.pcss"; @import "./views/voip/_LegacyCallViewSidebar.pcss";
@import "./views/voip/_VideoFeed.pcss"; @import "./views/voip/_VideoFeed.pcss";
@import "./voice-broadcast/atoms/_LiveBadge.pcss";
@import "./voice-broadcast/atoms/_VoiceBroadcastControl.pcss";
@import "./voice-broadcast/atoms/_VoiceBroadcastHeader.pcss";
@import "./voice-broadcast/atoms/_VoiceBroadcastRecordingConnectionError.pcss";
@import "./voice-broadcast/atoms/_VoiceBroadcastRoomSubtitle.pcss";
@import "./voice-broadcast/molecules/_VoiceBroadcastBody.pcss";

View file

@ -22,20 +22,6 @@ Please see LICENSE files in the repository root for full details.
pointer-events: none; /* makes the avatar non-draggable */ pointer-events: none; /* makes the avatar non-draggable */
} }
} }
.mx_UserMenu_userAvatarLive {
align-items: center;
background-color: $alert;
border-radius: 6px;
color: $live-badge-color;
display: flex;
height: 12px;
justify-content: center;
left: 25px;
position: absolute;
top: 20px;
width: 12px;
}
} }
.mx_UserMenu_contextMenuButton { .mx_UserMenu_contextMenuButton {

View file

@ -5,50 +5,73 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
.mx_ShareDialog hr { .mx_ShareDialog {
margin-top: 25px; /* Value from figma design */
margin-bottom: 25px; width: 416px;
border-color: $light-fg-color;
}
.mx_ShareDialog .mx_ShareDialog_content { .mx_Dialog_header {
margin: 10px 0; text-align: center;
margin-bottom: var(--cpd-space-6x);
/* Override dialog header padding to able to center it */
padding-inline-end: 0;
}
.mx_CopyableText { .mx_ShareDialog_content {
width: unset; /* full width */ display: flex;
flex-direction: column;
gap: var(--cpd-space-6x);
align-items: center;
> a { .mx_ShareDialog_top {
text-decoration: none; display: flex;
flex-shrink: 1; flex-direction: column;
overflow: hidden; gap: var(--cpd-space-4x);
align-items: center;
width: 100%;
span {
text-align: center;
font: var(--cpd-font-body-sm-semibold);
color: var(--cpd-color-text-secondary);
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
overflow: hidden;
width: 100%;
} }
} }
}
.mx_ShareDialog_split { label {
display: inline-flex;
gap: var(--cpd-space-3x);
justify-content: center;
align-items: center;
font: var(--cpd-font-body-md-medium);
}
button {
width: 100%;
}
.mx_ShareDialog_social {
display: flex; display: flex;
flex-wrap: wrap; gap: var(--cpd-space-3x);
} justify-content: center;
.mx_ShareDialog_qrcode_container { a {
float: left; width: 48px;
height: 256px; height: 48px;
width: 256px; border-radius: 99px;
margin-right: 64px; box-sizing: border-box;
} border: 1px solid var(--cpd-color-border-interactive-secondary);
display: flex;
justify-content: center;
align-items: center;
.mx_ShareDialog_qrcode_container + .mx_ShareDialog_social_container { img {
width: 299px; width: 24px;
} height: 24px;
}
.mx_ShareDialog_social_container { }
display: inline-block; }
} }
.mx_ShareDialog_social_icon {
display: inline-grid;
margin-right: 10px;
margin-bottom: 10px;
} }

View file

@ -256,10 +256,6 @@ Please see LICENSE files in the repository root for full details.
mask-image: url("@vector-im/compound-design-tokens/icons/mic-on-solid.svg"); mask-image: url("@vector-im/compound-design-tokens/icons/mic-on-solid.svg");
} }
.mx_MessageComposer_voiceBroadcast::before {
mask-image: url("$(res)/img/element-icons/live.svg");
}
.mx_MessageComposer_plain_text::before { .mx_MessageComposer_plain_text::before {
mask-image: url("$(res)/img/element-icons/room/composer/plain_text.svg"); mask-image: url("$(res)/img/element-icons/room/composer/plain_text.svg");
} }

View file

@ -1,23 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
.mx_LiveBadge {
align-items: center;
background-color: $alert;
border-radius: 2px;
color: $live-badge-color;
display: inline-flex;
font-size: $font-12px;
font-weight: var(--cpd-font-weight-semibold);
gap: $spacing-4;
padding: 2px 4px;
}
.mx_LiveBadge--grey {
background-color: $quaternary-content;
}

View file

@ -1,28 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
.mx_VoiceBroadcastControl {
align-items: center;
background-color: $background;
border-radius: 50%;
color: $secondary-content;
display: flex;
flex: 0 0 32px;
height: 32px;
justify-content: center;
width: 32px;
}
.mx_VoiceBroadcastControl-recording {
color: $alert;
}
.mx_VoiceBroadcastControl-play .mx_Icon {
left: 1px;
position: relative;
}

View file

@ -1,60 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
.mx_VoiceBroadcastHeader {
align-items: flex-start;
display: flex;
gap: $spacing-8;
line-height: 20px;
margin-bottom: $spacing-16;
min-width: 0;
}
.mx_VoiceBroadcastHeader_content {
flex-grow: 1;
min-width: 0;
}
.mx_VoiceBroadcastHeader_room_wrapper {
align-items: center;
display: flex;
gap: 4px;
justify-content: flex-start;
}
.mx_VoiceBroadcastHeader_room {
font-size: $font-12px;
font-weight: var(--cpd-font-weight-semibold);
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mx_VoiceBroadcastHeader_line {
align-items: center;
color: $secondary-content;
font-size: $font-12px;
display: flex;
gap: $spacing-4;
.mx_Spinner {
flex: 0 0 14px;
padding: 1px;
}
span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.mx_VoiceBroadcastHeader_mic--clickable {
cursor: pointer;
}

View file

@ -1,18 +0,0 @@
/*
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
Please see LICENSE files in the repository root for full details.
*/
.mx_VoiceBroadcastRecordingConnectionError {
align-items: center;
color: $alert;
display: flex;
gap: $spacing-12;
svg path {
fill: $alert;
}
}

View file

@ -1,14 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
.mx_RoomTile .mx_RoomTile_titleContainer .mx_RoomTile_subtitle.mx_RoomTile_subtitle--voice-broadcast {
align-items: center;
color: $alert;
display: flex;
gap: $spacing-4;
}

View file

@ -1,75 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
.mx_VoiceBroadcastBody {
background-color: $quinary-content;
border-radius: 8px;
color: $secondary-content;
display: inline-block;
font-size: $font-12px;
padding: $spacing-12;
width: 271px;
.mx_Clock {
line-height: 1;
}
}
.mx_VoiceBroadcastBody--pip {
background-color: $system;
box-shadow: 0 2px 8px 0 #0000004a;
}
.mx_VoiceBroadcastBody--small {
display: flex;
gap: $spacing-8;
width: 192px;
.mx_VoiceBroadcastHeader {
margin-bottom: 0;
}
.mx_VoiceBroadcastControl {
align-self: center;
}
.mx_LiveBadge {
margin-top: 4px;
}
}
.mx_VoiceBroadcastBody_divider {
background-color: $quinary-content;
border: 0;
height: 1px;
margin: $spacing-12 0;
}
.mx_VoiceBroadcastBody_controls {
align-items: center;
display: flex;
gap: $spacing-32;
justify-content: center;
margin-bottom: $spacing-8;
}
.mx_VoiceBroadcastBody_timerow {
display: flex;
justify-content: space-between;
}
.mx_AccessibleButton.mx_VoiceBroadcastBody_blockButton {
display: flex;
gap: $spacing-8;
}
.mx_VoiceBroadcastBody__small-close {
right: 8px;
position: absolute;
top: 8px;
}

View file

@ -240,11 +240,6 @@ $location-live-secondary-color: #deddfd;
} }
/* ******************** */ /* ******************** */
/* Voice Broadcast */
/* ******************** */
$live-badge-color: #ffffff;
/* ******************** */
/* One-off colors */ /* One-off colors */
/* ******************** */ /* ******************** */
$progressbar-bg-color: var(--cpd-color-gray-200); $progressbar-bg-color: var(--cpd-color-gray-200);

View file

@ -226,11 +226,6 @@ $location-live-color: #5c56f5;
$location-live-secondary-color: #deddfd; $location-live-secondary-color: #deddfd;
/* ******************** */ /* ******************** */
/* Voice Broadcast */
/* ******************** */
$live-badge-color: #ffffff;
/* ******************** */
body { body {
color-scheme: dark; color-scheme: dark;
} }

View file

@ -325,11 +325,6 @@ $location-live-color: #5c56f5;
$location-live-secondary-color: #deddfd; $location-live-secondary-color: #deddfd;
/* ******************** */ /* ******************** */
/* Voice Broadcast */
/* ******************** */
$live-badge-color: #ffffff;
/* ******************** */
body { body {
color-scheme: light; color-scheme: light;
} }

View file

@ -143,3 +143,21 @@ $inter-unicode-range: U+0000-20e2, U+20e4-23ce, U+23d0-24c1, U+24c3-259f, U+25c2
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC,
U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
} }
/* Twemoji COLR */
@font-face {
font-family: "Twemoji";
font-weight: 400;
src: url("$(res)/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2") format("woff2");
}
/* For at least Chrome on Windows 10, we have to explictly add extra weights for the emoji to appear in bold messages, etc. */
@font-face {
font-family: "Twemoji";
font-weight: 600;
src: url("$(res)/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2") format("woff2");
}
@font-face {
font-family: "Twemoji";
font-weight: 700;
src: url("$(res)/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2") format("woff2");
}

View file

@ -10,8 +10,8 @@
/* Noto Color Emoji contains digits, in fixed-width, therefore causing /* Noto Color Emoji contains digits, in fixed-width, therefore causing
digits in flowed text to stand out. digits in flowed text to stand out.
TODO: Consider putting all emoji fonts to the end rather than the front. */ TODO: Consider putting all emoji fonts to the end rather than the front. */
$font-family: "Inter", var(--emoji-font-family), "Apple Color Emoji", "Segoe UI Emoji", "Arial", "Helvetica", sans-serif, $font-family: "Inter", var(--emoji-font-family), "Apple Color Emoji", "Segoe UI Emoji", "Arial", "Helvetica",
"Noto Color Emoji"; sans-serif, "Noto Color Emoji";
$monospace-font-family: "Inconsolata", var(--emoji-font-family), "Apple Color Emoji", "Segoe UI Emoji", "Courier", $monospace-font-family: "Inconsolata", var(--emoji-font-family), "Apple Color Emoji", "Segoe UI Emoji", "Courier",
monospace, "Noto Color Emoji"; monospace, "Noto Color Emoji";
@ -355,11 +355,6 @@ $location-live-color: var(--cpd-color-purple-900);
$location-live-secondary-color: var(--cpd-color-purple-600); $location-live-secondary-color: var(--cpd-color-purple-600);
/* ******************** */ /* ******************** */
/* Voice Broadcast */
/* ******************** */
$live-badge-color: var(--cpd-color-icon-on-solid-primary);
/* ******************** */
body { body {
color-scheme: light; color-scheme: light;
} }

View file

@ -1,47 +0,0 @@
#!/usr/bin/env python
import json
import sys
import os
if len(sys.argv) < 3:
print "Usage: %s <source> <dest>" % (sys.argv[0],)
print "eg. %s pt_BR.json pt.json" % (sys.argv[0],)
print
print "Adds any translations to <dest> that exist in <source> but not <dest>"
sys.exit(1)
srcpath = sys.argv[1]
dstpath = sys.argv[2]
tmppath = dstpath + ".tmp"
with open(srcpath) as f:
src = json.load(f)
with open(dstpath) as f:
dst = json.load(f)
toAdd = {}
for k,v in src.iteritems():
if k not in dst:
print "Adding %s" % (k,)
toAdd[k] = v
# don't just json.dumps as we'll probably re-order all the keys (and they're
# not in any given order so we can't just sort_keys). Append them to the end.
with open(dstpath) as ifp:
with open(tmppath, 'w') as ofp:
for line in ifp:
strippedline = line.strip()
if strippedline in ('{', '}'):
ofp.write(line)
elif strippedline.endswith(','):
ofp.write(line)
else:
ofp.write(' '+strippedline+',')
toAddStr = json.dumps(toAdd, indent=4, separators=(',', ': '), ensure_ascii=False, encoding="utf8").strip("{}\n")
ofp.write("\n")
ofp.write(toAddStr.encode('utf8'))
ofp.write("\n")
os.rename(tmppath, dstpath)

View file

@ -1,84 +0,0 @@
#!/usr/bin/env bash
# Fetches the js-sdk dependency for development or testing purposes
# If there exists a branch of that dependency with the same name as
# the branch the current checkout is on, use that branch. Otherwise,
# use develop.
set -x
GIT_CLONE_ARGS=("$@")
[ -z "$defbranch" ] && defbranch="develop"
# clone a specific branch of a github repo
function clone() {
org=$1
repo=$2
branch=$3
# Chop 'origin' off the start as jenkins ends up using
# branches on the origin, but this doesn't work if we
# specify the branch when cloning.
branch=${branch#origin/}
if [ -n "$branch" ]
then
echo "Trying to use $org/$repo#$branch"
# Disable auth prompts: https://serverfault.com/a/665959
GIT_TERMINAL_PROMPT=0 git clone https://github.com/$org/$repo.git $repo --branch $branch \
"${GIT_CLONE_ARGS[@]}"
return $?
fi
return 1
}
function dodep() {
deforg=$1
defrepo=$2
rm -rf $defrepo
# Try the PR author's branch in case it exists on the deps as well.
# Try the target branch of the push or PR.
# Use the default branch as the last resort.
if [[ "$BUILDKITE" == true ]]; then
# If BUILDKITE_BRANCH is set, it will contain either:
# * "branch" when the author's branch and target branch are in the same repo
# * "author:branch" when the author's branch is in their fork
# We can split on `:` into an array to check.
BUILDKITE_BRANCH_ARRAY=(${BUILDKITE_BRANCH//:/ })
if [[ "${#BUILDKITE_BRANCH_ARRAY[@]}" == "2" ]]; then
prAuthor=${BUILDKITE_BRANCH_ARRAY[0]}
prBranch=${BUILDKITE_BRANCH_ARRAY[1]}
else
prAuthor=$deforg
prBranch=$BUILDKITE_BRANCH
fi
clone $prAuthor $defrepo $prBranch ||
clone $deforg $defrepo $BUILDKITE_PULL_REQUEST_BASE_BRANCH ||
clone $deforg $defrepo $defbranch ||
return $?
else
clone $deforg $defrepo $ghprbSourceBranch ||
clone $deforg $defrepo $GIT_BRANCH ||
clone $deforg $defrepo `git rev-parse --abbrev-ref HEAD` ||
clone $deforg $defrepo $defbranch ||
return $?
fi
echo "$defrepo set to branch "`git -C "$defrepo" rev-parse --abbrev-ref HEAD`
}
##############################
echo 'Setting up matrix-js-sdk'
dodep matrix-org matrix-js-sdk
pushd matrix-js-sdk
yarn link
yarn install --frozen-lockfile
popd
yarn link matrix-js-sdk
##############################

View file

@ -1,64 +0,0 @@
# Copyright 2017-2024 New Vector Ltd.
# SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
# Please see LICENSE in the repository root for full details.
# genflags.sh - Generates pngs for use with CountryDropdown.js
#
# Dependencies:
# - imagemagick --with-rsvg (because default imagemagick SVG
# renderer does not produce accurate results)
#
# on macOS, this is most easily done with:
# brew install imagemagick --with-librsvg
#
# This will clone the googlei18n flag repo before converting
# all phonenumber.js-supported country flags (as SVGs) into
# PNGs that can be used by CountryDropdown.js.
set -e
# Allow CTRL+C to terminate the script
trap "echo Exited!; exit;" SIGINT SIGTERM
# git clone the google repo to get flag SVGs
git clone git@github.com:googlei18n/region-flags
for f in region-flags/svg/*.svg; do
# Skip state flags
if [[ $f =~ [A-Z]{2}-[A-Z]{2,3}.svg ]] ; then
echo "Skipping state flag "$f
continue
fi
# Skip countries not included in phonenumber.js
if [[ $f =~ (AC|CP|DG|EA|EU|IC|TA|UM|UN|XK).svg ]] ; then
echo "Skipping non-phonenumber supported flag "$f
continue
fi
# Run imagemagick convert
# -background none : transparent background
# -resize 50x30 : resize the flag to have a height of 15px (2x)
# By default, aspect ratio is respected so the width will
# be correct and not necessarily 25px.
# -filter Lanczos : use sharper resampling to avoid muddiness
# -gravity Center : keep the image central when adding an -extent
# -border 1 : add a 1px border around the flag
# -bordercolor : set the border colour
# -extent 54x54 : surround the image with padding so that it
# has the dimensions 27x27px (2x).
convert $f -background none -filter Lanczos -resize 50x30 \
-gravity Center -border 1 -bordercolor \#e0e0e0 \
-extent 54x54 $f.png
# $f.png will be region-flags/svg/XX.svg.png at this point
# Extract filename from path $f
newname=${f##*/}
# Replace .svg with .png
newname=${newname%.svg}.png
# Move the file to flags directory
mv $f.png ../res/flags/$newname
echo "Generated res/flags/"$newname
done

View file

@ -10,7 +10,6 @@ import type { IWidget } from "matrix-widget-api";
import type { BLURHASH_FIELD } from "../utils/image-media"; import type { BLURHASH_FIELD } from "../utils/image-media";
import type { JitsiCallMemberEventType, JitsiCallMemberContent } from "../call-types"; import type { JitsiCallMemberEventType, JitsiCallMemberContent } from "../call-types";
import type { ILayoutStateEvent, WIDGET_LAYOUT_EVENT_TYPE } from "../stores/widgets/types"; import type { ILayoutStateEvent, WIDGET_LAYOUT_EVENT_TYPE } from "../stores/widgets/types";
import type { VoiceBroadcastInfoEventContent, VoiceBroadcastInfoEventType } from "../voice-broadcast/types";
import type { EncryptedFile } from "matrix-js-sdk/src/types"; import type { EncryptedFile } from "matrix-js-sdk/src/types";
// Extend Matrix JS SDK types via Typescript declaration merging to support unspecced event fields and types // Extend Matrix JS SDK types via Typescript declaration merging to support unspecced event fields and types
@ -37,9 +36,6 @@ declare module "matrix-js-sdk/src/types" {
"im.vector.modular.widgets": IWidget | {}; "im.vector.modular.widgets": IWidget | {};
[WIDGET_LAYOUT_EVENT_TYPE]: ILayoutStateEvent; [WIDGET_LAYOUT_EVENT_TYPE]: ILayoutStateEvent;
// Unstable voice broadcast state events
[VoiceBroadcastInfoEventType]: VoiceBroadcastInfoEventContent;
// Element custom state events // Element custom state events
"im.vector.web.settings": Record<string, any>; "im.vector.web.settings": Record<string, any>;
"org.matrix.room.preview_urls": { disable: boolean }; "org.matrix.room.preview_urls": { disable: boolean };
@ -78,7 +74,5 @@ declare module "matrix-js-sdk/src/types" {
waveform?: number[]; waveform?: number[];
}; };
"org.matrix.msc3245.voice"?: {}; "org.matrix.msc3245.voice"?: {};
"io.element.voice_broadcast_chunk"?: { sequence: number };
} }
} }

View file

@ -175,13 +175,6 @@ export interface IConfigOptions {
sync_timeline_limit?: number; sync_timeline_limit?: number;
dangerously_allow_unsafe_and_insecure_passwords?: boolean; // developer option dangerously_allow_unsafe_and_insecure_passwords?: boolean; // developer option
voice_broadcast?: {
// length per voice chunk in seconds
chunk_length?: number;
// max voice broadcast length in seconds
max_length?: number;
};
user_notice?: { user_notice?: {
title: string; title: string;
description: string; description: string;

View file

@ -55,8 +55,6 @@ import { OpenInviteDialogPayload } from "./dispatcher/payloads/OpenInviteDialogP
import { findDMForUser } from "./utils/dm/findDMForUser"; import { findDMForUser } from "./utils/dm/findDMForUser";
import { getJoinedNonFunctionalMembers } from "./utils/room/getJoinedNonFunctionalMembers"; import { getJoinedNonFunctionalMembers } from "./utils/room/getJoinedNonFunctionalMembers";
import { localNotificationsAreSilenced } from "./utils/notifications"; import { localNotificationsAreSilenced } from "./utils/notifications";
import { SdkContextClass } from "./contexts/SDKContext";
import { showCantStartACallDialog } from "./voice-broadcast/utils/showCantStartACallDialog";
import { isNotNull } from "./Typeguards"; import { isNotNull } from "./Typeguards";
import { BackgroundAudio } from "./audio/BackgroundAudio"; import { BackgroundAudio } from "./audio/BackgroundAudio";
import { Jitsi } from "./widgets/Jitsi.ts"; import { Jitsi } from "./widgets/Jitsi.ts";
@ -859,15 +857,6 @@ export default class LegacyCallHandler extends EventEmitter {
return; return;
} }
// Pause current broadcast, if any
SdkContextClass.instance.voiceBroadcastPlaybacksStore.getCurrent()?.pause();
if (SdkContextClass.instance.voiceBroadcastRecordingsStore.getCurrent()) {
// Do not start a call, if recording a broadcast
showCantStartACallDialog();
return;
}
// We might be using managed hybrid widgets // We might be using managed hybrid widgets
if (isManagedHybridWidgetEnabled(room)) { if (isManagedHybridWidgetEnabled(room)) {
await addManagedHybridWidget(room); await addManagedHybridWidget(room);

View file

@ -35,13 +35,11 @@ import IdentityAuthClient from "./IdentityAuthClient";
import { crossSigningCallbacks } from "./SecurityManager"; import { crossSigningCallbacks } from "./SecurityManager";
import { SlidingSyncManager } from "./SlidingSyncManager"; import { SlidingSyncManager } from "./SlidingSyncManager";
import { _t, UserFriendlyError } from "./languageHandler"; import { _t, UserFriendlyError } from "./languageHandler";
import { SettingLevel } from "./settings/SettingLevel";
import MatrixClientBackedController from "./settings/controllers/MatrixClientBackedController"; import MatrixClientBackedController from "./settings/controllers/MatrixClientBackedController";
import ErrorDialog from "./components/views/dialogs/ErrorDialog"; import ErrorDialog from "./components/views/dialogs/ErrorDialog";
import PlatformPeg from "./PlatformPeg"; import PlatformPeg from "./PlatformPeg";
import { formatList } from "./utils/FormattingUtils"; import { formatList } from "./utils/FormattingUtils";
import SdkConfig from "./SdkConfig"; import SdkConfig from "./SdkConfig";
import { Features } from "./settings/Settings";
import { setDeviceIsolationMode } from "./settings/controllers/DeviceIsolationModeController.ts"; import { setDeviceIsolationMode } from "./settings/controllers/DeviceIsolationModeController.ts";
export interface IMatrixClientCreds { export interface IMatrixClientCreds {
@ -333,11 +331,6 @@ class MatrixClientPegClass implements IMatrixClientPeg {
logger.error("Warning! Not using an encryption key for rust crypto store."); logger.error("Warning! Not using an encryption key for rust crypto store.");
} }
// Record the fact that we used the Rust crypto stack with this client. This just guards against people
// rolling back to versions of EW that did not default to Rust crypto (which would lead to an error, since
// we cannot migrate from Rust to Legacy crypto).
await SettingsStore.setValue(Features.RustCrypto, null, SettingLevel.DEVICE, true);
await this.matrixClient.initRustCrypto({ await this.matrixClient.initRustCrypto({
storageKey: rustCryptoStoreKey, storageKey: rustCryptoStoreKey,
storagePassword: rustCryptoStorePassword, storagePassword: rustCryptoStorePassword,

View file

@ -49,8 +49,6 @@ import { SdkContextClass } from "./contexts/SDKContext";
import { localNotificationsAreSilenced, createLocalNotificationSettingsIfNeeded } from "./utils/notifications"; import { localNotificationsAreSilenced, createLocalNotificationSettingsIfNeeded } from "./utils/notifications";
import { getIncomingCallToastKey, IncomingCallToast } from "./toasts/IncomingCallToast"; import { getIncomingCallToastKey, IncomingCallToast } from "./toasts/IncomingCallToast";
import ToastStore from "./stores/ToastStore"; import ToastStore from "./stores/ToastStore";
import { VoiceBroadcastChunkEventType, VoiceBroadcastInfoEventType } from "./voice-broadcast";
import { getSenderName } from "./utils/event/getSenderName";
import { stripPlainReply } from "./utils/Reply"; import { stripPlainReply } from "./utils/Reply";
import { BackgroundAudio } from "./audio/BackgroundAudio"; import { BackgroundAudio } from "./audio/BackgroundAudio";
@ -81,17 +79,6 @@ const msgTypeHandlers: Record<string, (event: MatrixEvent) => string | null> = {
return TextForEvent.textForLocationEvent(event)(); return TextForEvent.textForLocationEvent(event)();
}, },
[MsgType.Audio]: (event: MatrixEvent): string | null => { [MsgType.Audio]: (event: MatrixEvent): string | null => {
if (event.getContent()?.[VoiceBroadcastChunkEventType]) {
if (event.getContent()?.[VoiceBroadcastChunkEventType]?.sequence === 1) {
// Show a notification for the first broadcast chunk.
// At this point a user received something to listen to.
return _t("notifier|io.element.voice_broadcast_chunk", { senderName: getSenderName(event) });
}
// Mute other broadcast chunks
return null;
}
return TextForEvent.textForEvent(event, MatrixClientPeg.safeGet()); return TextForEvent.textForEvent(event, MatrixClientPeg.safeGet());
}, },
}; };
@ -460,8 +447,6 @@ class NotifierClass extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents
// XXX: exported for tests // XXX: exported for tests
public evaluateEvent(ev: MatrixEvent): void { public evaluateEvent(ev: MatrixEvent): void {
// Mute notifications for broadcast info events
if (ev.getType() === VoiceBroadcastInfoEventType) return;
let roomId = ev.getRoomId()!; let roomId = ev.getRoomId()!;
if (LegacyCallHandler.instance.getSupportsVirtualRooms()) { if (LegacyCallHandler.instance.getSupportsVirtualRooms()) {
// Attempt to translate a virtual room to a native one // Attempt to translate a virtual room to a native one

View file

@ -46,10 +46,6 @@ export const DEFAULTS: DeepReadonly<IConfigOptions> = {
logo: require("../res/img/element-desktop-logo.svg").default, logo: require("../res/img/element-desktop-logo.svg").default,
url: "https://element.io/get-started", url: "https://element.io/get-started",
}, },
voice_broadcast: {
chunk_length: 2 * 60, // two minutes
max_length: 4 * 60 * 60, // four hours
},
feedback: { feedback: {
existing_issues_url: existing_issues_url:

View file

@ -7,8 +7,8 @@ Please see LICENSE files in the repository root for full details.
*/ */
import { lazy } from "react"; import { lazy } from "react";
import { ICryptoCallbacks, SecretStorage } from "matrix-js-sdk/src/matrix"; import { SecretStorage } from "matrix-js-sdk/src/matrix";
import { deriveRecoveryKeyFromPassphrase, decodeRecoveryKey } from "matrix-js-sdk/src/crypto-api"; import { deriveRecoveryKeyFromPassphrase, decodeRecoveryKey, CryptoCallbacks } from "matrix-js-sdk/src/crypto-api";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import Modal from "./Modal"; import Modal from "./Modal";
@ -159,7 +159,7 @@ function cacheSecretStorageKey(
} }
} }
export const crossSigningCallbacks: ICryptoCallbacks = { export const crossSigningCallbacks: CryptoCallbacks = {
getSecretStorageKey, getSecretStorageKey,
cacheSecretStorageKey, cacheSecretStorageKey,
}; };

View file

@ -49,7 +49,6 @@ import VoipUserMapper from "./VoipUserMapper";
import { htmlSerializeFromMdIfNeeded } from "./editor/serialize"; import { htmlSerializeFromMdIfNeeded } from "./editor/serialize";
import { leaveRoomBehaviour } from "./utils/leave-behaviour"; import { leaveRoomBehaviour } from "./utils/leave-behaviour";
import { MatrixClientPeg } from "./MatrixClientPeg"; import { MatrixClientPeg } from "./MatrixClientPeg";
import { getDeviceCryptoInfo } from "./utils/crypto/deviceInfo";
import { isCurrentLocalRoom, reject, singleMxcUpload, success, successSync } from "./slash-commands/utils"; import { isCurrentLocalRoom, reject, singleMxcUpload, success, successSync } from "./slash-commands/utils";
import { deop, op } from "./slash-commands/op"; import { deop, op } from "./slash-commands/op";
import { CommandCategories } from "./slash-commands/interface"; import { CommandCategories } from "./slash-commands/interface";
@ -658,69 +657,6 @@ export const Commands = [
category: CommandCategories.admin, category: CommandCategories.admin,
renderingTypes: [TimelineRenderingType.Room], renderingTypes: [TimelineRenderingType.Room],
}), }),
new Command({
command: "verify",
args: "<user-id> <device-id> <device-signing-key>",
description: _td("slash_command|verify"),
runFn: function (cli, roomId, threadId, args) {
if (args) {
const matches = args.match(/^(\S+) +(\S+) +(\S+)$/);
if (matches) {
const userId = matches[1];
const deviceId = matches[2];
const fingerprint = matches[3];
return success(
(async (): Promise<void> => {
const device = await getDeviceCryptoInfo(cli, userId, deviceId);
if (!device) {
throw new UserFriendlyError("slash_command|verify_unknown_pair", {
userId,
deviceId,
cause: undefined,
});
}
const deviceTrust = await cli.getCrypto()?.getDeviceVerificationStatus(userId, deviceId);
if (deviceTrust?.isVerified()) {
if (device.getFingerprint() === fingerprint) {
throw new UserFriendlyError("slash_command|verify_nop");
} else {
throw new UserFriendlyError("slash_command|verify_nop_warning_mismatch");
}
}
if (device.getFingerprint() !== fingerprint) {
const fprint = device.getFingerprint();
throw new UserFriendlyError("slash_command|verify_mismatch", {
fprint,
userId,
deviceId,
fingerprint,
cause: undefined,
});
}
await cli.setDeviceVerified(userId, deviceId, true);
// Tell the user we verified everything
Modal.createDialog(InfoDialog, {
title: _t("slash_command|verify_success_title"),
description: (
<div>
<p>{_t("slash_command|verify_success_description", { userId, deviceId })}</p>
</div>
),
});
})(),
);
}
}
return reject(this.getUsage());
},
category: CommandCategories.advanced,
renderingTypes: [TimelineRenderingType.Room],
}),
new Command({ new Command({
command: "discardsession", command: "discardsession",
description: _td("slash_command|discardsession"), description: _td("slash_command|discardsession"),

View file

@ -36,7 +36,6 @@ import AccessibleButton from "./components/views/elements/AccessibleButton";
import RightPanelStore from "./stores/right-panel/RightPanelStore"; import RightPanelStore from "./stores/right-panel/RightPanelStore";
import { highlightEvent, isLocationEvent } from "./utils/EventUtils"; import { highlightEvent, isLocationEvent } from "./utils/EventUtils";
import { ElementCall } from "./models/Call"; import { ElementCall } from "./models/Call";
import { textForVoiceBroadcastStoppedEvent, VoiceBroadcastInfoEventType } from "./voice-broadcast";
import { getSenderName } from "./utils/event/getSenderName"; import { getSenderName } from "./utils/event/getSenderName";
import PosthogTrackers from "./PosthogTrackers.ts"; import PosthogTrackers from "./PosthogTrackers.ts";
@ -906,7 +905,6 @@ const stateHandlers: IHandlers = {
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111) // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
"im.vector.modular.widgets": textForWidgetEvent, "im.vector.modular.widgets": textForWidgetEvent,
[WIDGET_LAYOUT_EVENT_TYPE]: textForWidgetLayoutEvent, [WIDGET_LAYOUT_EVENT_TYPE]: textForWidgetLayoutEvent,
[VoiceBroadcastInfoEventType]: textForVoiceBroadcastStoppedEvent,
}; };
// Add all the Mjolnir stuff to the renderer // Add all the Mjolnir stuff to the renderer

View file

@ -36,7 +36,7 @@ interface IState {
export default class EmbeddedPage extends React.PureComponent<IProps, IState> { export default class EmbeddedPage extends React.PureComponent<IProps, IState> {
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public declare context: React.ContextType<typeof MatrixClientContext>; declare public context: React.ContextType<typeof MatrixClientContext>;
private unmounted = false; private unmounted = false;
private dispatcherRef?: string; private dispatcherRef?: string;

View file

@ -34,6 +34,7 @@ import { Layout } from "../../settings/enums/Layout";
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
import Measured from "../views/elements/Measured"; import Measured from "../views/elements/Measured";
import EmptyState from "../views/right_panel/EmptyState"; import EmptyState from "../views/right_panel/EmptyState";
import { ScopedRoomContextProvider } from "../../contexts/ScopedRoomContext.tsx";
interface IProps { interface IProps {
roomId: string; roomId: string;
@ -51,7 +52,7 @@ interface IState {
*/ */
class FilePanel extends React.Component<IProps, IState> { class FilePanel extends React.Component<IProps, IState> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
// This is used to track if a decrypted event was a live event and should be // This is used to track if a decrypted event was a live event and should be
// added to the timeline. // added to the timeline.
@ -104,7 +105,11 @@ class FilePanel extends React.Component<IProps, IState> {
} }
if (!this.state.timelineSet.eventIdToTimeline(ev.getId()!)) { if (!this.state.timelineSet.eventIdToTimeline(ev.getId()!)) {
this.state.timelineSet.addEventToTimeline(ev, timeline, false); this.state.timelineSet.addEventToTimeline(ev, timeline, {
fromCache: false,
addToState: false,
toStartOfTimeline: false,
});
} }
} }
@ -269,12 +274,10 @@ class FilePanel extends React.Component<IProps, IState> {
if (this.state.timelineSet) { if (this.state.timelineSet) {
return ( return (
<RoomContext.Provider <ScopedRoomContextProvider
value={{ {...this.context}
...this.context, timelineRenderingType={TimelineRenderingType.File}
timelineRenderingType: TimelineRenderingType.File, narrow={this.state.narrow}
narrow: this.state.narrow,
}}
> >
<BaseCard <BaseCard
className="mx_FilePanel" className="mx_FilePanel"
@ -298,16 +301,11 @@ class FilePanel extends React.Component<IProps, IState> {
layout={Layout.Group} layout={Layout.Group}
/> />
</BaseCard> </BaseCard>
</RoomContext.Provider> </ScopedRoomContextProvider>
); );
} else { } else {
return ( return (
<RoomContext.Provider <ScopedRoomContextProvider {...this.context} timelineRenderingType={TimelineRenderingType.File}>
value={{
...this.context,
timelineRenderingType: TimelineRenderingType.File,
}}
>
<BaseCard <BaseCard
className="mx_FilePanel" className="mx_FilePanel"
onClose={this.props.onClose} onClose={this.props.onClose}
@ -315,7 +313,7 @@ class FilePanel extends React.Component<IProps, IState> {
> >
<Spinner /> <Spinner />
</BaseCard> </BaseCard>
</RoomContext.Provider> </ScopedRoomContextProvider>
); );
} }
} }

View file

@ -23,7 +23,6 @@ import classNames from "classnames";
import { isOnlyCtrlOrCmdKeyEvent, Key } from "../../Keyboard"; import { isOnlyCtrlOrCmdKeyEvent, Key } from "../../Keyboard";
import PageTypes from "../../PageTypes"; import PageTypes from "../../PageTypes";
import MediaDeviceHandler from "../../MediaDeviceHandler"; import MediaDeviceHandler from "../../MediaDeviceHandler";
import { fixupColorFonts } from "../../utils/FontManager";
import dis from "../../dispatcher/dispatcher"; import dis from "../../dispatcher/dispatcher";
import { IMatrixClientCreds } from "../../MatrixClientPeg"; import { IMatrixClientCreds } from "../../MatrixClientPeg";
import SettingsStore from "../../settings/SettingsStore"; import SettingsStore from "../../settings/SettingsStore";
@ -149,8 +148,6 @@ class LoggedInView extends React.Component<IProps, IState> {
MediaDeviceHandler.loadDevices(); MediaDeviceHandler.loadDevices();
fixupColorFonts();
this._roomView = React.createRef(); this._roomView = React.createRef();
this._resizeContainer = React.createRef(); this._resizeContainer = React.createRef();
this.resizeHandler = React.createRef(); this.resizeHandler = React.createRef();

View file

@ -119,7 +119,6 @@ import { ValidatedServerConfig } from "../../utils/ValidatedServerConfig";
import { isLocalRoom } from "../../utils/localRoom/isLocalRoom"; import { isLocalRoom } from "../../utils/localRoom/isLocalRoom";
import { SDKContext, SdkContextClass } from "../../contexts/SDKContext"; import { SDKContext, SdkContextClass } from "../../contexts/SDKContext";
import { viewUserDeviceSettings } from "../../actions/handlers/viewUserDeviceSettings"; import { viewUserDeviceSettings } from "../../actions/handlers/viewUserDeviceSettings";
import { cleanUpBroadcasts, VoiceBroadcastResumer } from "../../voice-broadcast";
import GenericToast from "../views/toasts/GenericToast"; import GenericToast from "../views/toasts/GenericToast";
import RovingSpotlightDialog from "../views/dialogs/spotlight/SpotlightDialog"; import RovingSpotlightDialog from "../views/dialogs/spotlight/SpotlightDialog";
import { findDMForUser } from "../../utils/dm/findDMForUser"; import { findDMForUser } from "../../utils/dm/findDMForUser";
@ -227,7 +226,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
private focusNext: FocusNextType; private focusNext: FocusNextType;
private subTitleStatus: string; private subTitleStatus: string;
private prevWindowWidth: number; private prevWindowWidth: number;
private voiceBroadcastResumer?: VoiceBroadcastResumer;
private readonly loggedInView = createRef<LoggedInViewType>(); private readonly loggedInView = createRef<LoggedInViewType>();
private dispatcherRef?: string; private dispatcherRef?: string;
@ -501,7 +499,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
window.removeEventListener("resize", this.onWindowResized); window.removeEventListener("resize", this.onWindowResized);
this.stores.accountPasswordStore.clearPassword(); this.stores.accountPasswordStore.clearPassword();
this.voiceBroadcastResumer?.destroy();
} }
private onWindowResized = (): void => { private onWindowResized = (): void => {
@ -651,10 +648,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
break; break;
case "logout": case "logout":
LegacyCallHandler.instance.hangupAllCalls(); LegacyCallHandler.instance.hangupAllCalls();
Promise.all([ Promise.all([...[...CallStore.instance.connectedCalls].map((call) => call.disconnect())]).finally(() =>
...[...CallStore.instance.connectedCalls].map((call) => call.disconnect()), Lifecycle.logout(this.stores.oidcClientStore),
cleanUpBroadcasts(this.stores), );
]).finally(() => Lifecycle.logout(this.stores.oidcClientStore));
break; break;
case "require_registration": case "require_registration":
startAnyRegistrationFlow(payload as any); startAnyRegistrationFlow(payload as any);
@ -1679,8 +1675,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}); });
} }
}); });
this.voiceBroadcastResumer = new VoiceBroadcastResumer(cli);
} }
/** /**

View file

@ -196,7 +196,7 @@ interface IReadReceiptForUser {
*/ */
export default class MessagePanel extends React.Component<IProps, IState> { export default class MessagePanel extends React.Component<IProps, IState> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
public static defaultProps = { public static defaultProps = {
disableGrouping: false, disableGrouping: false,

View file

@ -19,6 +19,7 @@ import { Layout } from "../../settings/enums/Layout";
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
import Measured from "../views/elements/Measured"; import Measured from "../views/elements/Measured";
import EmptyState from "../views/right_panel/EmptyState"; import EmptyState from "../views/right_panel/EmptyState";
import { ScopedRoomContextProvider } from "../../contexts/ScopedRoomContext.tsx";
interface IProps { interface IProps {
onClose(): void; onClose(): void;
@ -33,7 +34,7 @@ interface IState {
*/ */
export default class NotificationPanel extends React.PureComponent<IProps, IState> { export default class NotificationPanel extends React.PureComponent<IProps, IState> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
private card = React.createRef<HTMLDivElement>(); private card = React.createRef<HTMLDivElement>();
@ -79,12 +80,10 @@ export default class NotificationPanel extends React.PureComponent<IProps, IStat
} }
return ( return (
<RoomContext.Provider <ScopedRoomContextProvider
value={{ {...this.context}
...this.context, timelineRenderingType={TimelineRenderingType.Notification}
timelineRenderingType: TimelineRenderingType.Notification, narrow={this.state.narrow}
narrow: this.state.narrow,
}}
> >
<BaseCard <BaseCard
header={_t("notifications|enable_prompt_toast_title")} header={_t("notifications|enable_prompt_toast_title")}
@ -99,7 +98,7 @@ export default class NotificationPanel extends React.PureComponent<IProps, IStat
{this.card.current && <Measured sensor={this.card.current} onMeasurement={this.onMeasurement} />} {this.card.current && <Measured sensor={this.card.current} onMeasurement={this.onMeasurement} />}
{content} {content}
</BaseCard> </BaseCard>
</RoomContext.Provider> </ScopedRoomContextProvider>
); );
} }
} }

View file

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import React, { MutableRefObject, ReactNode, useContext, useRef } from "react"; import React, { MutableRefObject, ReactNode, useRef } from "react";
import { CallEvent, CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call"; import { CallEvent, CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { Optional } from "matrix-events-sdk"; import { Optional } from "matrix-events-sdk";
@ -21,19 +21,7 @@ import { WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../../stores/ActiveWidgetStore"; import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../../stores/ActiveWidgetStore";
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
import { UPDATE_EVENT } from "../../stores/AsyncStore"; import { UPDATE_EVENT } from "../../stores/AsyncStore";
import { SDKContext, SdkContextClass } from "../../contexts/SDKContext"; import { SdkContextClass } from "../../contexts/SDKContext";
import {
useCurrentVoiceBroadcastPreRecording,
useCurrentVoiceBroadcastRecording,
VoiceBroadcastPlayback,
VoiceBroadcastPlaybackBody,
VoiceBroadcastPreRecording,
VoiceBroadcastPreRecordingPip,
VoiceBroadcastRecording,
VoiceBroadcastRecordingPip,
VoiceBroadcastSmallPlaybackBody,
} from "../../voice-broadcast";
import { useCurrentVoiceBroadcastPlayback } from "../../voice-broadcast/hooks/useCurrentVoiceBroadcastPlayback";
import { WidgetPip } from "../views/pips/WidgetPip"; import { WidgetPip } from "../views/pips/WidgetPip";
const SHOW_CALL_IN_STATES = [ const SHOW_CALL_IN_STATES = [
@ -46,9 +34,6 @@ const SHOW_CALL_IN_STATES = [
]; ];
interface IProps { interface IProps {
voiceBroadcastRecording: Optional<VoiceBroadcastRecording>;
voiceBroadcastPreRecording: Optional<VoiceBroadcastPreRecording>;
voiceBroadcastPlayback: Optional<VoiceBroadcastPlayback>;
movePersistedElement: MutableRefObject<(() => void) | undefined>; movePersistedElement: MutableRefObject<(() => void) | undefined>;
} }
@ -245,52 +230,9 @@ class PipContainerInner extends React.Component<IProps, IState> {
this.setState({ showWidgetInPip, persistentWidgetId, persistentRoomId }); this.setState({ showWidgetInPip, persistentWidgetId, persistentRoomId });
} }
private createVoiceBroadcastPlaybackPipContent(voiceBroadcastPlayback: VoiceBroadcastPlayback): CreatePipChildren {
const content =
this.state.viewedRoomId === voiceBroadcastPlayback.infoEvent.getRoomId() ? (
<VoiceBroadcastPlaybackBody playback={voiceBroadcastPlayback} pip={true} />
) : (
<VoiceBroadcastSmallPlaybackBody playback={voiceBroadcastPlayback} />
);
return ({ onStartMoving }) => (
<div key={`vb-playback-${voiceBroadcastPlayback.infoEvent.getId()}`} onMouseDown={onStartMoving}>
{content}
</div>
);
}
private createVoiceBroadcastPreRecordingPipContent(
voiceBroadcastPreRecording: VoiceBroadcastPreRecording,
): CreatePipChildren {
return ({ onStartMoving }) => (
<div key="vb-pre-recording" onMouseDown={onStartMoving}>
<VoiceBroadcastPreRecordingPip voiceBroadcastPreRecording={voiceBroadcastPreRecording} />
</div>
);
}
private createVoiceBroadcastRecordingPipContent(
voiceBroadcastRecording: VoiceBroadcastRecording,
): CreatePipChildren {
return ({ onStartMoving }) => (
<div key={`vb-recording-${voiceBroadcastRecording.infoEvent.getId()}`} onMouseDown={onStartMoving}>
<VoiceBroadcastRecordingPip recording={voiceBroadcastRecording} />
</div>
);
}
public render(): ReactNode { public render(): ReactNode {
const pipMode = true; const pipMode = true;
let pipContent: Array<CreatePipChildren> = []; const pipContent: Array<CreatePipChildren> = [];
if (this.props.voiceBroadcastRecording) {
pipContent = [this.createVoiceBroadcastRecordingPipContent(this.props.voiceBroadcastRecording)];
} else if (this.props.voiceBroadcastPreRecording) {
pipContent = [this.createVoiceBroadcastPreRecordingPipContent(this.props.voiceBroadcastPreRecording)];
} else if (this.props.voiceBroadcastPlayback) {
pipContent = [this.createVoiceBroadcastPlaybackPipContent(this.props.voiceBroadcastPlayback)];
}
if (this.state.primaryCall) { if (this.state.primaryCall) {
// get a ref to call inside the current scope // get a ref to call inside the current scope
@ -338,24 +280,7 @@ class PipContainerInner extends React.Component<IProps, IState> {
} }
export const PipContainer: React.FC = () => { export const PipContainer: React.FC = () => {
const sdkContext = useContext(SDKContext);
const voiceBroadcastPreRecordingStore = sdkContext.voiceBroadcastPreRecordingStore;
const { currentVoiceBroadcastPreRecording } = useCurrentVoiceBroadcastPreRecording(voiceBroadcastPreRecordingStore);
const voiceBroadcastRecordingsStore = sdkContext.voiceBroadcastRecordingsStore;
const { currentVoiceBroadcastRecording } = useCurrentVoiceBroadcastRecording(voiceBroadcastRecordingsStore);
const voiceBroadcastPlaybacksStore = sdkContext.voiceBroadcastPlaybacksStore;
const { currentVoiceBroadcastPlayback } = useCurrentVoiceBroadcastPlayback(voiceBroadcastPlaybacksStore);
const movePersistedElement = useRef<() => void>(); const movePersistedElement = useRef<() => void>();
return ( return <PipContainerInner movePersistedElement={movePersistedElement} />;
<PipContainerInner
voiceBroadcastPlayback={currentVoiceBroadcastPlayback}
voiceBroadcastPreRecording={currentVoiceBroadcastPreRecording}
voiceBroadcastRecording={currentVoiceBroadcastRecording}
movePersistedElement={movePersistedElement}
/>
);
}; };

View file

@ -63,7 +63,7 @@ interface IState {
export default class RightPanel extends React.Component<Props, IState> { export default class RightPanel extends React.Component<Props, IState> {
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public declare context: React.ContextType<typeof MatrixClientContext>; declare public context: React.ContextType<typeof MatrixClientContext>;
public constructor(props: Props, context: React.ContextType<typeof MatrixClientContext>) { public constructor(props: Props, context: React.ContextType<typeof MatrixClientContext>) {
super(props, context); super(props, context);
@ -109,10 +109,10 @@ export default class RightPanel extends React.Component<Props, IState> {
} }
// redraw the badge on the membership list // redraw the badge on the membership list
if (this.state.phase === RightPanelPhases.RoomMemberList) { if (this.state.phase === RightPanelPhases.MemberList) {
this.delayedUpdate(); this.delayedUpdate();
} else if ( } else if (
this.state.phase === RightPanelPhases.RoomMemberInfo && this.state.phase === RightPanelPhases.MemberInfo &&
member.userId === this.state.cardState?.member?.userId member.userId === this.state.cardState?.member?.userId
) { ) {
// refresh the member info (e.g. new power level) // refresh the member info (e.g. new power level)
@ -157,7 +157,7 @@ export default class RightPanel extends React.Component<Props, IState> {
const phase = this.props.overwriteCard?.phase ?? this.state.phase; const phase = this.props.overwriteCard?.phase ?? this.state.phase;
const cardState = this.props.overwriteCard?.state ?? this.state.cardState; const cardState = this.props.overwriteCard?.state ?? this.state.cardState;
switch (phase) { switch (phase) {
case RightPanelPhases.RoomMemberList: case RightPanelPhases.MemberList:
if (!!roomId) { if (!!roomId) {
card = ( card = (
<MemberList <MemberList
@ -170,22 +170,8 @@ export default class RightPanel extends React.Component<Props, IState> {
); );
} }
break; break;
case RightPanelPhases.SpaceMemberList:
if (!!cardState?.spaceId || !!roomId) {
card = (
<MemberList
roomId={cardState?.spaceId ?? roomId!}
key={cardState?.spaceId ?? roomId!}
onClose={this.onClose}
searchQuery={this.state.searchQuery}
onSearchQueryChanged={this.onSearchQueryChanged}
/>
);
}
break;
case RightPanelPhases.RoomMemberInfo: case RightPanelPhases.MemberInfo:
case RightPanelPhases.SpaceMemberInfo:
case RightPanelPhases.EncryptionPanel: { case RightPanelPhases.EncryptionPanel: {
if (!!cardState?.member) { if (!!cardState?.member) {
const roomMember = cardState.member instanceof RoomMember ? cardState.member : undefined; const roomMember = cardState.member instanceof RoomMember ? cardState.member : undefined;
@ -203,8 +189,7 @@ export default class RightPanel extends React.Component<Props, IState> {
} }
break; break;
} }
case RightPanelPhases.Room3pidMemberInfo: case RightPanelPhases.ThreePidMemberInfo:
case RightPanelPhases.Space3pidMemberInfo:
if (!!cardState?.memberInfoEvent) { if (!!cardState?.memberInfoEvent) {
card = ( card = (
<ThirdPartyMemberInfo event={cardState.memberInfoEvent} key={roomId} onClose={this.onClose} /> <ThirdPartyMemberInfo event={cardState.memberInfoEvent} key={roomId} onClose={this.onClose} />

View file

@ -26,7 +26,7 @@ import ErrorDialog from "../views/dialogs/ErrorDialog";
import ResizeNotifier from "../../utils/ResizeNotifier"; import ResizeNotifier from "../../utils/ResizeNotifier";
import MatrixClientContext from "../../contexts/MatrixClientContext"; import MatrixClientContext from "../../contexts/MatrixClientContext";
import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
import RoomContext from "../../contexts/RoomContext"; import { useScopedRoomContext } from "../../contexts/ScopedRoomContext.tsx";
const DEBUG = false; const DEBUG = false;
let debuglog = function (msg: string): void {}; let debuglog = function (msg: string): void {};
@ -53,7 +53,7 @@ interface Props {
export const RoomSearchView = forwardRef<ScrollPanel, Props>( export const RoomSearchView = forwardRef<ScrollPanel, Props>(
({ term, scope, promise, abortController, resizeNotifier, className, onUpdate, inProgress }: Props, ref) => { ({ term, scope, promise, abortController, resizeNotifier, className, onUpdate, inProgress }: Props, ref) => {
const client = useContext(MatrixClientContext); const client = useContext(MatrixClientContext);
const roomContext = useContext(RoomContext); const roomContext = useScopedRoomContext("showHiddenEvents");
const [highlights, setHighlights] = useState<string[] | null>(null); const [highlights, setHighlights] = useState<string[] | null>(null);
const [results, setResults] = useState<ISearchResults | null>(null); const [results, setResults] = useState<ISearchResults | null>(null);
const aborted = useRef(false); const aborted = useRef(false);

View file

@ -89,7 +89,7 @@ interface IState {
export default class RoomStatusBar extends React.PureComponent<IProps, IState> { export default class RoomStatusBar extends React.PureComponent<IProps, IState> {
private unmounted = false; private unmounted = false;
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public declare context: React.ContextType<typeof MatrixClientContext>; declare public context: React.ContextType<typeof MatrixClientContext>;
public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) { public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
super(props, context); super(props, context);

View file

@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import React, { ChangeEvent, ComponentProps, createRef, ReactElement, ReactNode, RefObject, useContext } from "react"; import React, { ChangeEvent, ComponentProps, createRef, ReactElement, ReactNode, RefObject, JSX } from "react";
import classNames from "classnames"; import classNames from "classnames";
import { import {
IRecommendedVersion, IRecommendedVersion,
@ -29,6 +29,7 @@ import {
MatrixError, MatrixError,
ISearchResults, ISearchResults,
THREAD_RELATION_TYPE, THREAD_RELATION_TYPE,
MatrixClient,
} from "matrix-js-sdk/src/matrix"; } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types"; import { KnownMembership } from "matrix-js-sdk/src/types";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
@ -54,7 +55,7 @@ import WidgetEchoStore from "../../stores/WidgetEchoStore";
import SettingsStore from "../../settings/SettingsStore"; import SettingsStore from "../../settings/SettingsStore";
import { Layout } from "../../settings/enums/Layout"; import { Layout } from "../../settings/enums/Layout";
import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton"; import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton";
import RoomContext, { TimelineRenderingType, MainSplitContentType } from "../../contexts/RoomContext"; import { TimelineRenderingType, MainSplitContentType } from "../../contexts/RoomContext";
import { E2EStatus, shieldStatusForRoom } from "../../utils/ShieldUtils"; import { E2EStatus, shieldStatusForRoom } from "../../utils/ShieldUtils";
import { Action } from "../../dispatcher/actions"; import { Action } from "../../dispatcher/actions";
import { IMatrixClientCreds } from "../../MatrixClientPeg"; import { IMatrixClientCreds } from "../../MatrixClientPeg";
@ -126,6 +127,7 @@ import RightPanelStore from "../../stores/right-panel/RightPanelStore";
import { onView3pidInvite } from "../../stores/right-panel/action-handlers"; import { onView3pidInvite } from "../../stores/right-panel/action-handlers";
import RoomSearchAuxPanel from "../views/rooms/RoomSearchAuxPanel"; import RoomSearchAuxPanel from "../views/rooms/RoomSearchAuxPanel";
import { PinnedMessageBanner } from "../views/rooms/PinnedMessageBanner"; import { PinnedMessageBanner } from "../views/rooms/PinnedMessageBanner";
import { ScopedRoomContextProvider, useScopedRoomContext } from "../../contexts/ScopedRoomContext";
const DEBUG = false; const DEBUG = false;
const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000; const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000;
@ -233,6 +235,11 @@ export interface IRoomState {
liveTimeline?: EventTimeline; liveTimeline?: EventTimeline;
narrow: boolean; narrow: boolean;
msc3946ProcessDynamicPredecessor: boolean; msc3946ProcessDynamicPredecessor: boolean;
/**
* Whether the room is encrypted or not.
* If null, we are still determining the encryption status.
*/
isRoomEncrypted: boolean | null;
canAskToJoin: boolean; canAskToJoin: boolean;
promptAskToJoin: boolean; promptAskToJoin: boolean;
@ -246,6 +253,7 @@ interface LocalRoomViewProps {
permalinkCreator: RoomPermalinkCreator; permalinkCreator: RoomPermalinkCreator;
roomView: RefObject<HTMLElement>; roomView: RefObject<HTMLElement>;
onFileDrop: (dataTransfer: DataTransfer) => Promise<void>; onFileDrop: (dataTransfer: DataTransfer) => Promise<void>;
mainSplitContentType: MainSplitContentType;
} }
/** /**
@ -255,7 +263,7 @@ interface LocalRoomViewProps {
* @returns {ReactElement} * @returns {ReactElement}
*/ */
function LocalRoomView(props: LocalRoomViewProps): ReactElement { function LocalRoomView(props: LocalRoomViewProps): ReactElement {
const context = useContext(RoomContext); const context = useScopedRoomContext("room");
const room = context.room as LocalRoom; const room = context.room as LocalRoom;
const encryptionEvent = props.localRoom.currentState.getStateEvents(EventType.RoomEncryption)[0]; const encryptionEvent = props.localRoom.currentState.getStateEvents(EventType.RoomEncryption)[0];
let encryptionTile: ReactNode; let encryptionTile: ReactNode;
@ -323,6 +331,7 @@ interface ILocalRoomCreateLoaderProps {
localRoom: LocalRoom; localRoom: LocalRoom;
names: string; names: string;
resizeNotifier: ResizeNotifier; resizeNotifier: ResizeNotifier;
mainSplitContentType: MainSplitContentType;
} }
/** /**
@ -363,7 +372,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
private roomViewBody = createRef<HTMLDivElement>(); private roomViewBody = createRef<HTMLDivElement>();
public static contextType = SDKContext; public static contextType = SDKContext;
public declare context: React.ContextType<typeof SDKContext>; declare public context: React.ContextType<typeof SDKContext>;
public constructor(props: IRoomProps, context: React.ContextType<typeof SDKContext>) { public constructor(props: IRoomProps, context: React.ContextType<typeof SDKContext>) {
super(props, context); super(props, context);
@ -417,6 +426,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
canAskToJoin: this.askToJoinEnabled, canAskToJoin: this.askToJoinEnabled,
promptAskToJoin: false, promptAskToJoin: false,
viewRoomOpts: { buttons: [] }, viewRoomOpts: { buttons: [] },
isRoomEncrypted: null,
}; };
} }
@ -655,6 +665,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
// the RoomView instance // the RoomView instance
if (initial) { if (initial) {
newState.room = this.context.client!.getRoom(newState.roomId) || undefined; newState.room = this.context.client!.getRoom(newState.roomId) || undefined;
newState.isRoomEncrypted = null;
if (newState.room) { if (newState.room) {
newState.showApps = this.shouldShowApps(newState.room); newState.showApps = this.shouldShowApps(newState.room);
this.onRoomLoaded(newState.room); this.onRoomLoaded(newState.room);
@ -697,6 +708,14 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
if (initial) { if (initial) {
this.setupRoom(newState.room, newState.roomId, !!newState.joining, !!newState.shouldPeek); this.setupRoom(newState.room, newState.roomId, !!newState.joining, !!newState.shouldPeek);
} }
// We don't block the initial setup but we want to make it early to not block the timeline rendering
const isRoomEncrypted = await this.getIsRoomEncrypted(newState.roomId);
this.setState({
isRoomEncrypted,
...(isRoomEncrypted &&
newState.roomId && { e2eStatus: RoomView.e2eStatusCache.get(newState.roomId) ?? E2EStatus.Warning }),
});
}; };
private onConnectedCalls = (): void => { private onConnectedCalls = (): void => {
@ -1214,18 +1233,18 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
if (payload.member) { if (payload.member) {
if (payload.push) { if (payload.push) {
RightPanelStore.instance.pushCard({ RightPanelStore.instance.pushCard({
phase: RightPanelPhases.RoomMemberInfo, phase: RightPanelPhases.MemberInfo,
state: { member: payload.member }, state: { member: payload.member },
}); });
} else { } else {
RightPanelStore.instance.setCards([ RightPanelStore.instance.setCards([
{ phase: RightPanelPhases.RoomSummary }, { phase: RightPanelPhases.RoomSummary },
{ phase: RightPanelPhases.RoomMemberList }, { phase: RightPanelPhases.MemberList },
{ phase: RightPanelPhases.RoomMemberInfo, state: { member: payload.member } }, { phase: RightPanelPhases.MemberInfo, state: { member: payload.member } },
]); ]);
} }
} else { } else {
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.RoomMemberList); RightPanelStore.instance.showOrHidePhase(RightPanelPhases.MemberList);
} }
break; break;
case Action.View3pidInvite: case Action.View3pidInvite:
@ -1342,13 +1361,12 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
this.context.widgetLayoutStore.on(WidgetLayoutStore.emissionForRoom(room), this.onWidgetLayoutChange); this.context.widgetLayoutStore.on(WidgetLayoutStore.emissionForRoom(room), this.onWidgetLayoutChange);
this.calculatePeekRules(room); this.calculatePeekRules(room);
this.updatePreviewUrlVisibility(room);
this.loadMembersIfJoined(room); this.loadMembersIfJoined(room);
this.calculateRecommendedVersion(room); this.calculateRecommendedVersion(room);
this.updateE2EStatus(room);
this.updatePermissions(room); this.updatePermissions(room);
this.checkWidgets(room); this.checkWidgets(room);
this.loadVirtualRoom(room); this.loadVirtualRoom(room);
this.updateRoomEncrypted(room);
if ( if (
this.getMainSplitContentType(room) !== MainSplitContentType.Timeline && this.getMainSplitContentType(room) !== MainSplitContentType.Timeline &&
@ -1377,6 +1395,13 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
return room?.currentState.getStateEvents(EventType.RoomTombstone, "") ?? undefined; return room?.currentState.getStateEvents(EventType.RoomTombstone, "") ?? undefined;
} }
private async getIsRoomEncrypted(roomId = this.state.roomId): Promise<boolean> {
const crypto = this.context.client?.getCrypto();
if (!crypto || !roomId) return false;
return await crypto.isEncryptionEnabledInRoom(roomId);
}
private async calculateRecommendedVersion(room: Room): Promise<void> { private async calculateRecommendedVersion(room: Room): Promise<void> {
const upgradeRecommendation = await room.getRecommendedVersion(); const upgradeRecommendation = await room.getRecommendedVersion();
if (this.unmounted) return; if (this.unmounted) return;
@ -1409,12 +1434,15 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
}); });
} }
private updatePreviewUrlVisibility({ roomId }: Room): void { private updatePreviewUrlVisibility(room: Room): void {
// URL Previews in E2EE rooms can be a privacy leak so use a different setting which is per-room explicit this.setState(({ isRoomEncrypted }) => ({
const key = this.context.client?.isRoomEncrypted(roomId) ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled"; showUrlPreview: this.getPreviewUrlVisibility(room, isRoomEncrypted),
this.setState({ }));
showUrlPreview: SettingsStore.getValue(key, roomId), }
});
private getPreviewUrlVisibility({ roomId }: Room, isRoomEncrypted: boolean | null): boolean {
const key = isRoomEncrypted ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled";
return SettingsStore.getValue(key, roomId);
} }
private onRoom = (room: Room): void => { private onRoom = (room: Room): void => {
@ -1456,22 +1484,20 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
}; };
private async updateE2EStatus(room: Room): Promise<void> { private async updateE2EStatus(room: Room): Promise<void> {
if (!this.context.client?.isRoomEncrypted(room.roomId)) return; if (!this.context.client || !this.state.isRoomEncrypted) return;
const e2eStatus = await this.cacheAndGetE2EStatus(room, this.context.client);
// If crypto is not currently enabled, we aren't tracking devices at all,
// so we don't know what the answer is. Let's error on the safe side and show
// a warning for this case.
let e2eStatus = RoomView.e2eStatusCache.get(room.roomId) ?? E2EStatus.Warning;
// set the state immediately then update, so we don't scare the user into thinking the room is unencrypted
this.setState({ e2eStatus });
if (this.context.client.getCrypto()) {
/* At this point, the user has encryption on and cross-signing on */
e2eStatus = await shieldStatusForRoom(this.context.client, room);
RoomView.e2eStatusCache.set(room.roomId, e2eStatus);
if (this.unmounted) return; if (this.unmounted) return;
this.setState({ e2eStatus }); this.setState({ e2eStatus });
} }
private async cacheAndGetE2EStatus(room: Room, client: MatrixClient): Promise<E2EStatus> {
let e2eStatus = RoomView.e2eStatusCache.get(room.roomId);
// set the state immediately then update, so we don't scare the user into thinking the room is unencrypted
if (e2eStatus) this.setState({ e2eStatus });
e2eStatus = await shieldStatusForRoom(client, room);
RoomView.e2eStatusCache.set(room.roomId, e2eStatus);
return e2eStatus;
} }
private onUrlPreviewsEnabledChange = (): void => { private onUrlPreviewsEnabledChange = (): void => {
@ -1480,20 +1506,36 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
} }
}; };
private onRoomStateEvents = (ev: MatrixEvent, state: RoomState): void => { private onRoomStateEvents = async (ev: MatrixEvent, state: RoomState): Promise<void> => {
// ignore if we don't have a room yet // ignore if we don't have a room yet
if (!this.state.room || this.state.room.roomId !== state.roomId) return; if (!this.state.room || this.state.room.roomId !== state.roomId || !this.context.client) return;
switch (ev.getType()) { switch (ev.getType()) {
case EventType.RoomTombstone: case EventType.RoomTombstone:
this.setState({ tombstone: this.getRoomTombstone() }); this.setState({ tombstone: this.getRoomTombstone() });
break; break;
case EventType.RoomEncryption: {
await this.updateRoomEncrypted();
break;
}
default: default:
this.updatePermissions(this.state.room); this.updatePermissions(this.state.room);
} }
}; };
private async updateRoomEncrypted(room = this.state.room): Promise<void> {
if (!room || !this.context.client) return;
const isRoomEncrypted = await this.getIsRoomEncrypted(room.roomId);
const newE2EStatus = isRoomEncrypted ? await this.cacheAndGetE2EStatus(room, this.context.client) : null;
this.setState({
isRoomEncrypted,
showUrlPreview: this.getPreviewUrlVisibility(room, isRoomEncrypted),
...(newE2EStatus && { e2eStatus: newE2EStatus }),
});
}
private onRoomStateUpdate = (state: RoomState): void => { private onRoomStateUpdate = (state: RoomState): void => {
// ignore members in other rooms // ignore members in other rooms
if (state.roomId !== this.state.room?.roomId) { if (state.roomId !== this.state.room?.roomId) {
@ -1959,35 +2001,41 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
if (!this.state.room || !this.context?.client) return null; if (!this.state.room || !this.context?.client) return null;
const names = this.state.room.getDefaultRoomName(this.context.client.getSafeUserId()); const names = this.state.room.getDefaultRoomName(this.context.client.getSafeUserId());
return ( return (
<RoomContext.Provider value={this.state}> <ScopedRoomContextProvider {...this.state}>
<LocalRoomCreateLoader localRoom={localRoom} names={names} resizeNotifier={this.props.resizeNotifier} /> <LocalRoomCreateLoader
</RoomContext.Provider> localRoom={localRoom}
names={names}
resizeNotifier={this.props.resizeNotifier}
mainSplitContentType={this.state.mainSplitContentType}
/>
</ScopedRoomContextProvider>
); );
} }
private renderLocalRoomView(localRoom: LocalRoom): ReactNode { private renderLocalRoomView(localRoom: LocalRoom): ReactNode {
return ( return (
<RoomContext.Provider value={this.state}> <ScopedRoomContextProvider {...this.state}>
<LocalRoomView <LocalRoomView
localRoom={localRoom} localRoom={localRoom}
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
permalinkCreator={this.permalinkCreator} permalinkCreator={this.permalinkCreator}
roomView={this.roomView} roomView={this.roomView}
onFileDrop={this.onFileDrop} onFileDrop={this.onFileDrop}
mainSplitContentType={this.state.mainSplitContentType}
/> />
</RoomContext.Provider> </ScopedRoomContextProvider>
); );
} }
private renderWaitingForThirdPartyRoomView(inviteEvent: MatrixEvent): ReactNode { private renderWaitingForThirdPartyRoomView(inviteEvent: MatrixEvent): ReactNode {
return ( return (
<RoomContext.Provider value={this.state}> <ScopedRoomContextProvider {...this.state}>
<WaitingForThirdPartyRoomView <WaitingForThirdPartyRoomView
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
roomView={this.roomView} roomView={this.roomView}
inviteEvent={inviteEvent} inviteEvent={inviteEvent}
/> />
</RoomContext.Provider> </ScopedRoomContextProvider>
); );
} }
@ -2027,6 +2075,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
public render(): ReactNode { public render(): ReactNode {
if (!this.context.client) return null; if (!this.context.client) return null;
const { isRoomEncrypted } = this.state;
const isRoomEncryptionLoading = isRoomEncrypted === null;
if (this.state.room instanceof LocalRoom) { if (this.state.room instanceof LocalRoom) {
if (this.state.room.state === LocalRoomState.CREATING) { if (this.state.room.state === LocalRoomState.CREATING) {
@ -2242,14 +2292,16 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
let aux: JSX.Element | undefined; let aux: JSX.Element | undefined;
let previewBar; let previewBar;
if (this.state.timelineRenderingType === TimelineRenderingType.Search) { if (this.state.timelineRenderingType === TimelineRenderingType.Search) {
if (!isRoomEncryptionLoading) {
aux = ( aux = (
<RoomSearchAuxPanel <RoomSearchAuxPanel
searchInfo={this.state.search} searchInfo={this.state.search}
onCancelClick={this.onCancelSearchClick} onCancelClick={this.onCancelSearchClick}
onSearchScopeChange={this.onSearchScopeChange} onSearchScopeChange={this.onSearchScopeChange}
isRoomEncrypted={this.context.client.isRoomEncrypted(this.state.room.roomId)} isRoomEncrypted={isRoomEncrypted}
/> />
); );
}
} else if (showRoomUpgradeBar) { } else if (showRoomUpgradeBar) {
aux = <RoomUpgradeWarningBar room={this.state.room} />; aux = <RoomUpgradeWarningBar room={this.state.room} />;
} else if (myMembership !== KnownMembership.Join) { } else if (myMembership !== KnownMembership.Join) {
@ -2325,8 +2377,10 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
let messageComposer; let messageComposer;
const showComposer = const showComposer =
!isRoomEncryptionLoading &&
// joined and not showing search results // joined and not showing search results
myMembership === KnownMembership.Join && !this.state.search; myMembership === KnownMembership.Join &&
!this.state.search;
if (showComposer) { if (showComposer) {
messageComposer = ( messageComposer = (
<MessageComposer <MessageComposer
@ -2367,7 +2421,9 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
highlightedEventId = this.state.initialEventId; highlightedEventId = this.state.initialEventId;
} }
const messagePanel = ( let messagePanel: JSX.Element | undefined;
if (!isRoomEncryptionLoading) {
messagePanel = (
<TimelinePanel <TimelinePanel
ref={this.gatherTimelinePanelRef} ref={this.gatherTimelinePanelRef}
timelineSet={this.state.room.getUnfilteredTimelineSet()} timelineSet={this.state.room.getUnfilteredTimelineSet()}
@ -2395,6 +2451,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
editState={this.state.editState} editState={this.state.editState}
/> />
); );
}
let topUnreadMessagesBar: JSX.Element | undefined; let topUnreadMessagesBar: JSX.Element | undefined;
// Do not show TopUnreadMessagesBar if we have search results showing, it makes no sense // Do not show TopUnreadMessagesBar if we have search results showing, it makes no sense
@ -2415,7 +2472,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
); );
} }
const showRightPanel = this.state.room && this.state.showRightPanel; const showRightPanel = !isRoomEncryptionLoading && this.state.room && this.state.showRightPanel;
const rightPanel = showRightPanel ? ( const rightPanel = showRightPanel ? (
<RightPanel <RightPanel
@ -2516,7 +2573,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
} }
return ( return (
<RoomContext.Provider value={this.state}> <ScopedRoomContextProvider {...this.state}>
<div className={mainClasses} ref={this.roomView} onKeyDown={this.onReactKeyDown}> <div className={mainClasses} ref={this.roomView} onKeyDown={this.onReactKeyDown}>
{showChatEffects && this.roomView.current && ( {showChatEffects && this.roomView.current && (
<EffectsOverlay roomWidth={this.roomView.current.offsetWidth} /> <EffectsOverlay roomWidth={this.roomView.current.offsetWidth} />
@ -2543,7 +2600,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
</MainSplit> </MainSplit>
</ErrorBoundary> </ErrorBoundary>
</div> </div>
</RoomContext.Provider> </ScopedRoomContextProvider>
); );
} }
} }

View file

@ -208,7 +208,7 @@ const SpaceLanding: React.FC<{ space: Room }> = ({ space }) => {
const storeIsShowingSpaceMembers = useCallback( const storeIsShowingSpaceMembers = useCallback(
() => () =>
RightPanelStore.instance.isOpenForRoom(space.roomId) && RightPanelStore.instance.isOpenForRoom(space.roomId) &&
RightPanelStore.instance.currentCardForRoom(space.roomId)?.phase === RightPanelPhases.SpaceMemberList, RightPanelStore.instance.currentCardForRoom(space.roomId)?.phase === RightPanelPhases.MemberList,
[space.roomId], [space.roomId],
); );
const isShowingMembers = useEventEmitterState(RightPanelStore.instance, UPDATE_EVENT, storeIsShowingSpaceMembers); const isShowingMembers = useEventEmitterState(RightPanelStore.instance, UPDATE_EVENT, storeIsShowingSpaceMembers);
@ -251,7 +251,7 @@ const SpaceLanding: React.FC<{ space: Room }> = ({ space }) => {
} }
const onMembersClick = (): void => { const onMembersClick = (): void => {
RightPanelStore.instance.setCard({ phase: RightPanelPhases.SpaceMemberList }); RightPanelStore.instance.setCard({ phase: RightPanelPhases.MemberList });
}; };
return ( return (
@ -597,7 +597,7 @@ const SpaceSetupPrivateInvite: React.FC<{
export default class SpaceRoomView extends React.PureComponent<IProps, IState> { export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public declare context: React.ContextType<typeof MatrixClientContext>; declare public context: React.ContextType<typeof MatrixClientContext>;
private dispatcherRef?: string; private dispatcherRef?: string;

View file

@ -20,7 +20,7 @@ import MatrixClientContext, { useMatrixClientContext } from "../../contexts/Matr
import { _t } from "../../languageHandler"; import { _t } from "../../languageHandler";
import { ContextMenuButton } from "../../accessibility/context_menu/ContextMenuButton"; import { ContextMenuButton } from "../../accessibility/context_menu/ContextMenuButton";
import ContextMenu, { ChevronFace, MenuItemRadio, useContextMenu } from "./ContextMenu"; import ContextMenu, { ChevronFace, MenuItemRadio, useContextMenu } from "./ContextMenu";
import RoomContext, { TimelineRenderingType, useRoomContext } from "../../contexts/RoomContext"; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
import TimelinePanel from "./TimelinePanel"; import TimelinePanel from "./TimelinePanel";
import { Layout } from "../../settings/enums/Layout"; import { Layout } from "../../settings/enums/Layout";
import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
@ -30,6 +30,7 @@ import { ButtonEvent } from "../views/elements/AccessibleButton";
import Spinner from "../views/elements/Spinner"; import Spinner from "../views/elements/Spinner";
import { clearRoomNotification } from "../../utils/notifications"; import { clearRoomNotification } from "../../utils/notifications";
import EmptyState from "../views/right_panel/EmptyState"; import EmptyState from "../views/right_panel/EmptyState";
import { ScopedRoomContextProvider, useScopedRoomContext } from "../../contexts/ScopedRoomContext.tsx";
interface IProps { interface IProps {
roomId: string; roomId: string;
@ -68,7 +69,7 @@ export const ThreadPanelHeader: React.FC<{
setFilterOption: (filterOption: ThreadFilterType) => void; setFilterOption: (filterOption: ThreadFilterType) => void;
}> = ({ filterOption, setFilterOption }) => { }> = ({ filterOption, setFilterOption }) => {
const mxClient = useMatrixClientContext(); const mxClient = useMatrixClientContext();
const roomContext = useRoomContext(); const roomContext = useScopedRoomContext("room");
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu<HTMLElement>(); const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu<HTMLElement>();
const options: readonly ThreadPanelHeaderOption[] = [ const options: readonly ThreadPanelHeaderOption[] = [
{ {
@ -184,13 +185,11 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
}, [timelineSet, timelinePanel]); }, [timelineSet, timelinePanel]);
return ( return (
<RoomContext.Provider <ScopedRoomContextProvider
value={{ {...roomContext}
...roomContext, timelineRenderingType={TimelineRenderingType.ThreadsList}
timelineRenderingType: TimelineRenderingType.ThreadsList, showHiddenEvents={true}
showHiddenEvents: true, narrow={narrow}
narrow,
}}
> >
<BaseCard <BaseCard
header={ header={
@ -241,7 +240,7 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
</div> </div>
)} )}
</BaseCard> </BaseCard>
</RoomContext.Provider> </ScopedRoomContextProvider>
); );
}; };
export default ThreadPanel; export default ThreadPanel;

View file

@ -51,6 +51,7 @@ import { ComposerInsertPayload, ComposerType } from "../../dispatcher/payloads/C
import Heading from "../views/typography/Heading"; import Heading from "../views/typography/Heading";
import { SdkContextClass } from "../../contexts/SDKContext"; import { SdkContextClass } from "../../contexts/SDKContext";
import { ThreadPayload } from "../../dispatcher/payloads/ThreadPayload"; import { ThreadPayload } from "../../dispatcher/payloads/ThreadPayload";
import { ScopedRoomContextProvider } from "../../contexts/ScopedRoomContext.tsx";
interface IProps { interface IProps {
room: Room; room: Room;
@ -75,7 +76,7 @@ interface IState {
export default class ThreadView extends React.Component<IProps, IState> { export default class ThreadView extends React.Component<IProps, IState> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
private dispatcherRef?: string; private dispatcherRef?: string;
private layoutWatcherRef?: string; private layoutWatcherRef?: string;
@ -422,14 +423,12 @@ export default class ThreadView extends React.Component<IProps, IState> {
} }
return ( return (
<RoomContext.Provider <ScopedRoomContextProvider
value={{ {...this.context}
...this.context, timelineRenderingType={TimelineRenderingType.Thread}
timelineRenderingType: TimelineRenderingType.Thread, threadId={this.state.thread?.id}
threadId: this.state.thread?.id, liveTimeline={this.state?.thread?.timelineSet?.getLiveTimeline()}
liveTimeline: this.state?.thread?.timelineSet?.getLiveTimeline(), narrow={this.state.narrow}
narrow: this.state.narrow,
}}
> >
<BaseCard <BaseCard
className={classNames("mx_ThreadView mx_ThreadPanel", { className={classNames("mx_ThreadView mx_ThreadPanel", {
@ -463,7 +462,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
/> />
)} )}
</BaseCard> </BaseCard>
</RoomContext.Provider> </ScopedRoomContextProvider>
); );
} }
} }

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