Merge remote-tracking branch 'origin/develop' into hs/url-preview-new-design
@ -31,7 +31,9 @@ runs:
|
||||
|
||||
- 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 "$OUT_PATH"
|
||||
env:
|
||||
OUT_PATH: ${{ inputs.out-file-path }}
|
||||
|
||||
- name: Clean up temp directory
|
||||
shell: bash
|
||||
|
||||
49
.github/actions/setup-playwright/action.yml
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
name: Setup playwright
|
||||
description: Installs playwright browsers and sets up a cache
|
||||
inputs:
|
||||
needs-webkit:
|
||||
description: Whether to install the additional dependencies for webkit
|
||||
required: false
|
||||
default: "false"
|
||||
write-cache:
|
||||
description: Whether to write the cache back
|
||||
required: true
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Calculate cache key
|
||||
id: key
|
||||
run: |
|
||||
PW_VERSION=$(pnpm --silent -- playwright --version | awk '{print $2}')
|
||||
echo "key=${PREFIX}-playwright-${PW_VERSION}" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
env:
|
||||
PREFIX: ${{ runner.os }}-${{ runner.arch }}
|
||||
|
||||
- name: Cache playwright binaries
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
if: inputs.write-cache == 'true'
|
||||
id: cache
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: ${{ steps.key.outputs.key }}
|
||||
|
||||
# When running in merge queue only restore the cache, never write it
|
||||
- name: Restore playwright binaries cache
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
if: inputs.write-cache != 'true'
|
||||
id: cache-restore
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: ${{ steps.key.outputs.key }}
|
||||
|
||||
- name: Install Playwright browsers
|
||||
if: (steps.cache.outputs.cache-hit || steps.cache-restore.outputs.cache-hit) != 'true'
|
||||
shell: bash
|
||||
run: pnpm playwright install --with-deps
|
||||
|
||||
# Some WebKit dependencies seem to lay outside the cache and will need to be installed separately
|
||||
- name: Install system dependencies for WebKit
|
||||
if: inputs.needs-webkit == 'true' && (steps.cache.outputs.cache-hit || steps.cache-restore.outputs.cache-hit) == 'true'
|
||||
shell: bash
|
||||
run: pnpm playwright install-deps webkit
|
||||
8
.github/renovate.json
vendored
@ -2,6 +2,14 @@
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["github>matrix-org/renovate-config-element-web"],
|
||||
"postUpdateOptions": ["pnpmDedupe"],
|
||||
"packageRules": [
|
||||
{
|
||||
"groupName": "testcontainers docker digests",
|
||||
"groupSlug": "{{manager}}-docker-digests",
|
||||
"matchManagers": ["custom.regex"],
|
||||
"matchPackageNames": ["*"]
|
||||
}
|
||||
],
|
||||
"customManagers": [
|
||||
{
|
||||
"customType": "regex",
|
||||
|
||||
30
.github/workflows/build-and-test.yaml
vendored
@ -18,8 +18,6 @@ on:
|
||||
push:
|
||||
# We do not build on push to develop as the merge_group check handles that
|
||||
branches: [staging, master]
|
||||
repository_dispatch:
|
||||
types: [element-web-notify]
|
||||
|
||||
# support triggering from other workflows
|
||||
workflow_call:
|
||||
@ -155,27 +153,11 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Get installed Playwright version
|
||||
id: playwright
|
||||
run: echo "version=$(pnpm --silent -- playwright --version | awk '{print $2}')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache playwright binaries
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
id: playwright-cache
|
||||
- name: Setup playwright
|
||||
uses: ./.github/actions/setup-playwright
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-playwright-${{ steps.playwright.outputs.version }}
|
||||
|
||||
- name: Install Playwright browsers
|
||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
working-directory: apps/web
|
||||
run: pnpm playwright install --with-deps --no-shell
|
||||
|
||||
- name: Install system dependencies for WebKit
|
||||
# Some WebKit dependencies seem to lay outside the cache and will need to be installed separately
|
||||
if: matrix.project == 'WebKit' && steps.playwright-cache.outputs.cache-hit == 'true'
|
||||
working-directory: apps/web
|
||||
run: pnpm playwright install-deps webkit
|
||||
needs-webkit: ${{ matrix.project == 'WebKit' }}
|
||||
write-cache: ${{ github.event_name != 'merge_group' }}
|
||||
|
||||
# We skip tests tagged with @mergequeue when running on PRs, but run them in MQ and everywhere else
|
||||
- name: Run Playwright tests
|
||||
@ -204,6 +186,7 @@ jobs:
|
||||
uses: element-hq/element-modules/.github/workflows/reusable-playwright-tests.yml@main # zizmor: ignore[unpinned-uses]
|
||||
with:
|
||||
webapp-artifact: webapp
|
||||
reporter: blob
|
||||
|
||||
prepare_ed:
|
||||
name: "Prepare Element Desktop"
|
||||
@ -214,7 +197,7 @@ jobs:
|
||||
contents: read
|
||||
with:
|
||||
config: ${{ (github.event.pull_request.base.ref || github.ref_name) == 'develop' && 'element.io/nightly' || 'element.io/release' }}
|
||||
version: ${{ (github.event.pull_request.base.ref || github.ref_name) == 'develop' && 'develop' || '' }}
|
||||
version: ${{ case((github.event.pull_request.base.ref || github.ref_name) == 'develop' || github.event_name == 'merge_group', 'develop', '') }}
|
||||
webapp-artifact: webapp
|
||||
|
||||
build_ed_windows:
|
||||
@ -262,6 +245,7 @@ jobs:
|
||||
needs:
|
||||
- playwright_ew
|
||||
- downstream-modules
|
||||
- prepare_ed
|
||||
- build_ed_windows
|
||||
- build_ed_linux
|
||||
- build_ed_macos
|
||||
|
||||
2
.github/workflows/build_desktop_test.yaml
vendored
@ -38,7 +38,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
repository: ${{ github.repository == 'element-hq/element-web-pro' && 'element-hq/element-web' || github.repository }}
|
||||
repository: element-hq/element-web
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
|
||||
2
.github/workflows/build_develop.yml
vendored
@ -111,7 +111,7 @@ jobs:
|
||||
running-workflow-name: "Build & Deploy develop.element.io"
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
wait-interval: 10
|
||||
check-regexp: ^((?!SonarCloud|SonarQube|issue|board|label|Release|prepare|GitHub Pages|Upload).)*$
|
||||
check-regexp: ^((?!SonarCloud|SonarQube|issue|board|label|Release|prepare|GitHub Pages|Upload|Netlify).)*$
|
||||
|
||||
# We keep the latest develop.tar.gz on R2 instead of relying on the github artifact uploaded earlier
|
||||
# as the expires after 24h and requires auth to download.
|
||||
|
||||
29
.github/workflows/merge-queue.yaml
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
# Tweaks the behaviour of Merge Queue to skip certain checks
|
||||
name: Merge Queue tweaks
|
||||
on:
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
run:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
statuses: write
|
||||
steps:
|
||||
# This is only needed as license/cla at time of writing seems to be extraordinarily flaky
|
||||
# and Github doesn't support conditional checks between PR & merge queue.
|
||||
# This is fine to do as a PR won't make it to merge queue until it has license/cla passing.
|
||||
- name: Skip license/cla on merge queues
|
||||
uses: guibranco/github-status-action-v2@9bfa8773cdbdc6c185747fd43cd7faa9d7c32f09
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: success
|
||||
context: license/cla
|
||||
sha: ${{ github.sha }}
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
3
.github/workflows/release.yml
vendored
@ -30,7 +30,8 @@ jobs:
|
||||
asset-path: dist/*.tar.gz
|
||||
expected-asset-count: 3
|
||||
# Desktop has no dist script so we only target web here
|
||||
dir: apps/web
|
||||
dist-dir: apps/web
|
||||
version-dirs: apps/web apps/desktop
|
||||
|
||||
check:
|
||||
name: Post release checks
|
||||
|
||||
@ -2,7 +2,9 @@
|
||||
# It uploads the received images and diffs to netlify, printing the URLs to the console
|
||||
name: Upload Shared Component Visual Test Diffs
|
||||
on:
|
||||
workflow_run:
|
||||
# Privilege escalation necessary to deploy to Netlify
|
||||
# 🚨 We must not execute any checked out code here.
|
||||
workflow_run: # zizmor: ignore[dangerous-triggers]
|
||||
workflows: ["Shared Component Visual Tests"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
@ -36,22 +36,10 @@ jobs:
|
||||
working-directory: packages/shared-components
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Get installed Playwright version
|
||||
working-directory: packages/shared-components
|
||||
id: playwright
|
||||
run: echo "version=$(pnpm list @playwright/test --depth=0 --json | jq -r '.[].devDependencies["@playwright/test"].version')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache playwright binaries
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
id: playwright-cache
|
||||
- name: Setup playwright
|
||||
uses: ./.github/actions/setup-playwright
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-playwright-${{ steps.playwright.outputs.version }}-onlyshell
|
||||
|
||||
- name: Install Playwright browsers
|
||||
working-directory: packages/shared-components
|
||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
run: "pnpm playwright install --with-deps --only-shell"
|
||||
write-cache: ${{ github.event_name != 'merge_group' }}
|
||||
|
||||
- name: Run Visual tests
|
||||
working-directory: packages/shared-components
|
||||
|
||||
4
.github/workflows/sonarqube.yml
vendored
@ -1,6 +1,8 @@
|
||||
name: SonarQube
|
||||
on:
|
||||
workflow_run:
|
||||
# Privilege escalation necessary to call upon SonarCloud
|
||||
# 🚨 We must not execute any checked out code here.
|
||||
workflow_run: # zizmor: ignore[dangerous-triggers]
|
||||
workflows: ["Tests"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
2
.github/workflows/static_analysis.yaml
vendored
@ -5,8 +5,6 @@ on:
|
||||
branches: [develop, master]
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
repository_dispatch:
|
||||
types: [element-web-notify]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
20
.github/workflows/tests.yml
vendored
@ -5,8 +5,6 @@ on:
|
||||
types: [checks_requested]
|
||||
push:
|
||||
branches: [develop, master]
|
||||
repository_dispatch:
|
||||
types: [element-web-notify]
|
||||
workflow_call:
|
||||
inputs:
|
||||
disable_coverage:
|
||||
@ -151,22 +149,10 @@ jobs:
|
||||
packages/shared-components/node_modules/.vite/vitest
|
||||
key: ${{ hashFiles('pnpm-lock.yaml') }}
|
||||
|
||||
- name: Get installed Playwright version
|
||||
working-directory: packages/shared-components
|
||||
id: playwright
|
||||
run: echo "version=$(pnpm list @playwright/test --depth=0 --json | jq -r '.[].devDependencies["@playwright/test"].version')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache playwright binaries
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
id: playwright-cache
|
||||
- name: Setup playwright
|
||||
uses: ./.github/actions/setup-playwright
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-playwright-${{ steps.playwright.outputs.version }}-onlyshell
|
||||
|
||||
- name: Install Playwright browsers
|
||||
working-directory: packages/shared-components
|
||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
run: "pnpm playwright install --with-deps --only-shell"
|
||||
write-cache: ${{ github.event_name != 'merge_group' }}
|
||||
|
||||
- name: Run tests
|
||||
working-directory: "packages/shared-components"
|
||||
|
||||
29
CHANGELOG.md
@ -1,3 +1,32 @@
|
||||
Changes in [1.12.15](https://github.com/element-hq/element-web/releases/tag/v1.12.15) (2026-04-08)
|
||||
==================================================================================================
|
||||
Fixes Desktop release workflow.
|
||||
|
||||
This release is identical to v1.12.14 otherwise.
|
||||
|
||||
Changes in [1.12.14](https://github.com/element-hq/element-web/releases/tag/v1.12.14) (2026-04-07)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Add analytics tracking for URL previews ([#32659](https://github.com/element-hq/element-web/pull/32659)). Contributed by @Half-Shot.
|
||||
* Collapsible Room List - Clicking on separator should expand to last set width ([#32909](https://github.com/element-hq/element-web/pull/32909)). Contributed by @MidhunSureshR.
|
||||
* RoomList: improve performance ([#32919](https://github.com/element-hq/element-web/pull/32919)). Contributed by @florianduros.
|
||||
* Implement collapsible panels for the new room list ([#32742](https://github.com/element-hq/element-web/pull/32742)). Contributed by @MidhunSureshR.
|
||||
* Hide the names of banned users behind a spoiler tag (attempt 2) ([#32636](https://github.com/element-hq/element-web/pull/32636)). Contributed by @andybalaam.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Use the code signing Subject Name as basis for Tray GUID on Windows ([#32939](https://github.com/element-hq/element-web/pull/32939)). Contributed by @t3chguy.
|
||||
* Ensure the incoming verification request appears above the please verify prompt ([#32931](https://github.com/element-hq/element-web/pull/32931)). Contributed by @andybalaam.
|
||||
* Collapsible Room List - Prevent any interaction with the separator when the panel is expanded ([#32910](https://github.com/element-hq/element-web/pull/32910)). Contributed by @MidhunSureshR.
|
||||
* Fix icon size of badges in right panel ([#32952](https://github.com/element-hq/element-web/pull/32952)). Contributed by @florianduros.
|
||||
* Fix room list often showing the wrong icons for calls ([#32881](https://github.com/element-hq/element-web/pull/32881)). Contributed by @robintown.
|
||||
* Fix emoticon slash commands including stale buffers ([#32928](https://github.com/element-hq/element-web/pull/32928)). Contributed by @t3chguy.
|
||||
* Fix presence indicators not showing without cache ([#32880](https://github.com/element-hq/element-web/pull/32880)). Contributed by @DLCSharp.
|
||||
* Show space name instead of 'Empty room' after creation ([#32886](https://github.com/element-hq/element-web/pull/32886)). Contributed by @gugaribeiro05.
|
||||
* Strip ephemeral query params from OIDC redirect URI ([#32875](https://github.com/element-hq/element-web/pull/32875)). Contributed by @azmeuk.
|
||||
|
||||
|
||||
Changes in [1.12.13](https://github.com/element-hq/element-web/releases/tag/v1.12.13) (2026-03-24)
|
||||
==================================================================================================
|
||||
## 🦖 Deprecations
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
"productName": "Element",
|
||||
"main": "lib/electron-main.js",
|
||||
"exports": "./lib/electron-main.js",
|
||||
"version": "1.12.13",
|
||||
"version": "1.12.15",
|
||||
"description": "Element: the future of secure communication",
|
||||
"author": {
|
||||
"name": "Element",
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
FROM mcr.microsoft.com/playwright:v1.58.2-jammy@sha256:4698a73749c5848d3f5fcd42a2174d172fcad2b2283e087843b115424303a565
|
||||
FROM mcr.microsoft.com/playwright:v1.59.1-jammy@sha256:8a0360d39d1973be506dd59002904a774f6d697d4946c94063b3fd006461c8ff
|
||||
|
||||
WORKDIR /work/element-desktop
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "element-web",
|
||||
"version": "1.12.13",
|
||||
"version": "1.12.15",
|
||||
"description": "Element: the future of secure communication",
|
||||
"author": "New Vector Ltd.",
|
||||
"repository": {
|
||||
@ -67,7 +67,7 @@
|
||||
"emojibase-regex": "^17.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"file-saver": "^2.0.5",
|
||||
"filesize": "11.0.13",
|
||||
"filesize": "11.0.15",
|
||||
"github-markdown-css": "^5.5.1",
|
||||
"glob-to-regexp": "^0.4.1",
|
||||
"highlight.js": "^11.3.1",
|
||||
@ -78,7 +78,7 @@
|
||||
"jsrsasign": "^11.0.0",
|
||||
"jszip": "^3.7.0",
|
||||
"katex": "^0.16.0",
|
||||
"lodash": "npm:lodash-es@^4.17.21",
|
||||
"lodash": "npm:lodash-es@4.18.1",
|
||||
"maplibre-gl": "^5.0.0",
|
||||
"matrix-encrypt-attachment": "^1.0.3",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
||||
@ -89,7 +89,7 @@
|
||||
"opus-recorder": "^8.0.3",
|
||||
"pako": "^2.0.3",
|
||||
"png-chunks-extract": "^1.0.0",
|
||||
"posthog-js": "1.360.2",
|
||||
"posthog-js": "1.364.7",
|
||||
"qrcode": "1.5.4",
|
||||
"re-resizable": "6.11.2",
|
||||
"react": "catalog:",
|
||||
|
||||
61
apps/web/playwright/e2e/devtools/lowbandwidth.spec.ts
Normal file
@ -0,0 +1,61 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { getSampleFilePath } from "../../sample-files";
|
||||
|
||||
test.describe("Devtools", () => {
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
});
|
||||
|
||||
test("should allow enabling low bandwidth mode", async ({ page, homeserver, user, app }) => {
|
||||
// Upload a picture
|
||||
const userSettings = await app.settings.openUserSettings("Account");
|
||||
const profileSettings = userSettings.locator(".mx_UserProfileSettings");
|
||||
await profileSettings.getByAltText("Upload").setInputFiles(getSampleFilePath("riot.png"));
|
||||
await app.closeDialog();
|
||||
|
||||
// Create an initial room.
|
||||
const createRoomDialog = await app.openCreateRoomDialog();
|
||||
await createRoomDialog.getByRole("textbox", { name: "Name" }).fill("Test Room");
|
||||
await createRoomDialog.getByRole("button", { name: "Create room" }).click();
|
||||
|
||||
const composer = app.getComposer().locator("[contenteditable]");
|
||||
await composer.fill("/devtools");
|
||||
await composer.press("Enter");
|
||||
const dialog = page.locator(".mx_Dialog");
|
||||
await dialog.getByLabel("Developer mode").check();
|
||||
await dialog.getByLabel("Disable bandwidth-heavy features").click();
|
||||
// Wait for refresh.
|
||||
await page.waitForEvent("domcontentloaded");
|
||||
await app.viewRoomByName("Test Room");
|
||||
|
||||
// This only appears when encryption has been disabled in the client.
|
||||
await expect(page.getByText("The encryption used by this room isn't supported.")).toBeVisible();
|
||||
|
||||
// None of these should be requested.
|
||||
let hasSentTyping = false;
|
||||
let hasRequestedThumbnail = false;
|
||||
await page.route("**/_matrix/client/v3/rooms/*/typing/*", async (route) => {
|
||||
hasSentTyping = true;
|
||||
await route.fulfill({ json: {} });
|
||||
});
|
||||
await page.route("**/_matrix/media/v3/thumbnail/**", async (route) => {
|
||||
hasRequestedThumbnail = true;
|
||||
await route.fulfill({ json: {} });
|
||||
});
|
||||
await page.route("**/_matrix/client/v1/media/thumbnail/**", async (route) => {
|
||||
hasRequestedThumbnail = true;
|
||||
await route.fulfill({ json: {} });
|
||||
});
|
||||
|
||||
await composer.pressSequentially("Provoke typing request", { delay: 5 });
|
||||
expect(hasSentTyping).toEqual(false);
|
||||
expect(hasRequestedThumbnail).toEqual(false);
|
||||
});
|
||||
});
|
||||
@ -24,7 +24,7 @@ test.describe("Account user settings tab", () => {
|
||||
},
|
||||
});
|
||||
|
||||
test("should be rendered properly", { tag: "@screenshot" }, async ({ uut, user }) => {
|
||||
test("should be rendered properly", { tag: "@screenshot" }, async ({ uut, user, axe }) => {
|
||||
await expect(uut).toMatchScreenshot("account.png");
|
||||
|
||||
// Assert that the top heading is rendered
|
||||
@ -70,6 +70,8 @@ test.describe("Account user settings tab", () => {
|
||||
await expect(accountManagementSection.getByRole("button", { name: "Deactivate Account" })).toHaveClass(
|
||||
/mx_AccessibleButton_kind_danger/,
|
||||
);
|
||||
|
||||
await expect(axe).toHaveNoViolations();
|
||||
});
|
||||
|
||||
test("should respond to small screen sizes", { tag: "@screenshot" }, async ({ page, uut }) => {
|
||||
|
||||
@ -13,7 +13,7 @@ test.describe("Appearance user settings tab", () => {
|
||||
displayName: "Hanako",
|
||||
});
|
||||
|
||||
test("should be rendered properly", { tag: "@screenshot" }, async ({ page, user, app }) => {
|
||||
test("should be rendered properly", { tag: "@screenshot" }, async ({ page, user, app, axe }) => {
|
||||
const tab = await app.settings.openUserSettings("Appearance");
|
||||
|
||||
// Click "Show advanced" link button
|
||||
@ -23,6 +23,8 @@ test.describe("Appearance user settings tab", () => {
|
||||
await expect(tab.getByRole("button", { name: "Hide advanced" })).toBeVisible();
|
||||
|
||||
await expect(tab).toMatchScreenshot("appearance-tab.png");
|
||||
|
||||
await expect(axe).toHaveNoViolations();
|
||||
});
|
||||
|
||||
test(
|
||||
|
||||
@ -23,7 +23,7 @@ test.describe("Appearance user settings tab", () => {
|
||||
test(
|
||||
"should be rendered with the light theme selected",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, util }) => {
|
||||
async ({ page, app, util, axe }) => {
|
||||
// Assert that 'Match system theme' is not checked
|
||||
await expect(util.getMatchSystemThemeSwitch()).not.toBeChecked();
|
||||
|
||||
@ -34,6 +34,8 @@ test.describe("Appearance user settings tab", () => {
|
||||
await expect(util.getHighContrastTheme()).not.toBeChecked();
|
||||
|
||||
await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-light.png");
|
||||
|
||||
await expect(axe).toHaveNoViolations();
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@ test.describe("Device manager", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("should display sessions", async ({ page, app }) => {
|
||||
test("should display sessions", async ({ page, app, axe }) => {
|
||||
await app.settings.openUserSettings("Sessions");
|
||||
const tab = page.locator(".mx_SettingsTab");
|
||||
|
||||
@ -85,7 +85,7 @@ test.describe("Device manager", () => {
|
||||
// session name updated in details
|
||||
await expect(firstSession.locator(".mx_DeviceDetailHeading h4").getByText(sessionName)).toBeVisible();
|
||||
// and main list item
|
||||
await expect(firstSession.locator(".mx_DeviceTile h4").getByText(sessionName)).toBeVisible();
|
||||
await expect(firstSession.locator(".mx_DeviceTile h3").getByText(sessionName)).toBeVisible();
|
||||
|
||||
// sign out using the device details sign out
|
||||
await firstSession.getByRole("button", { name: "Remove this session" }).click();
|
||||
@ -96,5 +96,7 @@ test.describe("Device manager", () => {
|
||||
// no other sessions or security recommendations sections when only one session
|
||||
await expect(tab.getByText("Other sessions")).not.toBeVisible();
|
||||
await expect(tab.getByTestId("security-recommendations-section")).not.toBeVisible();
|
||||
|
||||
await expect(axe).toHaveNoViolations();
|
||||
});
|
||||
});
|
||||
|
||||
@ -16,7 +16,7 @@ test.describe("Advanced section in Encryption tab", () => {
|
||||
await bootstrapCrossSigningForClient(clientHandle, credentials, true);
|
||||
});
|
||||
|
||||
test("should show the encryption details", { tag: "@screenshot" }, async ({ page, app, util }) => {
|
||||
test("should show the encryption details", { tag: "@screenshot" }, async ({ page, app, util, axe }) => {
|
||||
await util.openEncryptionTab();
|
||||
const section = util.getEncryptionDetailsSection();
|
||||
|
||||
@ -26,6 +26,8 @@ test.describe("Advanced section in Encryption tab", () => {
|
||||
await expect(section).toMatchScreenshot("encryption-details.png", {
|
||||
mask: [section.getByTestId("deviceId"), section.getByTestId("sessionKey")],
|
||||
});
|
||||
|
||||
await expect(axe).toHaveNoViolations();
|
||||
});
|
||||
|
||||
test("should show the import room keys dialog", async ({ page, app, util }) => {
|
||||
|
||||
@ -20,7 +20,7 @@ test.describe("General room settings tab", () => {
|
||||
await app.viewRoomByName(roomName);
|
||||
});
|
||||
|
||||
test("should be rendered properly", { tag: "@screenshot" }, async ({ page, app }) => {
|
||||
test("should be rendered properly", { tag: "@screenshot" }, async ({ page, app, axe }) => {
|
||||
const settings = await app.settings.openRoomSettings("General");
|
||||
|
||||
// Assert that "Show less" details element is rendered
|
||||
@ -34,6 +34,9 @@ test.describe("General room settings tab", () => {
|
||||
// Assert that "Show more" details element is rendered instead of "Show more"
|
||||
await expect(settings.getByText("Show less")).not.toBeVisible();
|
||||
await expect(settings.getByText("Show more")).toBeVisible();
|
||||
|
||||
axe.disableRules("color-contrast"); // XXX: We have some known contrast issues here
|
||||
await expect(axe).toHaveNoViolations();
|
||||
});
|
||||
|
||||
test("long address should not cause dialog to overflow", { tag: "@no-webkit" }, async ({ page, app, user }) => {
|
||||
|
||||
@ -25,7 +25,7 @@ test.describe("Preferences user settings tab", () => {
|
||||
labsFlags: ["feature_new_room_list"],
|
||||
});
|
||||
|
||||
test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page, user }) => {
|
||||
test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page, user, axe }) => {
|
||||
await page.setViewportSize({ width: 1024, height: 4000 });
|
||||
const tab = await app.settings.openUserSettings("Preferences");
|
||||
// Assert that the top heading is rendered
|
||||
@ -39,6 +39,8 @@ test.describe("Preferences user settings tab", () => {
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
await expect(axe).toHaveNoViolations();
|
||||
});
|
||||
|
||||
test("should be able to change the app language", { tag: ["@no-firefox", "@no-webkit"] }, async ({ uut, user }) => {
|
||||
|
||||
@ -8,11 +8,13 @@ Please see LICENSE files in the repository root for full details.
|
||||
import { test, expect } from "../../element-web-test";
|
||||
|
||||
test.describe("Quick settings menu", () => {
|
||||
test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page, user }) => {
|
||||
test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page, user, axe }) => {
|
||||
await page.getByRole("button", { name: "Quick settings" }).click();
|
||||
// Assert that the top heading is renderedc
|
||||
const settings = page.getByTestId("quick-settings-menu");
|
||||
await expect(settings).toBeVisible();
|
||||
await expect(settings).toMatchScreenshot("quick-settings.png");
|
||||
|
||||
await expect(axe).toHaveNoViolations();
|
||||
});
|
||||
});
|
||||
|
||||
@ -25,7 +25,7 @@ test.describe("Roles & Permissions room settings tab", () => {
|
||||
settings = await app.settings.openRoomSettings("Roles & Permissions");
|
||||
});
|
||||
|
||||
test("should be able to change the role of a user", async ({ page, app, user }) => {
|
||||
test("should be able to change the role of a user", async ({ page, app, user, axe }) => {
|
||||
const privilegedUserSection = settings.locator(".mx_SettingsFieldset").first();
|
||||
const applyButton = privilegedUserSection.getByRole("button", { name: "Apply" });
|
||||
|
||||
@ -55,5 +55,7 @@ test.describe("Roles & Permissions room settings tab", () => {
|
||||
settings = await app.settings.openRoomSettings("Roles & Permissions");
|
||||
combobox = privilegedUserSection.getByRole("combobox", { name: user.userId });
|
||||
await expect(combobox).toHaveValue("50");
|
||||
|
||||
await expect(axe).toHaveNoViolations();
|
||||
});
|
||||
});
|
||||
|
||||
@ -32,12 +32,14 @@ test.describe("Security user settings tab", () => {
|
||||
});
|
||||
|
||||
test.describe("AnalyticsLearnMoreDialog", () => {
|
||||
test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page, user }) => {
|
||||
test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page, user, axe }) => {
|
||||
const tab = await app.settings.openUserSettings("Security");
|
||||
await tab.getByRole("button", { name: "Learn more" }).click();
|
||||
await expect(page.locator(".mx_AnalyticsLearnMoreDialog_wrapper .mx_Dialog")).toMatchScreenshot(
|
||||
"Security-user-settings-tab-with-posthog-enable-b5d89-csLearnMoreDialog-should-be-rendered-properly-1.png",
|
||||
);
|
||||
|
||||
await expect(axe).toHaveNoViolations();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import type { Page, Request, Route } from "@playwright/test";
|
||||
import type { Page, Request, Route, Disposable } from "@playwright/test";
|
||||
import type { Client } from "./client";
|
||||
|
||||
/**
|
||||
@ -16,7 +16,7 @@ import type { Client } from "./client";
|
||||
*/
|
||||
export class Network {
|
||||
private isOffline = false;
|
||||
private setupPromise?: Promise<void>;
|
||||
private setupPromise?: Promise<Disposable>;
|
||||
|
||||
constructor(
|
||||
private page: Page,
|
||||
|
||||
|
Before Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 956 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 259 KiB After Width: | Height: | Size: 268 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 149 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 167 KiB After Width: | Height: | Size: 167 KiB |
@ -11,7 +11,7 @@ import {
|
||||
} from "@element-hq/element-web-playwright-common/lib/testcontainers/index.js";
|
||||
|
||||
const DOCKER_IMAGE =
|
||||
"ghcr.io/element-hq/matrix-authentication-service:main@sha256:f54c2214354ec3294694a525523debb5f38c8580c1a5afc8cdec0f8372374ef3";
|
||||
"ghcr.io/element-hq/matrix-authentication-service:main@sha256:b7bbeb4249bf4abfc86ccd6d1be60c3b68ccb41be407b2a50658f7ff53a44d80";
|
||||
|
||||
/**
|
||||
* MatrixAuthenticationServiceContainer which freezes the docker digest to
|
||||
|
||||
@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
import { SynapseContainer as BaseSynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers/index.js";
|
||||
|
||||
const DOCKER_IMAGE =
|
||||
"ghcr.io/element-hq/synapse:develop@sha256:73fe964d854412cd905f4f0e668b2a9d10edc86891036e03656714de0311f6f6";
|
||||
"ghcr.io/element-hq/synapse:develop@sha256:2ccad8f89462d119d910fbf91750477118a875a767d19e6387f750f8e8a9a5ae";
|
||||
|
||||
/**
|
||||
* SynapseContainer which freezes the docker digest to stabilise tests,
|
||||
|
||||
@ -70,6 +70,7 @@
|
||||
@import "./structures/_MatrixChat.pcss";
|
||||
@import "./structures/_MessagePanel.pcss";
|
||||
@import "./structures/_NonUrgentToastContainer.pcss";
|
||||
@import "./structures/_PictureInPictureDragger.pcss";
|
||||
@import "./structures/_QuickSettingsButton.pcss";
|
||||
@import "./structures/_RightPanel.pcss";
|
||||
@import "./structures/_RoomSearch.pcss";
|
||||
@ -375,7 +376,6 @@
|
||||
@import "./views/voip/_DialPad.pcss";
|
||||
@import "./views/voip/_DialPadContextMenu.pcss";
|
||||
@import "./views/voip/_DialPadModal.pcss";
|
||||
@import "./views/voip/_LegacyCallPreview.pcss";
|
||||
@import "./views/voip/_LegacyCallView.pcss";
|
||||
@import "./views/voip/_LegacyCallViewForRoom.pcss";
|
||||
@import "./views/voip/_LegacyCallViewHeader.pcss";
|
||||
|
||||
20
apps/web/res/css/structures/_PictureInPictureDragger.pcss
Normal file
@ -0,0 +1,20 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
.mx_PictureInPictureDragger {
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
/* Display above any widget elements */
|
||||
z-index: 102;
|
||||
}
|
||||
|
||||
.mx_PictureInPictureDragger:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
@ -1378,6 +1378,10 @@ $left-gutter: 64px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.mx_EventTile_annotatedInline {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.mx_EventTile_footer {
|
||||
display: flex;
|
||||
gap: var(--cpd-space-2x);
|
||||
|
||||
@ -36,5 +36,5 @@ Please see LICENSE files in the repository root for full details.
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: $spacing-32;
|
||||
|
||||
padding: $spacing-16 0;
|
||||
margin: $spacing-16 0;
|
||||
}
|
||||
|
||||
@ -1,28 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
.mx_LegacyCallPreview {
|
||||
align-items: flex-end;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-16;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
/* Display above any widget elements */
|
||||
z-index: 102;
|
||||
|
||||
.mx_VideoFeed_remote.mx_VideoFeed_voice {
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
.mx_VideoFeed_local {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
@ -103,10 +103,14 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
|
||||
|
||||
private async makeRecorder(): Promise<void> {
|
||||
try {
|
||||
const requestedDeviceId = MediaDeviceHandler.getAudioInput();
|
||||
const deviceIdConstraint =
|
||||
requestedDeviceId && requestedDeviceId !== "default" ? { deviceId: { exact: requestedDeviceId } } : {};
|
||||
|
||||
this.recorderStream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: {
|
||||
channelCount: CHANNELS,
|
||||
deviceId: MediaDeviceHandler.getAudioInput(),
|
||||
...deviceIdConstraint,
|
||||
autoGainControl: { ideal: MediaDeviceHandler.getAudioAutoGainControl() },
|
||||
echoCancellation: { ideal: MediaDeviceHandler.getAudioEchoCancellation() },
|
||||
noiseSuppression: { ideal: MediaDeviceHandler.getAudioNoiseSuppression() },
|
||||
|
||||
@ -8,7 +8,15 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type JSX, type CSSProperties, type RefObject, type SyntheticEvent, useRef, useState } from "react";
|
||||
import React, {
|
||||
type JSX,
|
||||
type CSSProperties,
|
||||
type RefObject,
|
||||
type SyntheticEvent,
|
||||
useRef,
|
||||
useState,
|
||||
type AriaRole,
|
||||
} from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import classNames from "classnames";
|
||||
import FocusLock from "react-focus-lock";
|
||||
@ -74,27 +82,31 @@ export interface MenuProps extends IPosition {
|
||||
|
||||
export interface IProps extends MenuProps {
|
||||
// If true, insert an invisible screen-sized element behind the menu that when clicked will close it.
|
||||
hasBackground?: boolean;
|
||||
"hasBackground"?: boolean;
|
||||
// whether this context menu should be focus managed. If false it must handle itself
|
||||
managed?: boolean;
|
||||
wrapperClassName?: string;
|
||||
menuClassName?: string;
|
||||
"managed"?: boolean;
|
||||
"wrapperClassName"?: string;
|
||||
"menuClassName"?: string;
|
||||
|
||||
// If true, this context menu will be mounted as a child to the parent container. Otherwise
|
||||
// it will be mounted to a container at the root of the DOM.
|
||||
mountAsChild?: boolean;
|
||||
"mountAsChild"?: boolean;
|
||||
|
||||
// If specified, contents will be wrapped in a FocusLock, this is only needed if the context menu is being rendered
|
||||
// within an existing FocusLock e.g inside a modal.
|
||||
focusLock?: boolean;
|
||||
"focusLock"?: boolean;
|
||||
|
||||
// call onFinished on any interaction with the menu
|
||||
closeOnInteraction?: boolean;
|
||||
"closeOnInteraction"?: boolean;
|
||||
|
||||
// Function to be called on menu close
|
||||
onFinished(this: void): void;
|
||||
// on resize callback
|
||||
windowResize?(this: void): void;
|
||||
|
||||
// Role & label for accessibility
|
||||
"role"?: AriaRole;
|
||||
"aria-label"?: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
@ -257,9 +269,11 @@ export default class ContextMenu extends React.PureComponent<React.PropsWithChil
|
||||
focusLock,
|
||||
managed,
|
||||
wrapperClassName,
|
||||
chevronFace: propsChevronFace,
|
||||
chevronOffset: propsChevronOffset,
|
||||
"chevronFace": propsChevronFace,
|
||||
"chevronOffset": propsChevronOffset,
|
||||
mountAsChild,
|
||||
role,
|
||||
"aria-label": ariaLabel,
|
||||
...props
|
||||
} = this.props;
|
||||
|
||||
@ -424,6 +438,8 @@ export default class ContextMenu extends React.PureComponent<React.PropsWithChil
|
||||
onClick={this.onClick}
|
||||
onKeyDown={onKeyDownHandler}
|
||||
onContextMenu={this.onContextMenuPreventBubbling}
|
||||
role={role}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
{background}
|
||||
<TooltipProvider>
|
||||
|
||||
@ -37,9 +37,7 @@ interface IChildrenOptions {
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
className?: string;
|
||||
children: Array<CreatePipChildren>;
|
||||
draggable: boolean;
|
||||
onDoubleClick?: () => void;
|
||||
onMove?: () => void;
|
||||
}
|
||||
@ -181,9 +179,6 @@ export default class PictureInPictureDragger extends React.Component<IProps> {
|
||||
};
|
||||
|
||||
private onStartMoving = (event: React.MouseEvent | MouseEvent): void => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this.mouseHeld = true;
|
||||
this.startingPositionX = event.clientX;
|
||||
this.startingPositionY = event.clientY;
|
||||
@ -217,9 +212,6 @@ export default class PictureInPictureDragger extends React.Component<IProps> {
|
||||
private onEndMoving = (event: MouseEvent): void => {
|
||||
if (!this.mouseHeld) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this.mouseHeld = false;
|
||||
// Delaying this to the next event loop tick is necessary for click
|
||||
// event cancellation to work
|
||||
@ -250,7 +242,7 @@ export default class PictureInPictureDragger extends React.Component<IProps> {
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={this.props.className}
|
||||
className="mx_PictureInPictureDragger"
|
||||
style={style}
|
||||
ref={this.callViewWrapper}
|
||||
onClickCapture={this.onClickCapture}
|
||||
|
||||
@ -266,12 +266,7 @@ class PipContainerInner extends React.Component<IProps, IState> {
|
||||
|
||||
if (pipContent.length) {
|
||||
return (
|
||||
<PictureInPictureDragger
|
||||
className="mx_LegacyCallPreview"
|
||||
draggable={pipMode}
|
||||
onDoubleClick={this.onDoubleClick}
|
||||
onMove={this.onMove}
|
||||
>
|
||||
<PictureInPictureDragger onDoubleClick={this.onDoubleClick} onMove={this.onMove}>
|
||||
{pipContent}
|
||||
</PictureInPictureDragger>
|
||||
);
|
||||
|
||||
@ -1380,12 +1380,12 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return;
|
||||
|
||||
if (ev.getType() === "org.matrix.room.preview_urls") {
|
||||
this.updatePreviewUrlVisibility(room);
|
||||
this.updatePreviewUrlVisibility();
|
||||
}
|
||||
|
||||
if (ev.getType() === "m.room.encryption") {
|
||||
this.updateE2EStatus(room);
|
||||
this.updatePreviewUrlVisibility(room);
|
||||
this.updatePreviewUrlVisibility();
|
||||
}
|
||||
|
||||
// ignore anything but real-time updates at the end of the room:
|
||||
@ -1541,15 +1541,14 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
});
|
||||
}
|
||||
|
||||
private updatePreviewUrlVisibility(room: Room): void {
|
||||
private updatePreviewUrlVisibility(): void {
|
||||
this.setState(({ isRoomEncrypted }) => ({
|
||||
showUrlPreview: this.getPreviewUrlVisibility(room, isRoomEncrypted),
|
||||
showUrlPreview: this.getPreviewUrlVisibility(isRoomEncrypted),
|
||||
}));
|
||||
}
|
||||
|
||||
private getPreviewUrlVisibility({ roomId }: Room, isRoomEncrypted: boolean | null): boolean {
|
||||
const key = isRoomEncrypted ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled";
|
||||
return SettingsStore.getValue(key, roomId);
|
||||
private getPreviewUrlVisibility(isRoomEncrypted: boolean | null): boolean {
|
||||
return SettingsStore.getValue(isRoomEncrypted ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled");
|
||||
}
|
||||
|
||||
private onRoom = (room: Room): void => {
|
||||
@ -1608,9 +1607,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
}
|
||||
|
||||
private onUrlPreviewsEnabledChange = (): void => {
|
||||
if (this.state.room) {
|
||||
this.updatePreviewUrlVisibility(this.state.room);
|
||||
}
|
||||
this.updatePreviewUrlVisibility();
|
||||
};
|
||||
|
||||
private onRoomStateEvents = async (ev: MatrixEvent, state: RoomState): Promise<void> => {
|
||||
@ -1638,7 +1635,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
|
||||
this.setState({
|
||||
isRoomEncrypted,
|
||||
showUrlPreview: this.getPreviewUrlVisibility(room, isRoomEncrypted),
|
||||
showUrlPreview: this.getPreviewUrlVisibility(isRoomEncrypted),
|
||||
...(newE2EStatus && { e2eStatus: newE2EStatus }),
|
||||
});
|
||||
}
|
||||
|
||||
@ -115,6 +115,7 @@ const DevtoolsDialog: React.FC<IProps> = ({ roomId, threadRootId, onFinished })
|
||||
<SettingsFlag name="developerMode" level={SettingLevel.ACCOUNT} />
|
||||
<SettingsFlag name="showHiddenEventsInTimeline" level={SettingLevel.DEVICE} />
|
||||
<SettingsFlag name="enableWidgetScreenshots" level={SettingLevel.ACCOUNT} />
|
||||
<SettingsFlag name="lowBandwidth" level={SettingLevel.DEVICE} />
|
||||
</Form.Root>
|
||||
{/* The settings field needs to be outside `Form.Root` because `SettingsField` will have a inner Form,
|
||||
Otherwise we end up with a nester `Form` and that prohibits `preventDefault` so setting the value
|
||||
|
||||
@ -25,6 +25,7 @@ interface IProps {
|
||||
label?: string;
|
||||
isExplicit?: boolean;
|
||||
hideIfCannotSet?: boolean;
|
||||
requires?: BooleanSettingKey[];
|
||||
onChange?(checked: boolean): void;
|
||||
}
|
||||
|
||||
@ -45,6 +46,12 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
|
||||
|
||||
public componentDidMount(): void {
|
||||
defaultWatchManager.watchSetting(this.props.name, this.props.roomId ?? null, this.onSettingChange);
|
||||
if (this.props.requires) {
|
||||
// If we have any dependencies for this feature, also watch those features to ensure we catch the disabled state.
|
||||
for (const flag of this.props.requires) {
|
||||
defaultWatchManager.watchSetting(flag, this.props.roomId ?? null, this.onSettingChange);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
|
||||
@ -301,6 +301,9 @@ class InnerTextualBody extends React.Component<Props> {
|
||||
const isCaption = [MsgType.Image, MsgType.File, MsgType.Audio, MsgType.Video].includes(
|
||||
content.msgtype as MsgType,
|
||||
);
|
||||
const annotatedClassName = isEmote
|
||||
? "mx_EventTile_annotated mx_EventTile_annotatedInline"
|
||||
: "mx_EventTile_annotated";
|
||||
|
||||
const willHaveWrapper =
|
||||
this.props.replacingEventId || this.props.isSeeingThroughMessageHiddenForModeration || isEmote;
|
||||
@ -315,7 +318,7 @@ class InnerTextualBody extends React.Component<Props> {
|
||||
|
||||
if (this.props.replacingEventId) {
|
||||
body = (
|
||||
<div dir="auto" className="mx_EventTile_annotated">
|
||||
<div dir="auto" className={annotatedClassName}>
|
||||
{body}
|
||||
{this.renderEditedMarker()}
|
||||
</div>
|
||||
@ -323,7 +326,7 @@ class InnerTextualBody extends React.Component<Props> {
|
||||
}
|
||||
if (this.props.isSeeingThroughMessageHiddenForModeration) {
|
||||
body = (
|
||||
<div dir="auto" className="mx_EventTile_annotated">
|
||||
<div dir="auto" className={annotatedClassName}>
|
||||
{body}
|
||||
{this.renderPendingModerationMarker()}
|
||||
</div>
|
||||
|
||||
@ -1,92 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2020-2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type ReactNode } from "react";
|
||||
import classNames from "classnames";
|
||||
import { type MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import Modal from "../../../Modal";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import ViewSource from "../../structures/ViewSource";
|
||||
import { type Layout } from "../../../settings/enums/Layout";
|
||||
import { BugReportDialogButton } from "../elements/BugReportDialogButton";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
layout: Layout;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
export default class TileErrorBoundary extends React.Component<IProps, IState> {
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
public static getDerivedStateFromError(error: Error): Partial<IState> {
|
||||
// Side effects are not permitted here, so we only update the state so
|
||||
// that the next render shows an error message.
|
||||
return { error };
|
||||
}
|
||||
|
||||
private onViewSource = (): void => {
|
||||
Modal.createDialog(
|
||||
ViewSource,
|
||||
{
|
||||
mxEvent: this.props.mxEvent,
|
||||
},
|
||||
"mx_Dialog_viewsource",
|
||||
);
|
||||
};
|
||||
|
||||
public render(): ReactNode {
|
||||
if (this.state.error) {
|
||||
const { mxEvent } = this.props;
|
||||
const classes = {
|
||||
mx_EventTile: true,
|
||||
mx_EventTile_info: true,
|
||||
mx_EventTile_content: true,
|
||||
mx_EventTile_tileError: true,
|
||||
};
|
||||
|
||||
let viewSourceButton;
|
||||
if (mxEvent && SettingsStore.getValue("developerMode")) {
|
||||
viewSourceButton = (
|
||||
<>
|
||||
|
||||
<AccessibleButton onClick={this.onViewSource} kind="link">
|
||||
{_t("action|view_source")}
|
||||
</AccessibleButton>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li className={classNames(classes)} data-layout={this.props.layout}>
|
||||
<div className="mx_EventTile_line">
|
||||
<span>
|
||||
{_t("timeline|error_rendering_message")}
|
||||
{mxEvent && ` (${mxEvent.getType()})`}
|
||||
<BugReportDialogButton error={this.state.error} label="react-tile-soft-crash" />
|
||||
{viewSourceButton}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
@ -1,152 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
Copyright 2017 Travis Ralston
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type ReactNode, type JSX } from "react";
|
||||
import { type Room } from "matrix-js-sdk/src/matrix";
|
||||
import { InlineSpinner } from "@vector-im/compound-web";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||
import SettingsFlag from "../elements/SettingsFlag";
|
||||
import SettingsFieldset from "../settings/SettingsFieldset";
|
||||
import AccessibleButton, { type ButtonEvent } from "../elements/AccessibleButton";
|
||||
import { useIsEncrypted } from "../../../hooks/useIsEncrypted.ts";
|
||||
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext.tsx";
|
||||
import { useSettingValueAt } from "../../../hooks/useSettings.ts";
|
||||
|
||||
/**
|
||||
* The URL preview settings for a room
|
||||
*/
|
||||
interface UrlPreviewSettingsProps {
|
||||
/**
|
||||
* The room.
|
||||
*/
|
||||
room: Room;
|
||||
}
|
||||
|
||||
export function UrlPreviewSettings({ room }: UrlPreviewSettingsProps): JSX.Element {
|
||||
const { roomId } = room;
|
||||
const matrixClient = useMatrixClientContext();
|
||||
const isEncrypted = useIsEncrypted(matrixClient, room);
|
||||
const isLoading = isEncrypted === null;
|
||||
|
||||
return (
|
||||
<SettingsFieldset
|
||||
legend={_t("room_settings|general|url_previews_section")}
|
||||
description={!isLoading && <Description isEncrypted={isEncrypted} />}
|
||||
>
|
||||
{isLoading ? (
|
||||
<InlineSpinner />
|
||||
) : (
|
||||
<>
|
||||
<PreviewsForRoom isEncrypted={isEncrypted} roomId={roomId} />
|
||||
<SettingsFlag
|
||||
name={isEncrypted ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled"}
|
||||
level={SettingLevel.ROOM_DEVICE}
|
||||
roomId={roomId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</SettingsFieldset>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Click handler for the user settings link
|
||||
* @param e
|
||||
*/
|
||||
function onClickUserSettings(e: ButtonEvent): void {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dis.fire(Action.ViewUserSettings);
|
||||
}
|
||||
|
||||
/**
|
||||
* The description for the URL preview settings
|
||||
*/
|
||||
interface DescriptionProps {
|
||||
/**
|
||||
* Whether the room is encrypted
|
||||
*/
|
||||
isEncrypted: boolean;
|
||||
}
|
||||
|
||||
function Description({ isEncrypted }: DescriptionProps): JSX.Element {
|
||||
const urlPreviewsEnabled = useSettingValueAt(SettingLevel.ACCOUNT, "urlPreviewsEnabled");
|
||||
|
||||
let previewsForAccount: ReactNode | undefined;
|
||||
if (isEncrypted) {
|
||||
previewsForAccount = _t("room_settings|general|url_preview_encryption_warning");
|
||||
} else {
|
||||
const button = {
|
||||
a: (sub: string) => (
|
||||
<AccessibleButton kind="link_inline" onClick={onClickUserSettings}>
|
||||
{sub}
|
||||
</AccessibleButton>
|
||||
),
|
||||
};
|
||||
|
||||
previewsForAccount = urlPreviewsEnabled
|
||||
? _t("room_settings|general|user_url_previews_default_on", {}, button)
|
||||
: _t("room_settings|general|user_url_previews_default_off", {}, button);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>{_t("room_settings|general|url_preview_explainer")}</p>
|
||||
<p>{previewsForAccount}</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The description for the URL preview settings
|
||||
*/
|
||||
interface PreviewsForRoomProps {
|
||||
/**
|
||||
* Whether the room is encrypted
|
||||
*/
|
||||
isEncrypted: boolean;
|
||||
/**
|
||||
* The room ID
|
||||
*/
|
||||
roomId: string;
|
||||
}
|
||||
|
||||
function PreviewsForRoom({ isEncrypted, roomId }: PreviewsForRoomProps): JSX.Element | null {
|
||||
const urlPreviewsEnabled = useSettingValueAt(
|
||||
SettingLevel.ACCOUNT,
|
||||
"urlPreviewsEnabled",
|
||||
roomId,
|
||||
/*explicit=*/ true,
|
||||
);
|
||||
if (isEncrypted) return null;
|
||||
|
||||
let previewsForRoom: ReactNode;
|
||||
if (SettingsStore.canSetValue("urlPreviewsEnabled", roomId, SettingLevel.ROOM)) {
|
||||
previewsForRoom = (
|
||||
<SettingsFlag name="urlPreviewsEnabled" level={SettingLevel.ROOM} roomId={roomId} isExplicit={true} />
|
||||
);
|
||||
} else {
|
||||
previewsForRoom = (
|
||||
<div>
|
||||
{urlPreviewsEnabled
|
||||
? _t("room_settings|general|default_url_previews_on")
|
||||
: _t("room_settings|general|default_url_previews_off")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return previewsForRoom;
|
||||
}
|
||||
@ -56,6 +56,8 @@ import {
|
||||
PinnedMessageBadge,
|
||||
ReactionsRowButtonView,
|
||||
ReactionsRowView,
|
||||
TileErrorView,
|
||||
type TileErrorViewLayout,
|
||||
useViewModel,
|
||||
} from "@element-hq/web-shared-components";
|
||||
|
||||
@ -89,7 +91,6 @@ import { DecryptionFailureTracker } from "../../../DecryptionFailureTracker";
|
||||
import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { shouldDisplayReply } from "../../../utils/Reply";
|
||||
import PosthogTrackers from "../../../PosthogTrackers";
|
||||
import TileErrorBoundary from "../messages/TileErrorBoundary";
|
||||
import { haveRendererForEvent, isMessageEvent, renderTile } from "../../../events/EventTileFactory";
|
||||
import ThreadSummary, { ThreadMessagePreview } from "./ThreadSummary";
|
||||
import { ReadReceiptGroup } from "./ReadReceiptGroup";
|
||||
@ -114,9 +115,11 @@ import {
|
||||
MAX_ITEMS_WHEN_LIMITED,
|
||||
ReactionsRowViewModel,
|
||||
} from "../../../viewmodels/room/timeline/event-tile/reactions/ReactionsRowViewModel";
|
||||
import { TileErrorViewModel } from "../../../viewmodels/message-body/TileErrorViewModel";
|
||||
import { EventTileActionBarViewModel } from "../../../viewmodels/room/EventTileActionBarViewModel";
|
||||
import { ThreadListActionBarViewModel } from "../../../viewmodels/room/ThreadListActionBarViewModel";
|
||||
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
|
||||
import { useSettingValue } from "../../../hooks/useSettings";
|
||||
import { DecryptionFailureBodyFactory, RedactedBodyFactory } from "../messages/MBodyFactory";
|
||||
|
||||
export type GetRelationsForEvent = (
|
||||
@ -1571,12 +1574,77 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for the event-tile fallback rendered after the tile error boundary catches a render failure.
|
||||
*/
|
||||
interface EventTileErrorFallbackProps {
|
||||
error: Error;
|
||||
layout: Layout;
|
||||
mxEvent: MatrixEvent;
|
||||
}
|
||||
|
||||
function EventTileErrorFallback({ error, layout, mxEvent }: Readonly<EventTileErrorFallbackProps>): JSX.Element {
|
||||
const developerMode = useSettingValue("developerMode");
|
||||
const vm = useCreateAutoDisposedViewModel(
|
||||
() => new TileErrorViewModel({ error, layout: layout as TileErrorViewLayout, mxEvent, developerMode }),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
vm.setError(error);
|
||||
}, [error, vm]);
|
||||
|
||||
useEffect(() => {
|
||||
vm.setLayout(layout as TileErrorViewLayout);
|
||||
}, [layout, vm]);
|
||||
|
||||
useEffect(() => {
|
||||
vm.setDeveloperMode(developerMode);
|
||||
}, [developerMode, vm]);
|
||||
|
||||
return <TileErrorView vm={vm} className="mx_EventTile mx_EventTile_info mx_EventTile_content" />;
|
||||
}
|
||||
|
||||
interface EventTileErrorBoundaryProps {
|
||||
children: ReactNode;
|
||||
layout: Layout;
|
||||
mxEvent: MatrixEvent;
|
||||
}
|
||||
|
||||
interface EventTileErrorBoundaryState {
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
class EventTileErrorBoundary extends React.Component<EventTileErrorBoundaryProps, EventTileErrorBoundaryState> {
|
||||
public constructor(props: EventTileErrorBoundaryProps) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
public static getDerivedStateFromError(error: Error): Partial<EventTileErrorBoundaryState> {
|
||||
return { error };
|
||||
}
|
||||
|
||||
public render(): ReactNode {
|
||||
if (this.state.error) {
|
||||
return (
|
||||
<EventTileErrorFallback
|
||||
error={this.state.error}
|
||||
layout={this.props.layout}
|
||||
mxEvent={this.props.mxEvent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap all event tiles with the tile error boundary so that any throws even during construction are captured
|
||||
const SafeEventTile = (props: EventTileProps): JSX.Element => {
|
||||
return (
|
||||
<TileErrorBoundary mxEvent={props.mxEvent} layout={props.layout ?? Layout.Group}>
|
||||
<EventTileErrorBoundary mxEvent={props.mxEvent} layout={props.layout ?? Layout.Group}>
|
||||
<UnwrappedEventTile {...props} />
|
||||
</TileErrorBoundary>
|
||||
</EventTileErrorBoundary>
|
||||
);
|
||||
};
|
||||
export default SafeEventTile;
|
||||
|
||||
@ -76,8 +76,10 @@ export default class SetIntegrationManager extends React.Component<EmptyObject,
|
||||
>
|
||||
<div className="mx_SettingsFlag">
|
||||
<div className="mx_SetIntegrationManager_heading_manager">
|
||||
<Heading size="3">{_t("integration_manager|manage_title")}</Heading>
|
||||
<Heading id="mx_SetIntegrationManager_ManagerName" size="4">
|
||||
<Heading as="h2" size="3">
|
||||
{_t("integration_manager|manage_title")}
|
||||
</Heading>
|
||||
<Heading id="mx_SetIntegrationManager_ManagerName" as="h3" size="4">
|
||||
{managerName}
|
||||
</Heading>
|
||||
</div>
|
||||
|
||||
@ -13,6 +13,10 @@ import { Heading } from "@vector-im/compound-web";
|
||||
* The heading for a settings section.
|
||||
*/
|
||||
interface SettingsHeaderProps {
|
||||
/**
|
||||
* The component to render the heading as, defaults to h2
|
||||
*/
|
||||
as?: React.ComponentProps<typeof Heading>["as"];
|
||||
/**
|
||||
* Whether the user has a recommended tag.
|
||||
*/
|
||||
@ -23,12 +27,12 @@ interface SettingsHeaderProps {
|
||||
label: string;
|
||||
}
|
||||
|
||||
export function SettingsHeader({ hasRecommendedTag = false, label }: SettingsHeaderProps): JSX.Element {
|
||||
export function SettingsHeader({ hasRecommendedTag = false, label, as = "h2" }: SettingsHeaderProps): JSX.Element {
|
||||
const classes = classNames("mx_SettingsHeader", {
|
||||
mx_SettingsHeader_recommended: hasRecommendedTag,
|
||||
});
|
||||
return (
|
||||
<Heading className={classes} as="h2" size="sm" weight="semibold">
|
||||
<Heading className={classes} as={as} size="sm" weight="semibold">
|
||||
{label}
|
||||
</Heading>
|
||||
);
|
||||
|
||||
@ -18,7 +18,7 @@ import { type ThirdPartyIdentifier } from "../../../AddThreepid";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
import { AddRemoveThreepids } from "./AddRemoveThreepids";
|
||||
import Heading from "../typography/Heading.tsx";
|
||||
import { SettingsSection } from "./shared/SettingsSection.tsx";
|
||||
|
||||
type LoadingState = "loading" | "loaded" | "error";
|
||||
|
||||
@ -82,8 +82,7 @@ export const UserPersonalInfoSettings: React.FC<UserPersonalInfoSettingsProps> =
|
||||
if (!SettingsStore.getValue(UIFeature.ThirdPartyID)) return null;
|
||||
|
||||
return (
|
||||
<div className="mx_UserPersonalInfoSettings">
|
||||
<Heading size="2">{_t("settings|general|personal_info")}</Heading>
|
||||
<SettingsSection heading={_t("settings|general|personal_info")} className="mx_UserPersonalInfoSettings">
|
||||
<SettingsSubsection
|
||||
heading={_t("settings|general|emails_heading")}
|
||||
stretchContent
|
||||
@ -123,6 +122,6 @@ export const UserPersonalInfoSettings: React.FC<UserPersonalInfoSettingsProps> =
|
||||
/>
|
||||
</ThreepidSectionWrapper>
|
||||
</SettingsSubsection>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
);
|
||||
};
|
||||
|
||||
@ -27,7 +27,7 @@ import AccessibleButton from "../elements/AccessibleButton";
|
||||
import LogoutDialog, { shouldShowLogoutDialog } from "../dialogs/LogoutDialog";
|
||||
import Modal from "../../../Modal";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import Heading from "../typography/Heading.tsx";
|
||||
import { SettingsSection } from "./shared/SettingsSection.tsx";
|
||||
|
||||
const SpinnerToast: React.FC<{ children?: ReactNode }> = ({ children }) => (
|
||||
<>
|
||||
@ -194,54 +194,55 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({
|
||||
const someFieldsDisabled = !canSetDisplayName || !canSetAvatar;
|
||||
|
||||
return (
|
||||
<div className="mx_UserProfileSettings">
|
||||
<Heading size="2">{_t("common|profile")}</Heading>
|
||||
<div>
|
||||
{someFieldsDisabled
|
||||
? _t("settings|general|profile_subtitle_oidc")
|
||||
: _t("settings|general|profile_subtitle")}
|
||||
</div>
|
||||
<div className="mx_UserProfileSettings_profile">
|
||||
<AvatarSetting
|
||||
avatar={avatarURL ?? undefined}
|
||||
avatarAccessibleName={_t("common|user_avatar")}
|
||||
onChange={onAvatarChange}
|
||||
removeAvatar={avatarURL ? onAvatarRemove : undefined}
|
||||
placeholderName={displayName}
|
||||
placeholderId={client.getUserId() ?? ""}
|
||||
disabled={!canSetAvatar}
|
||||
/>
|
||||
<EditInPlace
|
||||
className="mx_UserProfileSettings_profile_displayName"
|
||||
label={_t("settings|general|display_name")}
|
||||
value={displayName}
|
||||
saveButtonLabel={_t("common|save")}
|
||||
cancelButtonLabel={_t("common|cancel")}
|
||||
savedLabel={_t("common|saved")}
|
||||
savingLabel={_t("common|updating")}
|
||||
onChange={onDisplayNameChanged}
|
||||
onCancel={onDisplayNameCancel}
|
||||
onSave={onDisplayNameSave}
|
||||
disabled={!canSetDisplayName}
|
||||
>
|
||||
{displayNameError && <ErrorMessage>{_t("settings|general|display_name_error")}</ErrorMessage>}
|
||||
</EditInPlace>
|
||||
</div>
|
||||
{avatarError && (
|
||||
<Alert title={_t("settings|general|avatar_upload_error_title")} type="critical">
|
||||
{maxUploadSize === undefined
|
||||
? _t("settings|general|avatar_upload_error_text_generic")
|
||||
: _t("settings|general|avatar_upload_error_text", { size: formatBytes(maxUploadSize) })}
|
||||
</Alert>
|
||||
)}
|
||||
{userIdentifier && <UsernameBox username={userIdentifier} />}
|
||||
<Flex gap="var(--cpd-space-4x)" className="mx_UserProfileSettings_profile_buttons">
|
||||
{externalAccountManagementUrl && (
|
||||
<ManageAccountButton externalAccountManagementUrl={externalAccountManagementUrl} />
|
||||
<SettingsSection heading={_t("common|profile")}>
|
||||
<div className="mx_UserProfileSettings">
|
||||
<div>
|
||||
{someFieldsDisabled
|
||||
? _t("settings|general|profile_subtitle_oidc")
|
||||
: _t("settings|general|profile_subtitle")}
|
||||
</div>
|
||||
<div className="mx_UserProfileSettings_profile">
|
||||
<AvatarSetting
|
||||
avatar={avatarURL ?? undefined}
|
||||
avatarAccessibleName={_t("common|user_avatar")}
|
||||
onChange={onAvatarChange}
|
||||
removeAvatar={avatarURL ? onAvatarRemove : undefined}
|
||||
placeholderName={displayName}
|
||||
placeholderId={client.getUserId() ?? ""}
|
||||
disabled={!canSetAvatar}
|
||||
/>
|
||||
<EditInPlace
|
||||
className="mx_UserProfileSettings_profile_displayName"
|
||||
label={_t("settings|general|display_name")}
|
||||
value={displayName}
|
||||
saveButtonLabel={_t("common|save")}
|
||||
cancelButtonLabel={_t("common|cancel")}
|
||||
savedLabel={_t("common|saved")}
|
||||
savingLabel={_t("common|updating")}
|
||||
onChange={onDisplayNameChanged}
|
||||
onCancel={onDisplayNameCancel}
|
||||
onSave={onDisplayNameSave}
|
||||
disabled={!canSetDisplayName}
|
||||
>
|
||||
{displayNameError && <ErrorMessage>{_t("settings|general|display_name_error")}</ErrorMessage>}
|
||||
</EditInPlace>
|
||||
</div>
|
||||
{avatarError && (
|
||||
<Alert title={_t("settings|general|avatar_upload_error_title")} type="critical">
|
||||
{maxUploadSize === undefined
|
||||
? _t("settings|general|avatar_upload_error_text_generic")
|
||||
: _t("settings|general|avatar_upload_error_text", { size: formatBytes(maxUploadSize) })}
|
||||
</Alert>
|
||||
)}
|
||||
<SignOutButton />
|
||||
</Flex>
|
||||
</div>
|
||||
{userIdentifier && <UsernameBox username={userIdentifier} />}
|
||||
<Flex gap="var(--cpd-space-4x)" className="mx_UserProfileSettings_profile_buttons">
|
||||
{externalAccountManagementUrl && (
|
||||
<ManageAccountButton externalAccountManagementUrl={externalAccountManagementUrl} />
|
||||
)}
|
||||
<SignOutButton />
|
||||
</Flex>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -22,7 +22,11 @@ export interface DeviceTileProps {
|
||||
}
|
||||
|
||||
const DeviceTileName: React.FC<{ device: ExtendedDevice }> = ({ device }) => {
|
||||
return <Heading size="4">{device.display_name || device.device_id}</Heading>;
|
||||
return (
|
||||
<Heading as="h3" size="4">
|
||||
{device.display_name || device.device_id}
|
||||
</Heading>
|
||||
);
|
||||
};
|
||||
|
||||
const DeviceTile: React.FC<DeviceTileProps> = ({ device, children, isSelected, onClick }) => {
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import React, { useCallback } from "react";
|
||||
import { InlineField, InlineSpinner, Label, Root, ToggleControl } from "@vector-im/compound-web";
|
||||
import { InlineField, InlineSpinner, Label, Link, Root, ToggleControl } from "@vector-im/compound-web";
|
||||
|
||||
import type { FormEvent } from "react";
|
||||
import { SettingsSection } from "../shared/SettingsSection";
|
||||
@ -56,9 +56,9 @@ export const KeyStoragePanel: React.FC<Props> = ({ onKeyStorageDisableClick }) =
|
||||
}
|
||||
subHeading={_t("settings|encryption|key_storage|description", undefined, {
|
||||
a: (sub) => (
|
||||
<a href={SdkConfig.get("help_key_storage_url")} target="_blank" rel="noreferrer noopener">
|
||||
<Link href={SdkConfig.get("help_key_storage_url")} target="_blank">
|
||||
{sub}
|
||||
</a>
|
||||
</Link>
|
||||
),
|
||||
})}
|
||||
>
|
||||
|
||||
@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import classnames from "classnames";
|
||||
import React, { type HTMLAttributes } from "react";
|
||||
import React, { type ComponentProps, createContext, type HTMLAttributes, useContext } from "react";
|
||||
|
||||
import Heading from "../../typography/Heading";
|
||||
import { SettingsHeader } from "../SettingsHeader";
|
||||
@ -19,15 +19,30 @@ export interface SettingsSectionProps extends HTMLAttributes<HTMLDivElement> {
|
||||
legacy?: boolean;
|
||||
}
|
||||
|
||||
function renderHeading(heading: string | React.ReactNode | undefined, legacy: boolean): React.ReactNode | undefined {
|
||||
type HeadingLevel = 2 | 3 | 4 | 5 | 6;
|
||||
|
||||
/**
|
||||
* React context to correctly set heading levels in nested settings sections dynamically
|
||||
*/
|
||||
export const HeadingLevelContext = createContext<HeadingLevel>(2);
|
||||
|
||||
function SectionHeading({
|
||||
heading,
|
||||
legacy,
|
||||
level,
|
||||
}: {
|
||||
heading: string | React.ReactNode | undefined;
|
||||
legacy: boolean;
|
||||
level: HeadingLevel;
|
||||
}): React.ReactNode | undefined {
|
||||
switch (typeof heading) {
|
||||
case "string":
|
||||
return legacy ? (
|
||||
<Heading as="h2" size="3">
|
||||
<Heading as={`h${level}`} size={(level + 1).toString() as ComponentProps<typeof Heading>["size"]}>
|
||||
{heading}
|
||||
</Heading>
|
||||
) : (
|
||||
<SettingsHeader label={heading} />
|
||||
<SettingsHeader as={`h${level}`} label={heading} />
|
||||
);
|
||||
case "undefined":
|
||||
return undefined;
|
||||
@ -60,22 +75,28 @@ export const SettingsSection: React.FC<SettingsSectionProps> = ({
|
||||
legacy = true,
|
||||
children,
|
||||
...rest
|
||||
}) => (
|
||||
<div
|
||||
{...rest}
|
||||
className={classnames("mx_SettingsSection", className, {
|
||||
mx_SettingsSection_newUi: !legacy,
|
||||
})}
|
||||
>
|
||||
{heading &&
|
||||
(subHeading ? (
|
||||
<div className="mx_SettingsSection_header">
|
||||
{renderHeading(heading, legacy)}
|
||||
{subHeading}
|
||||
</div>
|
||||
) : (
|
||||
renderHeading(heading, legacy)
|
||||
))}
|
||||
{legacy ? <div className="mx_SettingsSection_subSections">{children}</div> : children}
|
||||
</div>
|
||||
);
|
||||
}) => {
|
||||
const level = useContext(HeadingLevelContext);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...rest}
|
||||
className={classnames("mx_SettingsSection", className, {
|
||||
mx_SettingsSection_newUi: !legacy,
|
||||
})}
|
||||
>
|
||||
{heading &&
|
||||
(subHeading ? (
|
||||
<div className="mx_SettingsSection_header">
|
||||
<SectionHeading heading={heading} legacy={legacy} level={level} />
|
||||
{subHeading}
|
||||
</div>
|
||||
) : (
|
||||
<SectionHeading heading={heading} legacy={legacy} level={level} />
|
||||
))}
|
||||
<HeadingLevelContext.Provider value={heading ? ((level + 1) as HeadingLevel) : level}>
|
||||
{legacy ? <div className="mx_SettingsSection_subSections">{children}</div> : children}
|
||||
</HeadingLevelContext.Provider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -7,10 +7,11 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import classNames from "classnames";
|
||||
import React, { type HTMLAttributes } from "react";
|
||||
import React, { type HTMLAttributes, useContext } from "react";
|
||||
import { Form, Separator } from "@vector-im/compound-web";
|
||||
|
||||
import { SettingsSubsectionHeading } from "./SettingsSubsectionHeading";
|
||||
import { HeadingLevelContext } from "./SettingsSection.tsx";
|
||||
|
||||
export interface SettingsSubsectionProps extends HTMLAttributes<HTMLDivElement> {
|
||||
heading?: string | React.ReactNode;
|
||||
@ -45,6 +46,8 @@ export const SettingsSubsection: React.FC<SettingsSubsectionProps> = ({
|
||||
formWrap,
|
||||
...rest
|
||||
}) => {
|
||||
const level = useContext(HeadingLevelContext);
|
||||
|
||||
const content = (
|
||||
<div
|
||||
{...rest}
|
||||
@ -52,7 +55,11 @@ export const SettingsSubsection: React.FC<SettingsSubsectionProps> = ({
|
||||
mx_SettingsSubsection_newUi: !legacy,
|
||||
})}
|
||||
>
|
||||
{typeof heading === "string" ? <SettingsSubsectionHeading heading={heading} /> : <>{heading}</>}
|
||||
{typeof heading === "string" ? (
|
||||
<SettingsSubsectionHeading heading={heading} as={`h${level}`} />
|
||||
) : (
|
||||
<>{heading}</>
|
||||
)}
|
||||
{!!description && (
|
||||
<div className="mx_SettingsSubsection_description">
|
||||
<SettingsSubsectionText>{description}</SettingsSubsectionText>
|
||||
|
||||
@ -18,7 +18,7 @@ export interface SettingsSubsectionHeadingProps extends HTMLAttributes<HTMLDivEl
|
||||
|
||||
export const SettingsSubsectionHeading: React.FC<SettingsSubsectionHeadingProps> = ({
|
||||
heading,
|
||||
as = "h3",
|
||||
as = "h2",
|
||||
children,
|
||||
...rest
|
||||
}) => {
|
||||
|
||||
@ -15,14 +15,11 @@ import RoomProfileSettings from "../../../room_settings/RoomProfileSettings";
|
||||
import AccessibleButton, { type ButtonEvent } from "../../../elements/AccessibleButton";
|
||||
import dis from "../../../../../dispatcher/dispatcher";
|
||||
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
|
||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||
import { UIFeature } from "../../../../../settings/UIFeature";
|
||||
import AliasSettings from "../../../room_settings/AliasSettings";
|
||||
import PosthogTrackers from "../../../../../PosthogTrackers";
|
||||
import { SettingsSubsection } from "../../shared/SettingsSubsection";
|
||||
import SettingsTab from "../SettingsTab";
|
||||
import { SettingsSection } from "../../shared/SettingsSection";
|
||||
import { UrlPreviewSettings } from "../../../room_settings/UrlPreviewSettings";
|
||||
import { MediaPreviewAccountSettings } from "../user/MediaPreviewAccountSettings";
|
||||
|
||||
interface IProps {
|
||||
@ -62,10 +59,6 @@ export default class GeneralRoomSettingsTab extends React.Component<IProps, ISta
|
||||
const canSetCanonical = room.currentState.mayClientSendStateEvent("m.room.canonical_alias", client);
|
||||
const canonicalAliasEv = room.currentState.getStateEvents("m.room.canonical_alias", "") ?? undefined;
|
||||
|
||||
const urlPreviewSettings = SettingsStore.getValue(UIFeature.URLPreviews) ? (
|
||||
<UrlPreviewSettings room={room} />
|
||||
) : null;
|
||||
|
||||
let leaveSection;
|
||||
if (room.getMyMembership() === KnownMembership.Join) {
|
||||
leaveSection = (
|
||||
@ -99,7 +92,6 @@ export default class GeneralRoomSettingsTab extends React.Component<IProps, ISta
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection heading={_t("room_settings|general|other_section")}>
|
||||
{urlPreviewSettings}
|
||||
<SettingsSubsection heading={_t("common|moderation_and_safety")} legacy={false}>
|
||||
<MediaPreviewAccountSettings roomId={room.roomId} />
|
||||
</SettingsSubsection>
|
||||
|
||||
@ -45,7 +45,7 @@ const AccountSection: React.FC<AccountSectionProps> = ({
|
||||
if (!canChangePassword) return <></>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsSection>
|
||||
<SettingsSubsection
|
||||
heading={_t("settings|general|account_section")}
|
||||
stretchContent
|
||||
@ -59,7 +59,7 @@ const AccountSection: React.FC<AccountSectionProps> = ({
|
||||
onFinished={onPasswordChanged}
|
||||
/>
|
||||
</SettingsSubsection>
|
||||
</>
|
||||
</SettingsSection>
|
||||
);
|
||||
};
|
||||
|
||||
@ -179,21 +179,19 @@ const AccountUserSettingsTab: React.FC<IProps> = ({ closeSettingsFn }) => {
|
||||
|
||||
return (
|
||||
<SettingsTab data-testid="mx_AccountUserSettingsTab">
|
||||
<SettingsSection>
|
||||
<UserProfileSettings
|
||||
externalAccountManagementUrl={externalAccountManagementUrl}
|
||||
canSetDisplayName={canSetDisplayName}
|
||||
canSetAvatar={canSetAvatar}
|
||||
/>
|
||||
{(!isAccountManagedExternally || canMake3pidChanges) && (
|
||||
<UserPersonalInfoSettings canMake3pidChanges={canMake3pidChanges} />
|
||||
)}
|
||||
<AccountSection
|
||||
canChangePassword={canChangePassword}
|
||||
onPasswordChanged={onPasswordChanged}
|
||||
onPasswordChangeError={onPasswordChangeError}
|
||||
/>
|
||||
</SettingsSection>
|
||||
<UserProfileSettings
|
||||
externalAccountManagementUrl={externalAccountManagementUrl}
|
||||
canSetDisplayName={canSetDisplayName}
|
||||
canSetAvatar={canSetAvatar}
|
||||
/>
|
||||
{(!isAccountManagedExternally || canMake3pidChanges) && (
|
||||
<UserPersonalInfoSettings canMake3pidChanges={canMake3pidChanges} />
|
||||
)}
|
||||
<AccountSection
|
||||
canChangePassword={canChangePassword}
|
||||
onPasswordChanged={onPasswordChanged}
|
||||
onPasswordChangeError={onPasswordChangeError}
|
||||
/>
|
||||
{accountManagementSection}
|
||||
</SettingsTab>
|
||||
);
|
||||
|
||||
@ -16,7 +16,7 @@ import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||
import SdkConfig from "../../../../../SdkConfig";
|
||||
import BetaCard from "../../../beta/BetaCard";
|
||||
import SettingsFlag from "../../../elements/SettingsFlag";
|
||||
import { type FeatureSettingKey, LabGroup, labGroupNames } from "../../../../../settings/Settings";
|
||||
import { type FeatureSettingKey, type LabGroup, labGroupNames } from "../../../../../settings/Settings";
|
||||
import { EnhancedMap } from "../../../../../utils/maps";
|
||||
import { SettingsSection } from "../../shared/SettingsSection";
|
||||
import { SettingsSubsection, SettingsSubsectionText } from "../../shared/SettingsSubsection";
|
||||
@ -71,10 +71,6 @@ export default class LabsUserSettingsTab extends React.Component<EmptyObject> {
|
||||
.push(<SettingsFlag level={SettingLevel.DEVICE} name={f} key={f} />);
|
||||
});
|
||||
|
||||
groups
|
||||
.getOrCreate(LabGroup.Experimental, [])
|
||||
.push(<SettingsFlag key="lowBandwidth" name="lowBandwidth" level={SettingLevel.DEVICE} />);
|
||||
|
||||
labsSections = (
|
||||
<>
|
||||
{sortBy(Array.from(groups.entries()), "0").map(([group, flags]) => (
|
||||
|
||||
@ -147,11 +147,7 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
|
||||
"showCodeLineNumbers",
|
||||
];
|
||||
|
||||
private static IMAGES_AND_VIDEOS_SETTINGS: BooleanSettingKey[] = [
|
||||
"urlPreviewsEnabled",
|
||||
"autoplayGifs",
|
||||
"autoplayVideo",
|
||||
];
|
||||
private static IMAGES_AND_VIDEOS_SETTINGS: BooleanSettingKey[] = ["autoplayGifs", "autoplayVideo"];
|
||||
|
||||
private static TIMELINE_SETTINGS: BooleanSettingKey[] = [
|
||||
"showTypingNotifications",
|
||||
@ -350,6 +346,19 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
|
||||
{this.renderGroup(PreferencesUserSettingsTab.CODE_BLOCKS_SETTINGS)}
|
||||
</SettingsSubsection>
|
||||
|
||||
<SettingsSubsection
|
||||
heading={_t("settings|preferences|link_previews_heading")}
|
||||
description={_t("settings|preferences|link_previews_description")}
|
||||
formWrap
|
||||
>
|
||||
<SettingsFlag name="urlPreviewsEnabled" level={SettingLevel.DEVICE} />
|
||||
<SettingsFlag
|
||||
name="urlPreviewsEnabled_e2ee"
|
||||
level={SettingLevel.DEVICE}
|
||||
requires={["urlPreviewsEnabled"]}
|
||||
/>
|
||||
</SettingsSubsection>
|
||||
|
||||
<SettingsSubsection heading={_t("settings|preferences|media_heading")} formWrap>
|
||||
{this.renderGroup(PreferencesUserSettingsTab.IMAGES_AND_VIDEOS_SETTINGS)}
|
||||
</SettingsSubsection>
|
||||
|
||||
@ -58,6 +58,8 @@ const QuickSettingsButton: React.FC<{
|
||||
onFinished={closeMenu}
|
||||
managed={false}
|
||||
focusLock={true}
|
||||
role="region"
|
||||
aria-label={_t("quick_settings|title")}
|
||||
>
|
||||
<h2>{_t("quick_settings|title")}</h2>
|
||||
|
||||
|
||||
@ -72,7 +72,7 @@ const QuickThemeSwitcher: React.FC<Props> = ({ requestClose }) => {
|
||||
|
||||
return (
|
||||
<div className="mx_QuickThemeSwitcher">
|
||||
<h4 className="mx_QuickThemeSwitcher_heading">{_t("common|theme")}</h4>
|
||||
<h3 className="mx_QuickThemeSwitcher_heading">{_t("common|theme")}</h3>
|
||||
<Dropdown
|
||||
id="mx_QuickSettingsButton_themePickerDropdown"
|
||||
onOptionChange={onOptionChange}
|
||||
|
||||
@ -828,8 +828,8 @@
|
||||
"invalid_device_key_id": "Invalid device key ID",
|
||||
"invalid_json": "Doesn't look like valid JSON.",
|
||||
"level": "Level",
|
||||
"low_bandwidth_mode": "Low bandwidth mode",
|
||||
"low_bandwidth_mode_description": "Requires compatible homeserver.",
|
||||
"low_bandwidth_mode": "Disable bandwidth-heavy features",
|
||||
"low_bandwidth_mode_description": "Disables encryption, presence, avatars, read receipts, and typing notifications",
|
||||
"main_timeline": "Main timeline",
|
||||
"manual_device_verification": "Manual device verification",
|
||||
"no_receipt_found": "No receipt found",
|
||||
@ -2238,8 +2238,6 @@
|
||||
"aliases_section": "Room Addresses",
|
||||
"avatar_field_label": "Room avatar",
|
||||
"canonical_alias_field_label": "Main address",
|
||||
"default_url_previews_off": "URL previews are disabled by default for participants in this room.",
|
||||
"default_url_previews_on": "URL previews are enabled by default for participants in this room.",
|
||||
"description_space": "Edit settings relating to your space.",
|
||||
"error_creating_alias_description": "There was an error creating that address. It may not be allowed by the server or a temporary failure occurred.",
|
||||
"error_creating_alias_title": "Error creating address",
|
||||
@ -2270,12 +2268,7 @@
|
||||
"published_aliases_explainer_space": "Published addresses can be used by anyone on any server to join your space.",
|
||||
"published_aliases_section": "Published Addresses",
|
||||
"save": "Save Changes",
|
||||
"topic_field_label": "Room Topic",
|
||||
"url_preview_encryption_warning": "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.",
|
||||
"url_preview_explainer": "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.",
|
||||
"url_previews_section": "URL Previews",
|
||||
"user_url_previews_default_off": "You have <a>disabled</a> URL previews by default.",
|
||||
"user_url_previews_default_on": "You have <a>enabled</a> URL previews by default."
|
||||
"topic_field_label": "Room Topic"
|
||||
},
|
||||
"notifications": {
|
||||
"browse_button": "Browse",
|
||||
@ -2697,9 +2690,8 @@
|
||||
"unable_to_load_msisdns": "Unable to load phone numbers",
|
||||
"username": "Username"
|
||||
},
|
||||
"inline_url_previews_default": "Enable inline URL previews by default",
|
||||
"inline_url_previews_room": "Enable URL previews by default for participants in this room",
|
||||
"inline_url_previews_room_account": "Enable URL previews for this room (only affects you)",
|
||||
"inline_url_previews_default": "Enable previews",
|
||||
"inline_url_previews_encrypted": "Enable previews in encrypted rooms",
|
||||
"insert_trailing_colon_mentions": "Insert a trailing colon after user mentions at the start of a message",
|
||||
"invite_controls": {
|
||||
"default_label": "Allow users to invite you to rooms"
|
||||
@ -2837,6 +2829,8 @@
|
||||
"enable_tray_icon": "Show tray icon and minimise window to it on close",
|
||||
"keyboard_heading": "Keyboard shortcuts",
|
||||
"keyboard_view_shortcuts_button": "To view all keyboard shortcuts, <a>click here</a>.",
|
||||
"link_previews_description": "Shows information about links underneath messages",
|
||||
"link_previews_heading": "Link previews",
|
||||
"media_heading": "Images, GIFs and videos",
|
||||
"presence_description": "Share your activity and status with others.",
|
||||
"publish_timezone": "Publish timezone on public profile",
|
||||
@ -4003,6 +3997,7 @@
|
||||
"change_name_this_room": "Change the name of this room",
|
||||
"change_topic_active_room": "Change the topic of your active room",
|
||||
"change_topic_this_room": "Change the topic of this room",
|
||||
"download_file": "Download files from the media repository",
|
||||
"receive_membership_active_room": "See when people join, leave, or are invited to your active room",
|
||||
"receive_membership_this_room": "See when people join, leave, or are invited to this room",
|
||||
"remove_ban_invite_leave_active_room": "Remove, ban, or invite people to your active room, and make you leave",
|
||||
|
||||
@ -1546,7 +1546,6 @@
|
||||
"mjolnir": "Új lehetőség emberek figyelmen kívül hagyására",
|
||||
"msc3531_hide_messages_pending_moderation": "A moderátorok kitakarhatják a még nem moderált üzeneteket.",
|
||||
"new_room_list": "Új szobalista engedélyezése",
|
||||
"new_timeline": "Új idővonal (MVVM)",
|
||||
"notification_settings": "Új értesítési beállítások",
|
||||
"notification_settings_beta_caption": "Bemutatjuk az értesítési beállítások módosításának egyszerűbb módját. Testreszabhatja a sajátját, ahogy tetszik itt: %(brand)s.",
|
||||
"notification_settings_beta_title": "Értesítési beállítások",
|
||||
|
||||
@ -115,7 +115,7 @@
|
||||
"show_advanced": "Показать дополнительные настройки",
|
||||
"show_all": "Показать все",
|
||||
"sign_in": "Войти",
|
||||
"sign_out": "Выйти",
|
||||
"sign_out": "Удалить это устройство",
|
||||
"skip": "Пропустить",
|
||||
"start": "Начать",
|
||||
"start_chat": "Отправить личное сообщение",
|
||||
@ -405,7 +405,7 @@
|
||||
"download_logs": "Скачать журналы",
|
||||
"downloading_logs": "Скачивание журналов",
|
||||
"error_empty": "Пожалуйста, расскажите нам что пошло не так, либо, ещё лучше, создайте отчёт в GitHub с описанием проблемы.",
|
||||
"failed_download_logs": "Не удалось загрузить журналы отладки: ",
|
||||
"failed_download_logs": "Не удалось скачать журналы отладки: ",
|
||||
"failed_send_logs_causes": {
|
||||
"disallowed_app": "Ваш отчет об ошибке отклонен. Сервер rageshake не поддерживает это приложение.",
|
||||
"rejected_generic": "Ваш отчет об ошибке отклонен. Сервер rageshake отклонил содержимое отчета из-за политики.",
|
||||
@ -419,7 +419,7 @@
|
||||
"log_request": "Чтобы помочь нам предотвратить это в будущем, пожалуйста, <a>отправьте нам логи</a>.",
|
||||
"logs_sent": "Журналы отправлены",
|
||||
"matrix_security_issue": "Чтобы сообщить о проблеме безопасности Matrix, пожалуйста, прочитайте <a>Политику раскрытия информации</a> Matrix.org.",
|
||||
"preparing_download": "Подготовка к загрузке журналов",
|
||||
"preparing_download": "Подготовка к скачиванию журналов",
|
||||
"preparing_logs": "Подготовка к отправке журналов",
|
||||
"send_logs": "Отправить журналы",
|
||||
"submit_debug_logs": "Отправить отладочные журналы",
|
||||
@ -563,6 +563,7 @@
|
||||
"someone": "Кто-то",
|
||||
"space": "Пространство",
|
||||
"spaces": "Пространства",
|
||||
"state_encryption_enabled": "Экспериментальное шифрование включено",
|
||||
"sticker": "Стикер",
|
||||
"stickerpack": "Набор стикеров",
|
||||
"success": "Успех",
|
||||
@ -666,10 +667,11 @@
|
||||
"join_rule_knock_label": "Любой желающий может подать заявку на участие, но администраторы или модераторы должны предоставить доступ. Это можно изменить позже.",
|
||||
"join_rule_public_label": "Любой желающий сможет найти эту комнату и присоединиться к ней.",
|
||||
"join_rule_public_parent_space_label": "Любой сможет найти и присоединиться к этой комнате, а не только участники <SpaceName/>.",
|
||||
"join_rule_restricted": "Видимая для участников пространства",
|
||||
"join_rule_restricted_label": "Все в <SpaceName/> смогут найти и присоединиться к этой комнате.",
|
||||
"join_rule_restricted": "Стандартная",
|
||||
"join_rule_restricted_label": "Любой может присоединиться в <SpaceName/>.",
|
||||
"name_validation_required": "Пожалуйста, введите название комнаты",
|
||||
"room_visibility_label": "Видимость комнаты",
|
||||
"state_encryption_label": "Шифровать события состояния",
|
||||
"title_private_room": "Создать приватную комнату",
|
||||
"title_public_room": "Создать публичную комнату",
|
||||
"title_video_room": "Создайте видеокомнату",
|
||||
@ -807,6 +809,7 @@
|
||||
"event_id": "ID события: %(eventId)s",
|
||||
"event_sent": "Событие отправлено!",
|
||||
"event_type": "Тип события",
|
||||
"expired": "Срок действия истек",
|
||||
"explore_account_data": "Посмотреть данные учётной записи",
|
||||
"explore_room_account_data": "Посмотреть данные учётной записи комнаты",
|
||||
"explore_room_state": "Посмотреть состояние комнаты",
|
||||
@ -907,7 +910,7 @@
|
||||
"widget_screenshots": "Включить скриншоты виджетов для поддерживаемых виджетов"
|
||||
},
|
||||
"dialog_close_label": "Закрыть диалог",
|
||||
"download_completed": "Загрузка завершена",
|
||||
"download_completed": "Скачивание завершено",
|
||||
"emoji": {
|
||||
"categories": "Категории",
|
||||
"category_activities": "Действия",
|
||||
@ -1665,9 +1668,9 @@
|
||||
},
|
||||
"member_list": {
|
||||
"count": {
|
||||
"one": "%(count)s Участник",
|
||||
"few": "%(count)s Участника",
|
||||
"many": "%(count)s Участников"
|
||||
"one": "%(count)s участник",
|
||||
"few": "%(count)s участника",
|
||||
"many": "%(count)s участников"
|
||||
},
|
||||
"filter_placeholder": "Поиск по участникам",
|
||||
"invite_button_no_perms_tooltip": "У вас нет разрешения приглашать пользователей",
|
||||
@ -3235,7 +3238,8 @@
|
||||
"copy_link_text": "Копировать ссылку приглашения",
|
||||
"count_of_members": {
|
||||
"one": "%(count)s участник",
|
||||
"other": "%(count)s участников"
|
||||
"few": "%(count)s участника",
|
||||
"many": "%(count)s участников"
|
||||
},
|
||||
"create_new_room_button": "Создать комнату",
|
||||
"failed_querying_public_rooms": "Не удалось запросить публичную комнату",
|
||||
|
||||
@ -51,6 +51,7 @@ import MediaPreviewConfigController from "./controllers/MediaPreviewConfigContro
|
||||
import InviteRulesConfigController from "./controllers/InviteRulesConfigController.ts";
|
||||
import { type ComputedInviteConfig } from "../@types/invite-rules.ts";
|
||||
import BlockInvitesConfigController from "./controllers/BlockInvitesConfigController.ts";
|
||||
import RequiresSettingsController from "./controllers/RequiresSettingsController.ts";
|
||||
|
||||
export const defaultWatchManager = new WatchManager();
|
||||
|
||||
@ -327,6 +328,10 @@ export interface Settings {
|
||||
}>;
|
||||
"breadcrumbs": IBaseSetting<boolean>;
|
||||
"showHiddenEventsInTimeline": IBaseSetting<boolean>;
|
||||
/**
|
||||
* This is the 2019-era low bandwidth that deals with disabling features of the
|
||||
* client. It does NOT make any API or spec changes.
|
||||
*/
|
||||
"lowBandwidth": IBaseSetting<boolean>;
|
||||
"fallbackICEServerAllowed": IBaseSetting<boolean | null>;
|
||||
"RoomList.preferredSorting": IBaseSetting<SortingAlgorithm>;
|
||||
@ -1136,22 +1141,22 @@ export const SETTINGS: Settings = {
|
||||
controller: new UIFeatureController(UIFeature.AdvancedEncryption),
|
||||
},
|
||||
"urlPreviewsEnabled": {
|
||||
supportedLevels: LEVELS_ROOM_SETTINGS_WITH_ROOM,
|
||||
displayName: {
|
||||
"default": _td("settings|inline_url_previews_default"),
|
||||
"room-account": _td("settings|inline_url_previews_room_account"),
|
||||
"room": _td("settings|inline_url_previews_room"),
|
||||
},
|
||||
// Enabled by default and client configurable as this setting only allows unencrypted
|
||||
// messages to be previewed.
|
||||
supportedLevels: [SettingLevel.DEVICE, SettingLevel.ACCOUNT, SettingLevel.CONFIG],
|
||||
supportedLevelsAreOrdered: true,
|
||||
displayName: _td("settings|inline_url_previews_default"),
|
||||
default: true,
|
||||
controller: new UIFeatureController(UIFeature.URLPreviews),
|
||||
},
|
||||
"urlPreviewsEnabled_e2ee": {
|
||||
supportedLevels: [SettingLevel.ROOM_DEVICE],
|
||||
displayName: {
|
||||
"room-device": _td("settings|inline_url_previews_room_account"),
|
||||
},
|
||||
// Can only be enabled per-device to ensure neither the homeserver nor client config
|
||||
// can impact the user's choices.
|
||||
supportedLevels: [SettingLevel.DEVICE],
|
||||
supportedLevelsAreOrdered: true,
|
||||
displayName: _td("settings|inline_url_previews_encrypted"),
|
||||
default: false,
|
||||
controller: new UIFeatureController(UIFeature.URLPreviews),
|
||||
controller: new RequiresSettingsController([UIFeature.URLPreviews, "urlPreviewsEnabled"]),
|
||||
},
|
||||
"notificationsEnabled": {
|
||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
|
||||
|
||||
@ -654,40 +654,6 @@ export default class SettingsStore {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate the setting for URL previews in e2e rooms from room account
|
||||
* data to the room device level.
|
||||
*
|
||||
* @param isFreshLogin True if the user has just logged in, false if a previous session is being restored.
|
||||
*/
|
||||
private static async migrateURLPreviewsE2EE(isFreshLogin: boolean): Promise<void> {
|
||||
const MIGRATION_DONE_FLAG = "url_previews_e2ee_migration_done";
|
||||
if (localStorage.getItem(MIGRATION_DONE_FLAG)) return;
|
||||
if (isFreshLogin) return;
|
||||
|
||||
const client = MatrixClientPeg.safeGet();
|
||||
|
||||
while (!client.isInitialSyncComplete()) {
|
||||
await new Promise((r) => client.once(ClientEvent.Sync, r));
|
||||
}
|
||||
|
||||
logger.info("Performing one-time settings migration of URL previews in E2EE rooms");
|
||||
|
||||
const roomAccounthandler = LEVEL_HANDLERS[SettingLevel.ROOM_ACCOUNT];
|
||||
|
||||
for (const room of client.getRooms()) {
|
||||
// We need to use the handler directly because this setting is no longer supported
|
||||
// at this level at all
|
||||
const val = roomAccounthandler.getValue("urlPreviewsEnabled_e2ee", room.roomId);
|
||||
|
||||
if (val !== undefined) {
|
||||
await SettingsStore.setValue("urlPreviewsEnabled_e2ee", room.roomId, SettingLevel.ROOM_DEVICE, val);
|
||||
}
|
||||
}
|
||||
|
||||
localStorage.setItem(MIGRATION_DONE_FLAG, "true");
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate the setting for visible images to a setting.
|
||||
*/
|
||||
@ -739,15 +705,6 @@ export default class SettingsStore {
|
||||
* Runs or queues any setting migrations needed.
|
||||
*/
|
||||
public static runMigrations(isFreshLogin: boolean): void {
|
||||
// This can be removed once enough users have run a version of Element with
|
||||
// this migration. A couple of months after its release should be sufficient
|
||||
// (so around October 2024).
|
||||
// The consequences of missing the migration are only that URL previews will
|
||||
// be disabled in E2EE rooms.
|
||||
SettingsStore.migrateURLPreviewsE2EE(isFreshLogin).catch((e) => {
|
||||
logger.error("Failed to migrate URL previews in E2EE rooms:", e);
|
||||
});
|
||||
|
||||
// This can be removed once enough users have run a version of Element with
|
||||
// this migration.
|
||||
// The consequences of missing the migration are that previously shown images
|
||||
|
||||
@ -0,0 +1,34 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import SettingController from "./SettingController";
|
||||
import SettingsStore from "../SettingsStore";
|
||||
import type { BooleanSettingKey } from "../Settings.tsx";
|
||||
|
||||
/**
|
||||
* Disables a setting & forces it's value if one or more settings are not enabled
|
||||
*/
|
||||
export default class RequiresSettingsController extends SettingController {
|
||||
public constructor(
|
||||
public readonly settingNames: BooleanSettingKey[],
|
||||
private forcedValue = false,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public getValueOverride(): any {
|
||||
if (this.settingDisabled) {
|
||||
// per the docs: we force a disabled state when the feature isn't active
|
||||
return this.forcedValue;
|
||||
}
|
||||
return null; // no override
|
||||
}
|
||||
|
||||
public get settingDisabled(): boolean {
|
||||
return this.settingNames.some((s) => !SettingsStore.getValue(s));
|
||||
}
|
||||
}
|
||||
@ -76,15 +76,6 @@ export default class RoomAccountSettingsHandler extends MatrixClientBackedSettin
|
||||
};
|
||||
|
||||
public getValue(settingName: string, roomId: string): any {
|
||||
// Special case URL previews
|
||||
if (settingName === "urlPreviewsEnabled") {
|
||||
const content = this.getSettings(roomId, "org.matrix.room.preview_urls") || {};
|
||||
|
||||
// Check to make sure that we actually got a boolean
|
||||
if (typeof content["disable"] !== "boolean") return null;
|
||||
return !content["disable"];
|
||||
}
|
||||
|
||||
// Special case allowed widgets
|
||||
if (settingName === "allowedWidgets") {
|
||||
return this.getSettings(roomId, ALLOWED_WIDGETS_EVENT_TYPE);
|
||||
|
||||
128
apps/web/src/viewmodels/message-body/TileErrorViewModel.ts
Normal file
@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type MouseEventHandler } from "react";
|
||||
import { type MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import {
|
||||
BaseViewModel,
|
||||
type TileErrorViewLayout,
|
||||
type TileErrorViewSnapshot as TileErrorViewSnapshotInterface,
|
||||
type TileErrorViewModel as TileErrorViewModelInterface,
|
||||
} from "@element-hq/web-shared-components";
|
||||
|
||||
import { _t } from "../../languageHandler";
|
||||
import Modal from "../../Modal";
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
import { BugReportEndpointURLLocal } from "../../IConfigOptions";
|
||||
import ViewSource from "../../components/structures/ViewSource";
|
||||
import BugReportDialog from "../../components/views/dialogs/BugReportDialog";
|
||||
|
||||
const TILE_ERROR_BUG_REPORT_LABEL = "react-tile-soft-crash";
|
||||
|
||||
export interface TileErrorViewModelProps {
|
||||
/**
|
||||
* Layout variant used by the host timeline.
|
||||
*/
|
||||
layout: TileErrorViewLayout;
|
||||
/**
|
||||
* Event whose tile failed to render.
|
||||
*/
|
||||
mxEvent: MatrixEvent;
|
||||
/**
|
||||
* Render error captured by the boundary.
|
||||
*/
|
||||
error: Error;
|
||||
/**
|
||||
* Whether developer mode is enabled, which controls the view-source action.
|
||||
*/
|
||||
developerMode: boolean;
|
||||
}
|
||||
|
||||
function getBugReportCtaLabel(): string | undefined {
|
||||
const bugReportUrl = SdkConfig.get().bug_report_endpoint_url;
|
||||
|
||||
if (!bugReportUrl) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return bugReportUrl === BugReportEndpointURLLocal
|
||||
? _t("bug_reporting|download_logs")
|
||||
: _t("bug_reporting|submit_debug_logs");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the localized view-source action label when developer mode is enabled.
|
||||
*/
|
||||
function getViewSourceCtaLabel(developerMode: boolean): string | undefined {
|
||||
return developerMode ? _t("action|view_source") : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* ViewModel for the tile error fallback, providing the snapshot shown when a tile fails to render.
|
||||
*
|
||||
* The snapshot includes the host timeline layout, the fallback message, the event type,
|
||||
* and optional bug-report and view-source action labels. The view model also exposes
|
||||
* click handlers for those actions, opening the bug-report or view-source dialog when
|
||||
* available.
|
||||
*/
|
||||
export class TileErrorViewModel
|
||||
extends BaseViewModel<TileErrorViewSnapshotInterface, TileErrorViewModelProps>
|
||||
implements TileErrorViewModelInterface
|
||||
{
|
||||
private static readonly computeSnapshot = (props: TileErrorViewModelProps): TileErrorViewSnapshotInterface => ({
|
||||
layout: props.layout,
|
||||
message: _t("timeline|error_rendering_message"),
|
||||
eventType: props.mxEvent.getType(),
|
||||
bugReportCtaLabel: getBugReportCtaLabel(),
|
||||
viewSourceCtaLabel: getViewSourceCtaLabel(props.developerMode),
|
||||
});
|
||||
|
||||
public constructor(props: TileErrorViewModelProps) {
|
||||
super(props, TileErrorViewModel.computeSnapshot(props));
|
||||
}
|
||||
|
||||
public setLayout(layout: TileErrorViewLayout): void {
|
||||
this.props.layout = layout;
|
||||
this.snapshot.merge({ layout });
|
||||
}
|
||||
|
||||
public setError(error: Error): void {
|
||||
this.props.error = error;
|
||||
}
|
||||
|
||||
public setDeveloperMode(developerMode: boolean): void {
|
||||
this.props.developerMode = developerMode;
|
||||
|
||||
const nextViewSourceCtaLabel = getViewSourceCtaLabel(developerMode);
|
||||
this.snapshot.merge({ viewSourceCtaLabel: nextViewSourceCtaLabel });
|
||||
}
|
||||
|
||||
public onBugReportClick: MouseEventHandler<HTMLButtonElement> = () => {
|
||||
if (!this.snapshot.current.bugReportCtaLabel) {
|
||||
return;
|
||||
}
|
||||
|
||||
Modal.createDialog(BugReportDialog, {
|
||||
label: TILE_ERROR_BUG_REPORT_LABEL,
|
||||
error: this.props.error,
|
||||
});
|
||||
};
|
||||
|
||||
public onViewSourceClick: MouseEventHandler<HTMLButtonElement> = () => {
|
||||
if (!this.snapshot.current.viewSourceCtaLabel) {
|
||||
return;
|
||||
}
|
||||
|
||||
Modal.createDialog(
|
||||
ViewSource,
|
||||
{
|
||||
mxEvent: this.props.mxEvent,
|
||||
},
|
||||
"mx_Dialog_viewsource",
|
||||
);
|
||||
};
|
||||
}
|
||||
@ -19,6 +19,10 @@ import { type RoomNotificationState } from "../../stores/notifications/RoomNotif
|
||||
interface RoomListSectionHeaderViewModelProps {
|
||||
tag: string;
|
||||
title: string;
|
||||
/**
|
||||
* The ID of the current space.
|
||||
*/
|
||||
spaceId: string;
|
||||
onToggleExpanded: (isExpanded: boolean) => void;
|
||||
}
|
||||
|
||||
@ -31,12 +35,19 @@ export class RoomListSectionHeaderViewModel
|
||||
*/
|
||||
private roomNotificationStates = new Set<RoomNotificationState>();
|
||||
|
||||
/**
|
||||
* Tracks the expanded/collapsed state per space.
|
||||
* Key is spaceId. Defaults to expanded if not set.
|
||||
*/
|
||||
private readonly expandedBySpace = new Map<string, boolean>();
|
||||
|
||||
public constructor(props: RoomListSectionHeaderViewModelProps) {
|
||||
super(props, { id: props.tag, title: props.title, isExpanded: true, isUnread: false });
|
||||
}
|
||||
|
||||
public onClick = (): void => {
|
||||
const isExpanded = !this.snapshot.current.isExpanded;
|
||||
this.expandedBySpace.set(this.props.spaceId, isExpanded);
|
||||
this.snapshot.merge({ isExpanded });
|
||||
this.props.onToggleExpanded(isExpanded);
|
||||
};
|
||||
@ -48,6 +59,25 @@ export class RoomListSectionHeaderViewModel
|
||||
return this.snapshot.current.isExpanded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether the section is expanded for the current space.
|
||||
* This will not trigger the onToggleExpanded callback.
|
||||
*/
|
||||
public set isExpanded(value: boolean) {
|
||||
this.expandedBySpace.set(this.props.spaceId, value);
|
||||
this.snapshot.merge({ isExpanded: value });
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to a different space, restoring the expanded state for that space.
|
||||
* Defaults to expanded if no state has been saved for the space.
|
||||
*/
|
||||
public setSpace(spaceId: string): void {
|
||||
this.props.spaceId = spaceId;
|
||||
const isExpanded = this.expandedBySpace.get(this.props.spaceId) ?? true;
|
||||
this.snapshot.merge({ isExpanded });
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the rooms tracked by this section header for unread state computation.
|
||||
* Only subscribes to new rooms and unsubscribes from rooms no longer in the section.
|
||||
|
||||
@ -182,6 +182,16 @@ export class RoomListViewModel
|
||||
// Update roomsMap immediately before clearing VMs
|
||||
this.updateRoomsMap(this.roomsResult);
|
||||
|
||||
// When a filter is toggled on, expand sections that have results so they're visible
|
||||
if (newFilter) {
|
||||
for (const section of this.roomsResult.sections) {
|
||||
if (section.rooms.length > 0) {
|
||||
const sectionHeaderVM = this.roomSectionHeaderViewModels.get(section.tag);
|
||||
if (sectionHeaderVM) sectionHeaderVM.isExpanded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.updateRoomListData();
|
||||
};
|
||||
|
||||
@ -258,6 +268,7 @@ export class RoomListViewModel
|
||||
const viewModel = new RoomListSectionHeaderViewModel({
|
||||
tag,
|
||||
title,
|
||||
spaceId: this.roomsResult.spaceId,
|
||||
onToggleExpanded: () => this.updateRoomListData(),
|
||||
});
|
||||
this.roomSectionHeaderViewModels.set(tag, viewModel);
|
||||
@ -367,6 +378,11 @@ export class RoomListViewModel
|
||||
|
||||
this.updateRoomsMap(this.roomsResult);
|
||||
|
||||
// Restore the expanded/collapsed state for the new space
|
||||
for (const viewModel of this.roomSectionHeaderViewModels.values()) {
|
||||
viewModel.setSpace(newSpaceId);
|
||||
}
|
||||
|
||||
// Space changed - get the last selected room for the new space to prevent flicker
|
||||
const lastSelectedRoom = SpaceStore.instance.getLastSelectedRoomIdForSpace(newSpaceId);
|
||||
|
||||
@ -501,6 +517,12 @@ export class RoomListViewModel
|
||||
this.roomsResult,
|
||||
(tag) => this.roomSectionHeaderViewModels.get(tag)?.isExpanded ?? true,
|
||||
);
|
||||
// If it's a flat list, we need to make sure the single section is expanded and has all rooms, otherwise the room list will be empty
|
||||
if (isFlatList) {
|
||||
const chatSections = this.roomSectionHeaderViewModels.get(CHATS_TAG);
|
||||
if (chatSections) chatSections.isExpanded = true;
|
||||
chatSections?.setRooms(this.roomsResult.sections.flatMap((section) => section.rooms));
|
||||
}
|
||||
this.sections = sections;
|
||||
|
||||
// Calculate the active room index from the computed sections (which exclude collapsed sections' rooms)
|
||||
|
||||
@ -57,6 +57,12 @@ export class ResizerViewModel
|
||||
}, 50);
|
||||
|
||||
public onLeftPanelResized = (newSize: number): void => {
|
||||
// We don't want the panels to have fractional widths as that can cause blurry UI elements.
|
||||
if (!Number.isInteger(newSize)) {
|
||||
this.panelHandle?.resize(`${Math.round(newSize)}%`);
|
||||
return;
|
||||
}
|
||||
|
||||
const isCollapsed = newSize === 0;
|
||||
// Store the size if the panel isn't collapsed.
|
||||
if (!isCollapsed) {
|
||||
@ -76,7 +82,7 @@ export class ResizerViewModel
|
||||
public onSeparatorClick = (): void => {
|
||||
if (this.panelHandle?.isCollapsed()) {
|
||||
const lastSize = SettingsStore.getValue("RoomList.panelSize");
|
||||
this.panelHandle.resize(`${lastSize}%`);
|
||||
this.panelHandle.resize(`${lastSize ?? 100}%`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -57,6 +57,9 @@ export class CapabilityText {
|
||||
[MatrixCapabilities.MSC2931Navigate]: {
|
||||
[GENERIC_WIDGET_KIND]: _td("widget|capability|switch_room_message_user"),
|
||||
},
|
||||
[MatrixCapabilities.MSC4039DownloadFile]: {
|
||||
[GENERIC_WIDGET_KIND]: _td("widget|capability|download_file"),
|
||||
},
|
||||
};
|
||||
|
||||
private static stateSendRecvCaps: SendRecvStaticCapText = {
|
||||
|
||||
@ -15,9 +15,9 @@ exports[`bodyToHtml feature_latex_maths should not mangle code blocks 1`] = `"<p
|
||||
|
||||
exports[`bodyToHtml feature_latex_maths should not mangle divs 1`] = `"<p>hello</p><div>world</div>"`;
|
||||
|
||||
exports[`bodyToHtml feature_latex_maths should render block katex 1`] = `"<p>hello</p><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>ξ</mi></mrow><annotation encoding="application/x-tex">\\xi</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.04601em;">ξ</span></span></span></span></span><p>world</p>"`;
|
||||
exports[`bodyToHtml feature_latex_maths should render block katex 1`] = `"<p>hello</p><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>ξ</mi></mrow><annotation encoding="application/x-tex">\\xi</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.046em;">ξ</span></span></span></span></span><p>world</p>"`;
|
||||
|
||||
exports[`bodyToHtml feature_latex_maths should render inline katex 1`] = `"hello <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>ξ</mi></mrow><annotation encoding="application/x-tex">\\xi</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.04601em;">ξ</span></span></span></span> world"`;
|
||||
exports[`bodyToHtml feature_latex_maths should render inline katex 1`] = `"hello <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>ξ</mi></mrow><annotation encoding="application/x-tex">\\xi</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.046em;">ξ</span></span></span></span> world"`;
|
||||
|
||||
exports[`bodyToNode generates big emoji for emoji made of multiple characters 1`] = `
|
||||
<DocumentFragment>
|
||||
|
||||
@ -120,6 +120,29 @@ describe("VoiceRecording", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should request the selected microphone as an exact device constraint", async () => {
|
||||
MediaDeviceHandlerMock.getAudioInput.mockReturnValue("selected-mic");
|
||||
await recording.start();
|
||||
|
||||
expect(navigator.mediaDevices.getUserMedia).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
audio: expect.objectContaining({ deviceId: { exact: "selected-mic" } }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should not force an exact microphone when default device is selected", async () => {
|
||||
MediaDeviceHandlerMock.getAudioInput.mockReturnValue("default");
|
||||
await recording.start();
|
||||
|
||||
const constraints = mocked(navigator.mediaDevices.getUserMedia).mock.calls[0][0] as MediaStreamConstraints;
|
||||
expect(constraints.audio).toEqual(
|
||||
expect.not.objectContaining({
|
||||
deviceId: expect.anything(),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when recording", () => {
|
||||
|
||||
@ -36,7 +36,7 @@ describe("PictureInPictureDragger", () => {
|
||||
|
||||
describe("when rendering the dragger with PiP content 1", () => {
|
||||
beforeEach(() => {
|
||||
renderResult = render(<PictureInPictureDragger draggable={true}>{mkContent1}</PictureInPictureDragger>);
|
||||
renderResult = render(<PictureInPictureDragger>{mkContent1}</PictureInPictureDragger>);
|
||||
});
|
||||
|
||||
it("should render the PiP content", () => {
|
||||
@ -45,7 +45,7 @@ describe("PictureInPictureDragger", () => {
|
||||
|
||||
describe("and rerendering PiP content 1", () => {
|
||||
beforeEach(() => {
|
||||
renderResult.rerender(<PictureInPictureDragger draggable={true}>{mkContent1}</PictureInPictureDragger>);
|
||||
renderResult.rerender(<PictureInPictureDragger>{mkContent1}</PictureInPictureDragger>);
|
||||
});
|
||||
|
||||
it("should not change the PiP content", () => {
|
||||
@ -55,7 +55,7 @@ describe("PictureInPictureDragger", () => {
|
||||
|
||||
describe("and rendering PiP content 2", () => {
|
||||
beforeEach(() => {
|
||||
renderResult.rerender(<PictureInPictureDragger draggable={true}>{mkContent2}</PictureInPictureDragger>);
|
||||
renderResult.rerender(<PictureInPictureDragger>{mkContent2}</PictureInPictureDragger>);
|
||||
});
|
||||
|
||||
it("should update the PiP content", () => {
|
||||
@ -66,9 +66,7 @@ describe("PictureInPictureDragger", () => {
|
||||
|
||||
describe("when rendering the dragger with PiP content 1 and 2", () => {
|
||||
beforeEach(() => {
|
||||
renderResult = render(
|
||||
<PictureInPictureDragger draggable={true}>{[...mkContent1, ...mkContent2]}</PictureInPictureDragger>,
|
||||
);
|
||||
renderResult = render(<PictureInPictureDragger>{[...mkContent1, ...mkContent2]}</PictureInPictureDragger>);
|
||||
});
|
||||
|
||||
it("should render both contents", () => {
|
||||
@ -83,7 +81,7 @@ describe("PictureInPictureDragger", () => {
|
||||
beforeEach(() => {
|
||||
clickSpy = jest.fn();
|
||||
render(
|
||||
<PictureInPictureDragger draggable={true}>
|
||||
<PictureInPictureDragger>
|
||||
{[
|
||||
({ onStartMoving }) => (
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
exports[`PictureInPictureDragger when rendering the dragger with PiP content 1 and 2 should render both contents 1`] = `
|
||||
<div>
|
||||
<aside
|
||||
class="mx_PictureInPictureDragger"
|
||||
style="transform: translateX(672px) translateY(80px);"
|
||||
>
|
||||
<div>
|
||||
@ -20,6 +21,7 @@ exports[`PictureInPictureDragger when rendering the dragger with PiP content 1 a
|
||||
exports[`PictureInPictureDragger when rendering the dragger with PiP content 1 and rendering PiP content 2 should update the PiP content 1`] = `
|
||||
<div>
|
||||
<aside
|
||||
class="mx_PictureInPictureDragger"
|
||||
style="transform: translateX(672px) translateY(80px);"
|
||||
>
|
||||
<div>
|
||||
@ -34,6 +36,7 @@ exports[`PictureInPictureDragger when rendering the dragger with PiP content 1 a
|
||||
exports[`PictureInPictureDragger when rendering the dragger with PiP content 1 and rerendering PiP content 1 should not change the PiP content: pip-content-1 1`] = `
|
||||
<div>
|
||||
<aside
|
||||
class="mx_PictureInPictureDragger"
|
||||
style="transform: translateX(672px) translateY(80px);"
|
||||
>
|
||||
<div>
|
||||
@ -46,6 +49,7 @@ exports[`PictureInPictureDragger when rendering the dragger with PiP content 1 a
|
||||
exports[`PictureInPictureDragger when rendering the dragger with PiP content 1 should render the PiP content: pip-content-1 1`] = `
|
||||
<div>
|
||||
<aside
|
||||
class="mx_PictureInPictureDragger"
|
||||
style="transform: translateX(672px) translateY(80px);"
|
||||
>
|
||||
<div>
|
||||
|
||||
@ -237,6 +237,50 @@ exports[`DevtoolsDialog renders the devtools dialog 1`] = `
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field_19upo_32"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_19upo_44"
|
||||
>
|
||||
<div
|
||||
class="_container_udcm8_10"
|
||||
>
|
||||
<input
|
||||
class="_input_udcm8_24"
|
||||
id="mx_SettingsFlag_4yVCeEefiPqp"
|
||||
role="switch"
|
||||
type="checkbox"
|
||||
/>
|
||||
<div
|
||||
class="_ui_udcm8_34"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field-body_19upo_38"
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59"
|
||||
for="mx_SettingsFlag_4yVCeEefiPqp"
|
||||
>
|
||||
Disable bandwidth-heavy features
|
||||
</label>
|
||||
<span
|
||||
class="_message_19upo_85 _help-message_19upo_91"
|
||||
id="radix-_r_c_"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
class="mx_SettingsTab_microcopy_warning"
|
||||
>
|
||||
WARNING:
|
||||
</span>
|
||||
Disables encryption, presence, avatars, read receipts, and typing notifications
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<form
|
||||
class="_root_19upo_16"
|
||||
@ -246,7 +290,7 @@ exports[`DevtoolsDialog renders the devtools dialog 1`] = `
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59"
|
||||
for="radix-_r_a_"
|
||||
for="radix-_r_d_"
|
||||
>
|
||||
Element Call URL
|
||||
</label>
|
||||
@ -255,7 +299,7 @@ exports[`DevtoolsDialog renders the devtools dialog 1`] = `
|
||||
>
|
||||
<input
|
||||
class="_control_sqdq4_10"
|
||||
id="radix-_r_a_"
|
||||
id="radix-_r_d_"
|
||||
name="input"
|
||||
title=""
|
||||
value=""
|
||||
|
||||
@ -11,8 +11,6 @@ import { type RenderResult, render } from "jest-matrix-react";
|
||||
import { type MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import MKeyVerificationRequest from "../../../../../src/components/views/messages/MKeyVerificationRequest";
|
||||
import TileErrorBoundary from "../../../../../src/components/views/messages/TileErrorBoundary";
|
||||
import { Layout } from "../../../../../src/settings/enums/Layout";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import { filterConsole } from "../../../../test-utils";
|
||||
|
||||
@ -60,22 +58,49 @@ describe("MKeyVerificationRequest", () => {
|
||||
});
|
||||
});
|
||||
|
||||
interface TestTileErrorBoundaryProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface TestTileErrorBoundaryState {
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
class TestTileErrorBoundary extends React.Component<TestTileErrorBoundaryProps, TestTileErrorBoundaryState> {
|
||||
public constructor(props: TestTileErrorBoundaryProps) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
public static getDerivedStateFromError(error: Error): Partial<TestTileErrorBoundaryState> {
|
||||
return { error };
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
if (this.state.error) {
|
||||
return "Can't load this message";
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
function renderEventNoClient(event: MatrixEvent): RenderResult {
|
||||
return render(
|
||||
<TileErrorBoundary mxEvent={event} layout={Layout.Group}>
|
||||
<TestTileErrorBoundary>
|
||||
<MKeyVerificationRequest mxEvent={event} />
|
||||
</TileErrorBoundary>,
|
||||
</TestTileErrorBoundary>,
|
||||
);
|
||||
}
|
||||
|
||||
function renderEvent(client: MatrixClient, event: MatrixEvent): RenderResult {
|
||||
return render(
|
||||
<TileErrorBoundary mxEvent={event} layout={Layout.Group}>
|
||||
<TestTileErrorBoundary>
|
||||
<MatrixClientContext.Provider value={client}>
|
||||
<MKeyVerificationRequest mxEvent={event} />
|
||||
</MatrixClientContext.Provider>
|
||||
,
|
||||
</TileErrorBoundary>,
|
||||
</TestTileErrorBoundary>,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -146,6 +146,28 @@ describe("<TextualBody />", () => {
|
||||
expect(content).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("keeps edited emote bodies inline with the sender", () => {
|
||||
DMRoomMap.makeShared(defaultMatrixClient);
|
||||
|
||||
const ev = mkEvent({
|
||||
type: "m.room.message",
|
||||
room: room1Id,
|
||||
user: "sender",
|
||||
content: {
|
||||
body: "winks",
|
||||
msgtype: "m.emote",
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
jest.spyOn(ev, "replacingEventDate").mockReturnValue(new Date(1993, 7, 3));
|
||||
|
||||
const { container } = getComponent({ mxEvent: ev, replacingEventId: ev.getId() });
|
||||
|
||||
const annotated = container.querySelector(".mx_MEmoteBody > .mx_EventTile_annotatedInline");
|
||||
expect(annotated).not.toBeNull();
|
||||
expect(annotated?.tagName).toBe("DIV");
|
||||
});
|
||||
|
||||
it("renders m.notice correctly", () => {
|
||||
DMRoomMap.makeShared(defaultMatrixClient);
|
||||
|
||||
|
||||
@ -1,96 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
|
||||
import { render, screen } from "jest-matrix-react";
|
||||
import { waitFor } from "@testing-library/dom";
|
||||
import { Form } from "@vector-im/compound-web";
|
||||
|
||||
import { createTestClient, mkStubRoom, withClientContextRenderOptions } from "../../../../test-utils";
|
||||
import { UrlPreviewSettings } from "../../../../../src/components/views/room_settings/UrlPreviewSettings.tsx";
|
||||
import SettingsStore from "../../../../../src/settings/SettingsStore.ts";
|
||||
import dis from "../../../../../src/dispatcher/dispatcher.ts";
|
||||
import { Action } from "../../../../../src/dispatcher/actions.ts";
|
||||
|
||||
describe("UrlPreviewSettings", () => {
|
||||
let client: MatrixClient;
|
||||
let room: Room;
|
||||
|
||||
beforeEach(() => {
|
||||
client = createTestClient();
|
||||
room = mkStubRoom("roomId", "room", client);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
function renderComponent() {
|
||||
return render(
|
||||
<Form.Root>
|
||||
<UrlPreviewSettings room={room} />
|
||||
</Form.Root>,
|
||||
withClientContextRenderOptions(client),
|
||||
);
|
||||
}
|
||||
|
||||
it("should display the correct preview when the setting is in a loading state", () => {
|
||||
jest.spyOn(client, "getCrypto").mockReturnValue(undefined);
|
||||
const { asFragment } = renderComponent();
|
||||
expect(screen.getByText("URL Previews")).toBeInTheDocument();
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should display the correct preview when the room is encrypted and the url preview is enabled", async () => {
|
||||
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
|
||||
jest.spyOn(SettingsStore, "getValueAt").mockReturnValue(true);
|
||||
|
||||
const { asFragment } = renderComponent();
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText(
|
||||
"In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.",
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should display the correct preview when the room is unencrypted and the url preview is enabled", async () => {
|
||||
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(false);
|
||||
jest.spyOn(SettingsStore, "getValueAt").mockReturnValue(true);
|
||||
jest.spyOn(dis, "fire").mockReturnValue(undefined);
|
||||
|
||||
const { asFragment } = renderComponent();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "enabled" })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("URL previews are enabled by default for participants in this room."),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
screen.getByRole("button", { name: "enabled" }).click();
|
||||
expect(dis.fire).toHaveBeenCalledWith(Action.ViewUserSettings);
|
||||
});
|
||||
|
||||
it("should display the correct preview when the room is unencrypted and the url preview is disabled", async () => {
|
||||
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(false);
|
||||
jest.spyOn(SettingsStore, "getValueAt").mockReturnValue(false);
|
||||
|
||||
const { asFragment } = renderComponent();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "disabled" })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("URL previews are disabled by default for participants in this room."),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@ -1,270 +0,0 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`UrlPreviewSettings should display the correct preview when the room is encrypted and the url preview is enabled 1`] = `
|
||||
<DocumentFragment>
|
||||
<form
|
||||
class="_root_19upo_16"
|
||||
>
|
||||
<fieldset
|
||||
class="mx_SettingsFieldset"
|
||||
>
|
||||
<legend
|
||||
class="mx_SettingsFieldset_legend"
|
||||
>
|
||||
URL Previews
|
||||
</legend>
|
||||
<div
|
||||
class="mx_SettingsFieldset_description"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSubsection_text"
|
||||
>
|
||||
<p>
|
||||
When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.
|
||||
</p>
|
||||
<p>
|
||||
In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsFieldset_content"
|
||||
>
|
||||
<div
|
||||
class="_inline-field_19upo_32"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_19upo_44"
|
||||
>
|
||||
<div
|
||||
class="_container_udcm8_10"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
class="_input_udcm8_24"
|
||||
id="mx_SettingsFlag_vY7Q4uEh9K38"
|
||||
role="switch"
|
||||
type="checkbox"
|
||||
/>
|
||||
<div
|
||||
class="_ui_udcm8_34"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field-body_19upo_38"
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59"
|
||||
for="mx_SettingsFlag_vY7Q4uEh9K38"
|
||||
>
|
||||
Enable URL previews for this room (only affects you)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`UrlPreviewSettings should display the correct preview when the room is unencrypted and the url preview is disabled 1`] = `
|
||||
<DocumentFragment>
|
||||
<form
|
||||
class="_root_19upo_16"
|
||||
>
|
||||
<fieldset
|
||||
class="mx_SettingsFieldset"
|
||||
>
|
||||
<legend
|
||||
class="mx_SettingsFieldset_legend"
|
||||
>
|
||||
URL Previews
|
||||
</legend>
|
||||
<div
|
||||
class="mx_SettingsFieldset_description"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSubsection_text"
|
||||
>
|
||||
<p>
|
||||
When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.
|
||||
</p>
|
||||
<p>
|
||||
<span>
|
||||
You have
|
||||
</span>
|
||||
</p>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
disabled
|
||||
</div>
|
||||
URL previews by default.
|
||||
<p />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsFieldset_content"
|
||||
>
|
||||
<div>
|
||||
URL previews are disabled by default for participants in this room.
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field_19upo_32"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_19upo_44"
|
||||
>
|
||||
<div
|
||||
class="_container_udcm8_10"
|
||||
>
|
||||
<input
|
||||
class="_input_udcm8_24"
|
||||
disabled=""
|
||||
id="mx_SettingsFlag_vY7Q4uEh9K38"
|
||||
role="switch"
|
||||
type="checkbox"
|
||||
/>
|
||||
<div
|
||||
class="_ui_udcm8_34"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field-body_19upo_38"
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59"
|
||||
for="mx_SettingsFlag_vY7Q4uEh9K38"
|
||||
>
|
||||
Enable inline URL previews by default
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`UrlPreviewSettings should display the correct preview when the room is unencrypted and the url preview is enabled 1`] = `
|
||||
<DocumentFragment>
|
||||
<form
|
||||
class="_root_19upo_16"
|
||||
>
|
||||
<fieldset
|
||||
class="mx_SettingsFieldset"
|
||||
>
|
||||
<legend
|
||||
class="mx_SettingsFieldset_legend"
|
||||
>
|
||||
URL Previews
|
||||
</legend>
|
||||
<div
|
||||
class="mx_SettingsFieldset_description"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSubsection_text"
|
||||
>
|
||||
<p>
|
||||
When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.
|
||||
</p>
|
||||
<p>
|
||||
<span>
|
||||
You have
|
||||
</span>
|
||||
</p>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
enabled
|
||||
</div>
|
||||
URL previews by default.
|
||||
<p />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsFieldset_content"
|
||||
>
|
||||
<div>
|
||||
URL previews are enabled by default for participants in this room.
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field_19upo_32"
|
||||
>
|
||||
<div
|
||||
class="_inline-field-control_19upo_44"
|
||||
>
|
||||
<div
|
||||
class="_container_udcm8_10"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
class="_input_udcm8_24"
|
||||
id="mx_SettingsFlag_vY7Q4uEh9K38"
|
||||
role="switch"
|
||||
type="checkbox"
|
||||
/>
|
||||
<div
|
||||
class="_ui_udcm8_34"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_inline-field-body_19upo_38"
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59"
|
||||
for="mx_SettingsFlag_vY7Q4uEh9K38"
|
||||
>
|
||||
Enable inline URL previews by default
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`UrlPreviewSettings should display the correct preview when the setting is in a loading state 1`] = `
|
||||
<DocumentFragment>
|
||||
<form
|
||||
class="_root_19upo_16"
|
||||
>
|
||||
<fieldset
|
||||
class="mx_SettingsFieldset"
|
||||
>
|
||||
<legend
|
||||
class="mx_SettingsFieldset_legend"
|
||||
>
|
||||
URL Previews
|
||||
</legend>
|
||||
<div
|
||||
class="mx_SettingsFieldset_content"
|
||||
>
|
||||
<svg
|
||||
class="_icon_11k6c_18"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
style="width: 20px; height: 20px;"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M12 4.031a8 8 0 1 0 8 8 1 1 0 0 1 2 0c0 5.523-4.477 10-10 10s-10-4.477-10-10 4.477-10 10-10a1 1 0 1 1 0 2"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
@ -31,6 +31,7 @@ import { mkEncryptedMatrixEvent } from "matrix-js-sdk/src/testing";
|
||||
import { getByTestId } from "@testing-library/dom";
|
||||
|
||||
import EventTile, { type EventTileProps } from "../../../../../src/components/views/rooms/EventTile";
|
||||
import * as EventTileFactory from "../../../../../src/events/EventTileFactory";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import { type RoomContextType, TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
|
||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||
@ -106,7 +107,7 @@ describe("EventTile", () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.spyOn(PinningUtils, "isPinned").mockReturnValue(false);
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("EventTile thread summary", () => {
|
||||
@ -200,6 +201,19 @@ describe("EventTile", () => {
|
||||
expect(screen.getByText("Pinned message")).toBeInTheDocument();
|
||||
},
|
||||
);
|
||||
|
||||
it("renders the tile error fallback when tile rendering throws", async () => {
|
||||
jest.spyOn(console, "error").mockImplementation(() => {});
|
||||
jest.spyOn(EventTileFactory, "renderTile").mockImplementation(() => {
|
||||
throw new Error("Boom");
|
||||
});
|
||||
|
||||
getComponent();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Can't load this message (m.room.message)")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("EventTile in the right panel", () => {
|
||||
|
||||
@ -9,11 +9,11 @@ exports[`FontScalingPanel renders the font scaling UI 1`] = `
|
||||
<div
|
||||
class="mx_SettingsSubsectionHeading"
|
||||
>
|
||||
<h3
|
||||
<h2
|
||||
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
|
||||
>
|
||||
Font size
|
||||
</h3>
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsSubsection_content mx_SettingsSubsection_contentStretch"
|
||||
|
||||
@ -9,11 +9,11 @@ exports[`<LayoutSwitcher /> should render 1`] = `
|
||||
<div
|
||||
class="mx_SettingsSubsectionHeading"
|
||||
>
|
||||
<h3
|
||||
<h2
|
||||
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
|
||||
>
|
||||
Message layout
|
||||
</h3>
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi"
|
||||
|
||||
@ -11,17 +11,17 @@ exports[`SetIntegrationManager should render manage integrations sections 1`] =
|
||||
<div
|
||||
class="mx_SetIntegrationManager_heading_manager"
|
||||
>
|
||||
<h3
|
||||
<h2
|
||||
class="mx_Heading_h3"
|
||||
>
|
||||
Manage integrations
|
||||
</h3>
|
||||
<h4
|
||||
</h2>
|
||||
<h3
|
||||
class="mx_Heading_h4"
|
||||
id="mx_SetIntegrationManager_ManagerName"
|
||||
>
|
||||
(scalar.vector.im)
|
||||
</h4>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
||||