Merge branch 'hs/refactor-upload-logic' into hs/refactor-upload-logic+upload-module-api

This commit is contained in:
Half-Shot 2026-05-12 11:15:14 +01:00
commit 7d84e13bc2
317 changed files with 6943 additions and 4174 deletions

View File

@ -289,7 +289,7 @@ jobs:
id-token: write # This is required for requesting the JWT
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6
uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6
with:
role-to-assume: arn:aws:iam::264135176173:role/Push-ElementDesktop-MSI
role-session-name: githubaction-run-${{ github.run_id }}

View File

@ -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|Netlify|Report).)*$
check-regexp: ^((?!SonarCloud|SonarQube|issue|board|label|Release|prepare|GitHub Pages|Upload|Netlify|Report|deploy).)*$
# 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.

View File

@ -31,7 +31,7 @@ jobs:
persist-credentials: false
- name: Install Cosign
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: Set up QEMU
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4

View File

@ -26,7 +26,7 @@ jobs:
persist-credentials: false
- name: Install Cosign
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
if: github.event_name != 'pull_request'
- name: Set up QEMU

View File

@ -63,7 +63,7 @@ jobs:
- name: Get number of CPU cores
id: cpu-cores
uses: SimenB/github-actions-cpu-cores@97ba232459a8e02ff6121db9362b09661c875ab8 # v2
uses: SimenB/github-actions-cpu-cores@97330871fe1b7d3529392ea000e3d2c4b357e403 # v3
- name: Run tests
working-directory: apps/web

View File

@ -15,7 +15,7 @@ jobs:
contains(github.event.issue.assignees.*.login, 'dbkr') ||
contains(github.event.issue.assignees.*.login, 'MidhunSureshR')
steps:
- uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
- uses: actions/add-to-project@5afcf98fcd03f1c2f92c3c83f58ae24323cc57fd # v2.0.0
with:
project-url: https://github.com/orgs/element-hq/projects/67
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}

View File

@ -10,7 +10,7 @@ jobs:
automate-project-columns:
runs-on: ubuntu-24.04
steps:
- uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
- uses: actions/add-to-project@5afcf98fcd03f1c2f92c3c83f58ae24323cc57fd # v2.0.0
with:
project-url: https://github.com/orgs/element-hq/projects/120
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}

View File

@ -61,7 +61,7 @@ jobs:
contains(github.event.issue.labels.*.name, 'X-Needs-Info')
steps:
- id: add_to_project
uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
uses: actions/add-to-project@5afcf98fcd03f1c2f92c3c83f58ae24323cc57fd # v2.0.0
with:
project-url: ${{ env.PROJECT_URL }}
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
@ -84,7 +84,7 @@ jobs:
contains(github.event.issue.labels.*.name, 'Z-Flaky-Test')
steps:
- id: add_to_project
uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
uses: actions/add-to-project@5afcf98fcd03f1c2f92c3c83f58ae24323cc57fd # v2.0.0
with:
project-url: ${{ env.PROJECT_URL }}
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
@ -112,7 +112,7 @@ jobs:
contains(github.event.issue.labels.*.name, 'O-Frequent') ||
contains(github.event.issue.labels.*.name, 'A11y'))
steps:
- uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
- uses: actions/add-to-project@5afcf98fcd03f1c2f92c3c83f58ae24323cc57fd # v2.0.0
with:
project-url: https://github.com/orgs/element-hq/projects/18
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
@ -123,7 +123,7 @@ jobs:
if: >
contains(github.event.issue.labels.*.name, 'X-Needs-Product')
steps:
- uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
- uses: actions/add-to-project@5afcf98fcd03f1c2f92c3c83f58ae24323cc57fd # v2.0.0
with:
project-url: https://github.com/orgs/element-hq/projects/28
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
@ -134,7 +134,7 @@ jobs:
if: >
contains(github.event.issue.labels.*.name, 'A-New-Search-Experience')
steps:
- uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
- uses: actions/add-to-project@5afcf98fcd03f1c2f92c3c83f58ae24323cc57fd # v2.0.0
with:
project-url: https://github.com/orgs/element-hq/projects/48
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
@ -145,7 +145,7 @@ jobs:
if: >
contains(github.event.issue.labels.*.name, 'Team: VoIP')
steps:
- uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
- uses: actions/add-to-project@5afcf98fcd03f1c2f92c3c83f58ae24323cc57fd # v2.0.0
with:
project-url: https://github.com/orgs/element-hq/projects/41
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
@ -156,7 +156,7 @@ jobs:
if: >
contains(github.event.issue.labels.*.name, 'Team: Crypto')
steps:
- uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
- uses: actions/add-to-project@5afcf98fcd03f1c2f92c3c83f58ae24323cc57fd # v2.0.0
with:
project-url: https://github.com/orgs/element-hq/projects/76
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
@ -172,7 +172,7 @@ jobs:
contains(github.event.issue.labels.*.name, 'A-Testing') ||
contains(github.event.issue.labels.*.name, 'Z-Flaky-Test')
steps:
- uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
- uses: actions/add-to-project@5afcf98fcd03f1c2f92c3c83f58ae24323cc57fd # v2.0.0
with:
project-url: https://github.com/orgs/element-hq/projects/101
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}

View File

@ -1,7 +1,7 @@
# Docker image to facilitate building Element Desktop's native bits using a glibc version (2.31)
# with broader compatibility, down to Debian bullseye & Ubuntu focal.
FROM rust:bullseye@sha256:949b0903defbfc4e374dc85f947b153859e9ee0104e425cd9a74d94474a9a335
FROM rust:bullseye@sha256:85f9d38ab80fa5752a6fd5bff34c953a59ce2c7ccb0d47fb678d3c0300b8a331
ENV DEBIAN_FRONTEND=noninteractive

View File

@ -83,12 +83,12 @@
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"@vitest/coverage-v8": "catalog:",
"app-builder-lib": "26.9.0",
"app-builder-lib": "26.9.1",
"chokidar": "^5.0.0",
"detect-libc": "^2.0.0",
"electron": "41.2.2",
"electron-builder": "26.9.0",
"electron-builder-squirrel-windows": "26.9.0",
"electron": "42.0.0",
"electron-builder": "26.9.1",
"electron-builder-squirrel-windows": "26.9.1",
"electron-devtools-installer": "^4.0.0",
"eslint": "^8.26.0",
"eslint-config-google": "^0.14.0",
@ -112,7 +112,7 @@
"hakDependencies": {
"matrix-seshat": "4.2.0"
},
"packageManager": "pnpm@10.33.2+sha512.a90faf6feeab71ad6c6e57f94e0fe1a12f5dcc22cd754db40ae9593eb6a3e0b6b12e3540218bb37ae083404b1f2ce6db2a4121e979829b4aff94b99f49da1cf8",
"packageManager": "pnpm@10.33.3+sha512.a19744364a7e248b92657a4ca5973f9354d21caf982579674b1c539f32c7420c47138ad8b1254df07aba9bc782d9b3029e3db34d5dbff974326eb74dac8ff489",
"nx": {
"includedScripts": []
}

View File

@ -0,0 +1,35 @@
/*
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 { expect, describe, it, beforeEach, vi } from "vitest";
import { fs as memfs, vol } from "memfs";
import { getBuildConfig } from "./build-config.js";
vi.mock("node:fs", () => ({ default: memfs }));
beforeEach(() => {
// Reset the state of the in-memory fs
vol.reset();
});
describe("getBuildConfig", () => {
it("should read fields from package.json correctly", () => {
vol.fromJSON({
"package.json": JSON.stringify({
electron_appId: "app.id",
electron_protocol: "proto",
electron_windows_cert_sn: "subject.name",
}),
});
const config = getBuildConfig();
expect(config.appId).toBe("app.id");
expect(config.protocol).toBe("proto");
expect(config.windowsCertSubjectName).toBe("subject.name");
});
});

View File

@ -0,0 +1,44 @@
/*
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 { expect, describe, it, beforeEach, vi } from "vitest";
import { fs as memfs, vol } from "memfs";
import path from "node:path";
import { getIconPath } from "./icon.js";
vi.mock("node:fs/promises", () => ({ default: memfs.promises }));
beforeEach(() => {
// Reset the state of the in-memory fs
vol.reset();
});
describe("getIconPath", () => {
beforeEach(() => {
vol.fromJSON(
{
"build/icon.png": "png",
"build/icon.ico": "ico",
},
"../webapp",
);
});
it("should use .ico on Windows", async () => {
vi.spyOn(process, "platform", "get").mockReturnValue("win32");
await expect(getIconPath()).resolves.toEqual(path.resolve("../build/icon.ico"));
});
it("should use .png on macOS", async () => {
vi.spyOn(process, "platform", "get").mockReturnValue("darwin");
await expect(getIconPath()).resolves.toEqual(path.resolve("../build/icon.png"));
});
it("should use .png on Linux", async () => {
vi.spyOn(process, "platform", "get").mockReturnValue("linux");
await expect(getIconPath()).resolves.toEqual(path.resolve("../build/icon.png"));
});
});

View File

@ -0,0 +1,78 @@
/*
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 { expect, describe, it, beforeEach, vi } from "vitest";
import { fs as memfs, vol } from "memfs";
import { loadJsonFile, tryPaths, randomArray } from "./utils.js";
vi.mock("node:fs", () => ({ default: memfs }));
vi.mock("node:fs/promises", () => ({ default: memfs.promises }));
beforeEach(() => {
// Reset the state of the in-memory fs
vol.reset();
});
describe("randomArray", () => {
it("should return an array matching the requested size", async () => {
function toUnpaddedBase64Size(size: number): number {
return Math.ceil((4 * size) / 3);
}
await expect(randomArray(100)).resolves.toHaveLength(toUnpaddedBase64Size(100));
await expect(randomArray(32)).resolves.toHaveLength(toUnpaddedBase64Size(32));
});
it("should return a unique random array", async () => {
const arr1 = await randomArray(60);
const arr2 = await randomArray(60);
expect(arr1).not.toEqual(arr2);
});
});
describe("loadJsonFile", () => {
beforeEach(() => {
vol.fromJSON({
"./file.json": JSON.stringify({ file1: true }),
"./nested/deep/file.json": JSON.stringify({ file2: true }),
});
});
it("should load and parse a JSON file correctly", () => {
expect(loadJsonFile("file.json")).toStrictEqual({ file1: true });
});
it("should use args as path segments", () => {
expect(loadJsonFile("nested", "deep", "file.json")).toStrictEqual({ file2: true });
});
it("should return an empty object when file does not exist", () => {
expect(loadJsonFile("unknown-file.json")).toStrictEqual({});
});
});
describe("tryPaths", () => {
beforeEach(() => {
vol.fromNestedJSON({
"./dirA/": {},
"./dir/dirB/": {},
});
});
it("should find file relative to given root", async () => {
await expect(tryPaths("name", "dir", ["dirB"])).resolves.toEqual("dir/dirB/");
});
it("should handle unknown paths", async () => {
await expect(tryPaths("name", ".", ["dirB", "dirA"])).resolves.toEqual("dirA/");
});
it("should throw error if file does not exist", async () => {
await expect(tryPaths("name", "dir", ["a.json", "b.json"])).rejects.toThrow("Failed to find name path");
});
});

View File

@ -10,6 +10,10 @@ import fs from "node:fs";
import path from "node:path";
import afs from "node:fs/promises";
/**
* Returns a random array of a specified size in unpadded base64
* @param size - the size of the underlying random array
*/
export async function randomArray(size: number): Promise<string> {
return new Promise((resolve, reject) => {
crypto.randomBytes(size, (err, buf) => {
@ -62,9 +66,9 @@ export async function tryPaths(name: string, root: string, rawPaths: string[]):
return p + "/";
} catch {}
}
console.log(`Couldn't find ${name} files in any of: `);
console.log(`Couldn't find '${name}' in any of: `);
for (const p of paths) {
console.log("\t" + path.resolve(p));
}
throw new Error(`Failed to find ${name} files`);
throw new Error(`Failed to find ${name} path`);
}

View File

@ -2,7 +2,7 @@
# Context must be the root of the monorepo
# Builder
FROM --platform=$BUILDPLATFORM node:24-bullseye@sha256:d2059a9c157c9f70739736979fa3635008bf3ca74560b30930dc181228bc427f AS builder
FROM --platform=$BUILDPLATFORM node:24-bullseye@sha256:2c00db8852d28215c6203fafe9f05046acd9fdd48bcfc42467f4cba39b42dab4 AS builder
# Support custom branch of the js-sdk. This also helps us build images of element-web develop.
ARG USE_CUSTOM_SDKS=false
@ -25,7 +25,7 @@ RUN --mount=type=bind,source=.git,target=/src/.git /src/scripts/docker-package.s
RUN cp /src/apps/web/config.sample.json /src/apps/web/webapp/config.json
# App
FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:360465db60105a4cbf5215cd9e5a2ba40ef956978dd94f99707e9674050e38ea
FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:1df9285ed5bdaaad9ca503ac608e12fe1ba93136bb249fe976477989c1db4ede
# Need root user to install packages & manipulate the usr directory
USER root

View File

@ -42,7 +42,6 @@ const config: Config = {
"workers/(.+)Factory": "<rootDir>/__mocks__/workerFactoryMock.js",
"^!!raw-loader!.*": "jest-raw-loader",
"recorderWorkletFactory": "<rootDir>/__mocks__/empty.js",
"counterpart": "<rootDir>/../../node_modules/counterpart",
"@vector-im/compound-web": "<rootDir>/../../node_modules/@vector-im/compound-web",
},
transformIgnorePatterns: [

View File

@ -67,7 +67,7 @@
"emojibase-regex": "^17.0.0",
"escape-html": "^1.0.3",
"file-saver": "^2.0.5",
"filesize": "11.0.15",
"filesize": "11.0.17",
"github-markdown-css": "^5.5.1",
"glob-to-regexp": "^0.4.1",
"highlight.js": "^11.3.1",
@ -89,7 +89,7 @@
"opus-recorder": "^8.0.3",
"pako": "^2.0.3",
"png-chunks-extract": "^1.0.0",
"posthog-js": "1.369.3",
"posthog-js": "1.372.8",
"qrcode": "1.5.4",
"re-resizable": "6.11.2",
"react": "catalog:",
@ -124,7 +124,7 @@
"@babel/preset-env": "^7.12.11",
"@babel/preset-react": "^7.12.10",
"@babel/preset-typescript": "^7.12.7",
"@casualbot/jest-sonar-reporter": "2.6.0",
"@casualbot/jest-sonar-reporter": "2.7.0",
"@element-hq/element-call-embedded": "0.19.2",
"@element-hq/element-web-playwright-common": "workspace:*",
"@fetch-mock/jest": "^0.2.20",
@ -141,7 +141,6 @@
"@testing-library/user-event": "^14.5.2",
"@types/commonmark": "^0.27.4",
"@types/content-type": "^1.1.9",
"@types/counterpart": "^0.18.1",
"@types/css-tree": "^2.3.8",
"@types/diff-match-patch": "^1.0.32",
"@types/escape-html": "^1.0.1",
@ -204,7 +203,7 @@
"mini-css-extract-plugin": "2.10.2",
"modernizr": "^3.12.0",
"playwright-core": "catalog:",
"postcss": "8.5.10",
"postcss": "8.5.14",
"postcss-easings": "4.0.0",
"postcss-hexrgba": "2.1.0",
"postcss-import": "16.1.1",
@ -244,6 +243,6 @@
"engines": {
"node": ">=22.18"
},
"packageManager": "pnpm@10.33.2+sha512.a90faf6feeab71ad6c6e57f94e0fe1a12f5dcc22cd754db40ae9593eb6a3e0b6b12e3540218bb37ae083404b1f2ce6db2a4121e979829b4aff94b99f49da1cf8",
"packageManager": "pnpm@10.33.3+sha512.a19744364a7e248b92657a4ca5973f9354d21caf982579674b1c539f32c7420c47138ad8b1254df07aba9bc782d9b3029e3db34d5dbff974326eb74dac8ff489",
"private": true
}

View File

@ -10,6 +10,7 @@ Please see LICENSE files in the repository root for full details.
* Tests for application startup with guest registration enabled on the server.
*/
import type { Page } from "playwright-core";
import { expect, test } from "../../element-web-test";
test.use({
@ -18,12 +19,28 @@ test.use({
},
});
const screenshotOptions = (page?: Page) => ({
// Hide the UserID
css: `
span[data-testid="userId"] {
display: none !important;
}
`,
});
test("Shows the welcome page by default", async ({ page }) => {
await page.goto("/");
await expect(page.getByRole("heading", { name: "Be in your element" })).toBeVisible();
await expect(page.getByRole("link", { name: "Sign in" })).toBeVisible();
});
test("Shows the user menu for guests", { tag: ["@screenshot"] }, async ({ page, app }) => {
await page.goto("/#/room/!room:id");
await page.waitForSelector(".mx_MatrixChat", { timeout: 30000 });
const menu = await app.openUserMenu();
await expect(menu).toMatchScreenshot("guest-menu.png", screenshotOptions(page));
});
test("Room link correctly loads a room view", async ({ page }) => {
await page.goto("/#/room/!room:id");
await page.waitForSelector(".mx_MatrixChat", { timeout: 30000 });

View File

@ -23,6 +23,18 @@ test("Shows the homepage by default", async ({ pageWithCredentials: page }) => {
await expect(page.getByRole("heading", { name: "Welcome Boris", exact: true })).toBeVisible();
});
test(
"Adjusts homepage button layout in thin viewport",
{ tag: "@screenshot" },
async ({ pageWithCredentials: page }) => {
await page.setViewportSize({ width: 920, height: 720 });
await page.goto("/#/home");
await page.waitForSelector(".mx_HomePage", { timeout: 30000 });
await expect(page.locator(".mx_HomePage")).toMatchScreenshot("home-thin-viewport.png");
},
);
test("Shows the last known page on reload", async ({ pageWithCredentials: page }) => {
await page.goto("/");
await page.waitForSelector(".mx_MatrixChat", { timeout: 30000 });

View File

@ -23,7 +23,9 @@ test.describe("Logout tests", () => {
await sendMessageInCurrentRoom(page, "Hello secret world");
const locator = await app.settings.openUserMenu();
await locator.getByRole("menuitem", { name: "Remove this device", exact: true }).click();
await locator.getByRole("menuitem", { name: "All settings", exact: true }).click();
await page.getByRole("button", { name: "Remove this device", exact: true }).click();
const currentDialogLocator = page.locator(".mx_Dialog");
@ -41,7 +43,8 @@ test.describe("Logout tests", () => {
await sendMessageInCurrentRoom(page, "Hello secret world");
const locator = await app.settings.openUserMenu();
await locator.getByRole("menuitem", { name: "Remove this device", exact: true }).click();
await locator.getByRole("menuitem", { name: "All settings", exact: true }).click();
await page.getByRole("button", { name: "Remove this device", exact: true }).click();
const currentDialogLocator = page.locator(".mx_Dialog");
@ -54,7 +57,8 @@ test.describe("Logout tests", () => {
await sendMessageInCurrentRoom(page, "Hello public world!");
const locator = await app.settings.openUserMenu();
await locator.getByRole("menuitem", { name: "Remove this device", exact: true }).click();
await locator.getByRole("menuitem", { name: "All settings", exact: true }).click();
await page.getByRole("button", { name: "Remove this device", exact: true }).click();
// Should have logged out directly
await expect(page.getByRole("heading", { name: "Be in your element" })).toBeVisible();

View File

@ -252,7 +252,9 @@ export async function logIntoElementAndVerify(page: Page, credentials: Credentia
*/
export async function logOutOfElement(page: Page, discardKeys: boolean = false) {
await page.getByRole("button", { name: "User menu" }).click();
await page.locator(".mx_UserMenu_contextMenu").getByRole("menuitem", { name: "Remove this device" }).click();
await page.getByRole("menu", { name: "User menu" }).getByRole("menuitem", { name: "All settings" }).click();
await page.getByRole("button", { name: "Remove this device" }).click();
if (discardKeys) {
await page.getByRole("button", { name: "I don't want my encrypted messages" }).click();
} else {

View File

@ -328,8 +328,11 @@ test.describe("Room list", () => {
const videoRoom = roomListView.getByRole("option", { name: "video room" });
await expect(videoRoom).toHaveAttribute("aria-selected", "true"); // wait for room list update
// Ensure we highlight the video
await videoRoom.click();
// focus the user menu to avoid to have hover decoration
await page.getByRole("button", { name: "User menu" }).focus();
await page.getByRole("button", { name: "User menu" }).hover();
await expect(videoRoom).toMatchScreenshot("room-list-item-video.png");
});

View File

@ -340,11 +340,8 @@ test.describe("Login", () => {
// Allow the outstanding requests queue to settle before logging out
await page.waitForTimeout(2000);
await page
.locator(".mx_UserMenu_contextMenu")
.getByRole("menuitem", { name: "Remove this device" })
.click();
await page.getByRole("menu", { name: "User menu" }).getByRole("menuitem", { name: "All settings" }).click();
await page.getByRole("button", { name: "Remove this device" }).click();
await expect(page).toHaveURL(/\/#\/welcome$/);
});
});

View File

@ -28,8 +28,9 @@ test.describe("logout with logout_redirect_url", () => {
// give a change for the outstanding requests queue to settle before logging out
await page.waitForTimeout(2000);
await page.getByRole("menu", { name: "User menu" }).getByRole("menuitem", { name: "All settings" }).click();
await page.getByRole("button", { name: "Remove this device" }).click();
await page.locator(".mx_UserMenu_contextMenu").getByRole("menuitem", { name: "Remove this device" }).click();
await expect(page).toHaveURL(/\/decoder-ring\/$/);
});
});

View File

@ -74,7 +74,8 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
(request) => request.url() === revokeUri && request.postDataJSON()["token_type_hint"] === "refresh_token",
);
const locator = await app.settings.openUserMenu();
await locator.getByRole("menuitem", { name: "Remove this device", exact: true }).click();
await locator.getByRole("menuitem", { name: "All settings", exact: true }).click();
await page.getByRole("button", { name: "Remove this device", exact: true }).click();
await revokeAccessTokenPromise;
await revokeRefreshTokenPromise;
});
@ -122,7 +123,8 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
// Allow the outstanding requests queue to settle before logging out
await page.waitForTimeout(2000);
await page.locator(".mx_UserMenu_contextMenu").getByRole("menuitem", { name: "Remove this device" }).click();
await page.getByRole("menu", { name: "User menu" }).getByRole("menuitem", { name: "All settings" }).click();
await page.getByRole("button", { name: "Remove this device" }).click();
await expect(page).toHaveURL(/\/#\/welcome$/);
// Log in again
@ -155,10 +157,8 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
await page.getByRole("button", { name: "User menu" }).click();
await expect(page.getByText(userId, { exact: true })).toBeVisible();
await page.waitForTimeout(2000);
await page
.locator(".mx_UserMenu_contextMenu")
.getByRole("menuitem", { name: "Remove this device" })
.click();
await page.getByRole("menu", { name: "User menu" }).getByRole("menuitem", { name: "All settings" }).click();
await page.getByRole("button", { name: "Remove this device" }).click();
await expect(page).toHaveURL(/\/#\/welcome$/);
// Log in again
@ -206,9 +206,10 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
await expect(page.getByText(userId, { exact: true })).toBeVisible();
await page.waitForTimeout(2000);
await page
.locator(".mx_UserMenu_contextMenu")
.getByRole("menuitem", { name: "Remove this device" })
.getByRole("menu", { name: "User menu" })
.getByRole("menuitem", { name: "All settings" })
.click();
await page.getByRole("button", { name: "Remove this device" }).click();
await expect(page).toHaveURL(/\/#\/welcome$/);
// Log in again

View File

@ -6,11 +6,14 @@ 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 Route } from "@playwright/test";
import { test, expect } from "../../element-web-test";
import { getSampleFilePath } from "../../sample-files";
const USER_NAME = "Bob";
const USER_NAME_NEW = "Alice";
const EXTERNAL_ACCOUNT_MANAGEMENT_URL = "https://just.for.test.io/";
test.describe("Account user settings tab", () => {
test.use({
@ -79,6 +82,46 @@ test.describe("Account user settings tab", () => {
await expect(uut).toMatchScreenshot("account-smallscreen.png");
});
test.describe("with external account management", () => {
test.use({
page: async ({ page }, runFixture) => {
const authMetadataHandler = async (route: Route): Promise<void> => {
await route.fulfill({
json: {
issuer: EXTERNAL_ACCOUNT_MANAGEMENT_URL,
authorization_endpoint: `${EXTERNAL_ACCOUNT_MANAGEMENT_URL}authorize`,
token_endpoint: `${EXTERNAL_ACCOUNT_MANAGEMENT_URL}token`,
revocation_endpoint: `${EXTERNAL_ACCOUNT_MANAGEMENT_URL}revoke`,
response_types_supported: ["code"],
grant_types_supported: ["authorization_code"],
code_challenge_methods_supported: ["S256"],
account_management_uri: EXTERNAL_ACCOUNT_MANAGEMENT_URL,
},
});
};
await page.route("**/_matrix/client/v1/auth_metadata", authMetadataHandler);
await page.route("**/_matrix/client/unstable/org.matrix.msc2965/auth_metadata", authMetadataHandler);
await runFixture(page);
},
});
test("should render the manage account button properly", { tag: "@screenshot" }, async ({ uut, axe }) => {
const manageAccountButton = uut.getByTestId("external-account-management-link");
await expect(manageAccountButton).toBeVisible();
await expect(manageAccountButton).toHaveAttribute("href", EXTERNAL_ACCOUNT_MANAGEMENT_URL);
await expect(manageAccountButton).toHaveAttribute("target", "_blank");
await expect(manageAccountButton).toHaveText(/Manage account/);
const profileButtons = uut.locator(".mx_UserProfileSettings_profile_buttons");
await profileButtons.scrollIntoViewIfNeeded();
await expect(profileButtons).toMatchScreenshot("account-manage-account-button.png");
await expect(axe).toHaveNoViolations();
});
});
test("should show tooltips on narrow screen", async ({ page, uut }) => {
await page.setViewportSize({ width: 700, height: 600 });
await page.getByRole("tab", { name: "Account" }).hover();
@ -129,7 +172,7 @@ test.describe("Account user settings tab", () => {
await expect(accountPhoneNumbers.getByRole("button", { name: "Add" })).toBeVisible();
});
test("should support changing a display name", async ({ uut, page, app }) => {
test("should support changing a display name", async ({ uut, page, app, user }) => {
// Change the diaplay name to USER_NAME_NEW
const displayNameInput = uut
.locator(".mx_SettingsTab .mx_UserProfileSettings")
@ -140,7 +183,8 @@ test.describe("Account user settings tab", () => {
await app.closeDialog();
// Assert the avatar's initial characters are set
await expect(page.locator(".mx_UserMenu .mx_BaseAvatar").getByText("A")).toBeVisible(); // Alice
const menu = await app.openUserMenu();
await expect(menu.getByRole("img", { name: user.userId }).getByText("A")).toBeVisible(); // Alice
await expect(page.locator(".mx_RoomView_wrapper .mx_BaseAvatar").getByText("A")).toBeVisible(); // Alice
});

View File

@ -318,6 +318,9 @@ test.describe("Spaces", () => {
await spaceTree.getByRole("button", { name: "Expand" }).click();
await expect(page.locator(".mx_SpacePanel:not(.collapsed)")).toBeVisible(); // TODO: replace :not() selector
// focus the quick settings button to ensure the spaces aren't being hovered over for consistent screenshots
await page.getByRole("button", { name: "Quick settings" }).focus();
const item = page.locator(".mx_SpaceItem", { hasText: "Root Space" });
await expect(item).toBeVisible();
await expect(item.locator(".mx_SpaceItem", { hasText: "Child Space" })).toBeVisible();

View File

@ -6,8 +6,18 @@ 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 } from "playwright-core";
import { test, expect } from "../../element-web-test";
const screenshotOptions = (page?: Page) => ({
// Hide the UserID
css: `
span[data-testid="userId"] {
display: none !important;
}
`,
});
test.describe("User Menu", () => {
test.use({ displayName: "Jeff" });
@ -15,8 +25,8 @@ test.describe("User Menu", () => {
await page.getByRole("button", { name: "User menu", exact: true }).click();
const menu = page.getByRole("menu");
await expect(menu.locator(".mx_UserMenu_contextMenu_displayName", { hasText: user.displayName })).toBeVisible();
await expect(menu.locator(".mx_UserMenu_contextMenu_userId", { hasText: user.userId })).toBeVisible();
await expect(menu).toMatchScreenshot("user-menu.png");
await expect(menu.getByText(user.displayName)).toBeVisible();
await expect(menu.getByText(user.userId)).toBeVisible();
await expect(menu).toMatchScreenshot("user-menu.png", screenshotOptions(page));
});
});

View File

@ -17,8 +17,8 @@ export class Settings {
* Open the top left user menu, returning a Locator to the resulting context menu.
*/
public async openUserMenu(): Promise<Locator> {
const locator = this.page.locator(".mx_ContextualMenu");
if (await locator.locator(".mx_UserMenu_contextMenu_header").isVisible()) return locator;
const locator = this.page.getByRole("menu", { name: "User menu" });
if (await locator.isVisible()) return locator;
await this.page.getByRole("button", { name: "User menu" }).click();
await locator.waitFor();
return locator;

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -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:c765fb602f78e77eccaa8e020e56c39eef99eccbabc9cb0df2c5705f60ca899e";
"ghcr.io/element-hq/matrix-authentication-service:main@sha256:4b32f35c0c3367d0884abf039f3741fd55ff2dca683c04ee92bee351fd9a9403";
/**
* MatrixAuthenticationServiceContainer which freezes the docker digest to

View File

@ -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:53b1c81dc161be1d999344ca727a520972b912fc24681dfafe25fca4b766b2a5";
"ghcr.io/element-hq/synapse:develop@sha256:db0edf9064ca0e6da942eff82e328ac5aecbe64dff301ffb9f4fa6e03bd28e4d";
/**
* SynapseContainer which freezes the docker digest to stabilise tests,

View File

@ -84,7 +84,6 @@
@import "./structures/_ThreadsActivityCentre.pcss";
@import "./structures/_ToastContainer.pcss";
@import "./structures/_UploadBar.pcss";
@import "./structures/_UserMenu.pcss";
@import "./structures/_ViewSource.pcss";
@import "./structures/auth/_CompleteSecurity.pcss";
@import "./structures/auth/_ConfirmSessionLockTheftView.pcss";
@ -223,8 +222,6 @@
@import "./views/messages/_CallEvent.pcss";
@import "./views/messages/_CreateEvent.pcss";
@import "./views/messages/_DisambiguatedProfile.pcss";
@import "./views/messages/_HiddenBody.pcss";
@import "./views/messages/_HiddenMediaPlaceholder.pcss";
@import "./views/messages/_LegacyCallEvent.pcss";
@import "./views/messages/_MFileBody.pcss";
@import "./views/messages/_MImageBody.pcss";
@ -235,12 +232,10 @@
@import "./views/messages/_MStickerBody.pcss";
@import "./views/messages/_MediaBody.pcss";
@import "./views/messages/_MessageActionBar.pcss";
@import "./views/messages/_MjolnirBody.pcss";
@import "./views/messages/_ReactionsRow.pcss";
@import "./views/messages/_RoomAvatarEvent.pcss";
@import "./views/messages/_TextualEvent.pcss";
@import "./views/messages/_ThreadActionBar.pcss";
@import "./views/messages/_UnknownBody.pcss";
@import "./views/messages/_ViewSourceEvent.pcss";
@import "./views/messages/_common_CryptoEvent.pcss";
@import "./views/polls/pollHistory/_PollHistory.pcss";

View File

@ -8,30 +8,27 @@ Please see LICENSE files in the repository root for full details.
*/
.mx_HomePage {
max-width: 960px;
width: 100%;
display: grid;
place-items: center;
width: min(100% - 50px, 960px);
height: 100%;
margin-left: auto;
margin-right: auto;
margin: 0 auto;
container-type: inline-size;
container-name: homepage;
}
.mx_HomePage_default {
text-align: center;
display: flex;
.mx_HomePage_default_wrapper {
margin: auto;
}
img {
height: 48px;
}
h1 {
margin-bottom: 4px;
font-weight: var(--cpd-font-weight-semibold);
font-size: $font-32px;
line-height: 1.375;
margin-bottom: 4px;
}
h2 {
@ -47,23 +44,24 @@ Please see LICENSE files in the repository root for full details.
}
.mx_HomePage_default_buttons {
display: flex;
margin: 60px auto 0;
width: fit-content;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 5px 40px;
width: 100%;
margin-top: 80px;
margin-bottom: 20px;
.mx_AccessibleButton {
padding: 73px 8px 15px; /* top: 20px top padding + 40px icon + 13px margin */
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 13px;
box-sizing: border-box;
width: 160px;
min-height: 132px;
margin: 20px;
position: relative;
display: inline-block;
padding: 8px 15px;
border-radius: 8px;
vertical-align: top;
word-break: break-word;
box-sizing: border-box;
font-weight: var(--cpd-font-weight-semibold);
font-size: $font-15px;
line-height: $font-20px;
@ -71,13 +69,26 @@ Please see LICENSE files in the repository root for full details.
background-color: $accent;
svg {
top: 20px;
left: 60px; /* (160px-40px)/2 */
width: 40px;
display: block;
width: 100%;
height: 40px;
position: absolute;
object-fit: contain;
color: #fff; /* on all themes */
}
}
@container homepage (max-width: 559px) {
grid-template-columns: 1fr;
.mx_AccessibleButton {
flex-direction: row;
width: 100%;
min-height: 0;
svg {
width: 24px;
}
}
}
}
}

View File

@ -339,26 +339,6 @@ Please see LICENSE files in the repository root for full details.
mask-repeat: no-repeat;
}
}
.mx_UserMenu {
padding-bottom: 12px;
border-bottom: 1px solid $separator;
margin: 12px 14px 4px 18px;
width: min-content;
max-width: 226px;
/* Display the container and img here as block elements so they don't take
* up extra vertical space.
*/
.mx_UserMenu_userAvatar_BaseAvatar {
display: block;
}
}
&.newUi .mx_UserMenu {
margin-top: var(--cpd-space-4x);
border-bottom: none;
}
}
.mx_SpacePanel_contextMenu {

View File

@ -86,6 +86,12 @@
/* Arbitrary size, keep the TAC as the wanted width */
width: 202px;
}
/* Hide the notification badge on hover — compound's `nav-hint ~ *` rule would normally
* do this, but the app-web CSS layer overrides compound-web regardless of specificity. */
&:hover .mx_NotificationBadge {
display: none;
}
}
}

View File

@ -1,126 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2020 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.
*/
.mx_UserMenu {
box-sizing: border-box;
display: flex;
align-items: center;
.mx_AccessibleButton {
display: flex;
align-items: center;
.mx_UserMenu_userAvatar {
position: relative;
.mx_BaseAvatar {
pointer-events: none; /* makes the avatar non-draggable */
}
}
}
.mx_UserMenu_contextMenuButton {
width: 100%;
}
.mx_UserMenu_name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: var(--cpd-font-weight-semibold);
font-size: $font-15px;
line-height: $font-24px;
margin-left: 10px;
}
}
.mx_IconizedContextMenu {
&.mx_UserMenu_contextMenu {
width: 258px;
}
}
.mx_UserMenu_contextMenu {
&.mx_IconizedContextMenu .mx_IconizedContextMenu_optionList_red {
.mx_AccessibleButton {
padding-top: 16px;
padding-bottom: 16px;
}
}
.mx_UserMenu_contextMenu_header {
padding: 20px;
/* Create a flexbox to organize the header a bit easier */
display: flex;
align-items: center;
.mx_UserMenu_contextMenu_name {
/* Create another flexbox of columns to handle large user IDs */
display: flex;
flex-direction: column;
width: calc(100% - 40px); /* 40px = 32px theme button + 8px margin to theme button */
.mx_UserMenu_contextMenu_displayName,
.mx_UserMenu_contextMenu_userId {
font: var(--cpd-font-body-lg-regular);
/* Automatically grow subelements to fit the container */
flex: 1;
width: 100%;
/* Ellipsize text overflow */
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.mx_UserMenu_contextMenu_displayName {
font-weight: var(--cpd-font-weight-semibold);
}
}
.mx_UserMenu_contextMenu_themeButton {
flex-shrink: 0;
margin-left: 8px;
/* to make alignment easier, create flexbox for the image */
display: flex;
align-items: center;
justify-content: center;
/* For enhanced visibility under contrast control */
outline: 1px solid transparent;
/* Compound overrides to match transitional designs */
padding: var(--cpd-space-2x);
svg {
width: 16px;
height: 16px;
}
}
&.mx_UserMenu_contextMenu_guestPrompts {
padding-top: 0;
display: inline-block;
> span {
font-weight: var(--cpd-font-weight-semibold);
display: block;
& + span {
margin-top: 8px;
}
}
}
}
.mx_IconizedContextMenu_icon svg {
color: $icon-button-color;
}
}

View File

@ -1,22 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
.mx_HiddenBody {
white-space: pre-wrap;
color: $muted-fg-color;
vertical-align: middle;
svg {
height: 14px;
width: 14px;
display: inline-block;
margin-right: var(--cpd-space-1-5x);
color: $muted-fg-color;
vertical-align: -2px;
}
}

View File

@ -1,29 +0,0 @@
.mx_HiddenMediaPlaceholder {
border: none;
width: 100%;
height: 100%;
inset: 0;
/* To center the text in the middle of the frame */
display: flex;
align-items: center;
justify-content: center;
text-align: center;
cursor: pointer;
background-color: $header-panel-bg-color;
> div {
color: $accent;
/* Icon alignment */
display: flex;
> svg {
margin-top: auto;
margin-bottom: auto;
}
}
}
.mx_EventTile:hover .mx_HiddenMediaPlaceholder {
background-color: $background;
}

View File

@ -1,11 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2019 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.
*/
.mx_MjolnirBody {
opacity: 0.4;
}

View File

@ -7,12 +7,8 @@ Please see LICENSE files in the repository root for full details.
*/
.mx_TextualEvent {
overflow-y: hidden;
line-height: normal;
a {
color: $accent;
cursor: pointer;
}
.mx_RoomView_searchResultsPanel & {

View File

@ -1,11 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2015, 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.
*/
.mx_UnknownBody {
white-space: pre-wrap;
}

View File

@ -21,7 +21,7 @@
"type": "image/png"
},
{
"src": "/vector-icons/152png",
"src": "/vector-icons/152.png",
"sizes": "152x152",
"type": "image/png"
},

View File

@ -82,10 +82,6 @@ $accent-1400: var(--cpd-color-green-1400);
}
}
.mx_UserMenu_contextMenu .mx_UserMenu_contextMenu_header .mx_UserMenu_contextMenu_themeButton {
background-color: $panel-actions !important;
}
.mx_ThemeChoicePanel_themeSelectors > .mx_StyledRadioButton input[type="radio"]:disabled + div {
border-color: $primary-content;
}

View File

@ -34,6 +34,7 @@ import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
import Measured from "../views/elements/Measured";
import EmptyState from "../views/right_panel/EmptyState";
import { ScopedRoomContextProvider } from "../../contexts/ScopedRoomContext.tsx";
import { EventPresentationContextProvider } from "../../utils/EventPresentationContextProvider";
interface IProps {
roomId: string;
@ -286,15 +287,17 @@ class FilePanel extends React.Component<IProps, IState> {
>
<Measured sensor={this.card} onMeasurement={this.onMeasurement} />
<SearchWarning isRoomEncrypted={isRoomEncrypted} kind={WarningKind.Files} />
<TimelinePanel
manageReadReceipts={false}
manageReadMarkers={false}
timelineSet={this.state.timelineSet}
showUrlPreview={false}
onPaginationRequest={this.onPaginationRequest}
empty={emptyState}
layout={Layout.Group}
/>
<EventPresentationContextProvider layout={Layout.Group}>
<TimelinePanel
manageReadReceipts={false}
manageReadMarkers={false}
timelineSet={this.state.timelineSet}
showUrlPreview={false}
onPaginationRequest={this.onPaginationRequest}
empty={emptyState}
layout={Layout.Group}
/>
</EventPresentationContextProvider>
</BaseCard>
</ScopedRoomContextProvider>
);

View File

@ -143,6 +143,7 @@ import { RoomStatusBarViewModel } from "../../viewmodels/room/RoomStatusBar.ts";
import { EncryptionEventViewModel } from "../../viewmodels/room/timeline/event-tile/EncryptionEventViewModel.ts";
import { ModuleApi } from "../../modules/Api.ts";
import { RoomUploadContextProvider } from "../../viewmodels/room/RoomUploadViewModel.tsx";
import { EventPresentationContextProvider } from "../../utils/EventPresentationContextProvider";
const DEBUG = false;
const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000;
@ -2571,32 +2572,34 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
let messagePanel: JSX.Element | undefined;
if (!isRoomEncryptionLoading) {
messagePanel = (
<TimelinePanel
ref={this.gatherTimelinePanelRef}
timelineSet={this.state.room.getUnfilteredTimelineSet()}
showReadReceipts={this.state.showReadReceipts}
manageReadReceipts={!this.state.isPeeking}
sendReadReceiptOnLoad={
!this.state.wasContextSwitch && this.props.enableReadReceiptsAndMarkersOnActivity
}
manageReadMarkers={!this.state.isPeeking}
hidden={hideMessagePanel}
highlightedEventId={highlightedEventId}
eventId={this.state.initialEventId}
eventScrollIntoView={this.state.initialEventScrollIntoView}
eventPixelOffset={this.state.initialEventPixelOffset}
onScroll={this.onMessageListScroll}
onEventScrolledIntoView={this.resetJumpToEvent}
onReadMarkerUpdated={this.updateTopUnreadMessagesBar}
showUrlPreview={this.state.showUrlPreview}
className={this.messagePanelClassNames}
membersLoaded={this.state.membersLoaded}
permalinkCreator={this.permalinkCreator}
showReactions={true}
layout={this.state.layout}
editState={this.state.editState}
enableReadReceiptsAndMarkersOnActivity={this.props.enableReadReceiptsAndMarkersOnActivity}
/>
<EventPresentationContextProvider layout={this.state.layout}>
<TimelinePanel
ref={this.gatherTimelinePanelRef}
timelineSet={this.state.room.getUnfilteredTimelineSet()}
showReadReceipts={this.state.showReadReceipts}
manageReadReceipts={!this.state.isPeeking}
sendReadReceiptOnLoad={
!this.state.wasContextSwitch && this.props.enableReadReceiptsAndMarkersOnActivity
}
manageReadMarkers={!this.state.isPeeking}
hidden={hideMessagePanel}
highlightedEventId={highlightedEventId}
eventId={this.state.initialEventId}
eventScrollIntoView={this.state.initialEventScrollIntoView}
eventPixelOffset={this.state.initialEventPixelOffset}
onScroll={this.onMessageListScroll}
onEventScrolledIntoView={this.resetJumpToEvent}
onReadMarkerUpdated={this.updateTopUnreadMessagesBar}
showUrlPreview={this.state.showUrlPreview}
className={this.messagePanelClassNames}
membersLoaded={this.state.membersLoaded}
permalinkCreator={this.permalinkCreator}
showReactions={true}
layout={this.state.layout}
editState={this.state.editState}
enableReadReceiptsAndMarkersOnActivity={this.props.enableReadReceiptsAndMarkersOnActivity}
/>
</EventPresentationContextProvider>
);
}

View File

@ -51,6 +51,7 @@ import Heading from "../views/typography/Heading";
import { type ThreadPayload } from "../../dispatcher/payloads/ThreadPayload";
import { ScopedRoomContextProvider } from "../../contexts/ScopedRoomContext.tsx";
import { RoomUploadContextProvider } from "../../viewmodels/room/RoomUploadViewModel.tsx";
import { EventPresentationContextProvider } from "../../utils/EventPresentationContextProvider";
interface IProps {
room: Room;
@ -372,32 +373,36 @@ export default class ThreadView extends React.Component<IProps, IState> {
);
}
const layout = this.state.layout === Layout.Bubble ? Layout.Bubble : Layout.Group;
timeline = (
<>
<FileDropTarget parent={this.card.current} />
<TimelinePanel
key={this.state.thread.id}
ref={this.timelinePanel}
showReadReceipts={this.context.showReadReceipts}
manageReadReceipts={true}
manageReadMarkers={true}
sendReadReceiptOnLoad={true}
timelineSet={this.state.thread.timelineSet}
showUrlPreview={this.context.showUrlPreview}
// ThreadView doesn't support IRC layout at this time
layout={this.state.layout === Layout.Bubble ? Layout.Bubble : Layout.Group}
hideThreadedMessages={false}
hidden={false}
showReactions={true}
className="mx_RoomView_messagePanel"
permalinkCreator={this.props.permalinkCreator}
membersLoaded={true}
editState={this.state.editState}
eventId={this.props.initialEvent?.getId()}
highlightedEventId={highlightedEventId}
eventScrollIntoView={this.props.initialEventScrollIntoView}
onEventScrolledIntoView={this.resetJumpToEvent}
/>
<EventPresentationContextProvider layout={layout}>
<TimelinePanel
key={this.state.thread.id}
ref={this.timelinePanel}
showReadReceipts={this.context.showReadReceipts}
manageReadReceipts={true}
manageReadMarkers={true}
sendReadReceiptOnLoad={true}
timelineSet={this.state.thread.timelineSet}
showUrlPreview={this.context.showUrlPreview}
// ThreadView doesn't support IRC layout at this time
layout={layout}
hideThreadedMessages={false}
hidden={false}
showReactions={true}
className="mx_RoomView_messagePanel"
permalinkCreator={this.props.permalinkCreator}
membersLoaded={true}
editState={this.state.editState}
eventId={this.props.initialEvent?.getId()}
highlightedEventId={highlightedEventId}
eventScrollIntoView={this.props.initialEventScrollIntoView}
onEventScrolledIntoView={this.resetJumpToEvent}
/>
</EventPresentationContextProvider>
</>
);
} else {

View File

@ -1,441 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2020, 2021 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 JSX, createRef, type ReactNode, useMemo } from "react";
import { type Room } from "matrix-js-sdk/src/matrix";
import {
ChatSolidIcon,
HomeSolidIcon,
LockSolidIcon,
QrCodeIcon,
SettingsSolidIcon,
LeaveIcon,
NotificationsSolidIcon,
ThemeIcon,
} from "@vector-im/compound-design-tokens/assets/web/icons";
import { IconButton } from "@vector-im/compound-web";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import defaultDispatcher from "../../dispatcher/dispatcher";
import { type ActionPayload } from "../../dispatcher/payloads";
import { Action } from "../../dispatcher/actions";
import { _t } from "../../languageHandler";
import { ChevronFace, ContextMenuButton, type MenuProps } from "./ContextMenu";
import { UserTab } from "../views/dialogs/UserTab";
import { type OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
import FeedbackDialog from "../views/dialogs/FeedbackDialog";
import Modal from "../../Modal";
import LogoutDialog, { shouldShowLogoutDialog } from "../views/dialogs/LogoutDialog";
import SettingsStore from "../../settings/SettingsStore";
import { findHighContrastTheme, isHighContrastTheme } from "../../theme";
import { useRovingTabIndex } from "../../accessibility/RovingTabIndex";
import AccessibleButton, { type ButtonEvent } from "../views/elements/AccessibleButton";
import SdkConfig from "../../SdkConfig";
import { getHomePageUrl } from "../../utils/pages";
import { OwnProfileStore } from "../../stores/OwnProfileStore";
import { UPDATE_EVENT } from "../../stores/AsyncStore";
import BaseAvatar from "../views/avatars/BaseAvatar";
import { SettingLevel } from "../../settings/SettingLevel";
import IconizedContextMenu, {
IconizedContextMenuOption,
IconizedContextMenuOptionList,
} from "../views/context_menus/IconizedContextMenu";
import { UIFeature } from "../../settings/UIFeature";
import SpaceStore from "../../stores/spaces/SpaceStore";
import { UPDATE_SELECTED_SPACE } from "../../stores/spaces";
import UserIdentifierCustomisations from "../../customisations/UserIdentifier";
import PosthogTrackers from "../../PosthogTrackers";
import { type ViewHomePagePayload } from "../../dispatcher/payloads/ViewHomePagePayload";
import { SDKContext } from "../../contexts/SDKContext";
import { shouldShowFeedback } from "../../utils/Feedback";
import ThemeWatcher, { ThemeWatcherEvent } from "../../settings/watchers/ThemeWatcher.ts";
import { useTypedEventEmitterState } from "../../hooks/useEventEmitter.ts";
interface IProps {
isPanelCollapsed: boolean;
children?: ReactNode;
}
type PartialDOMRect = Pick<DOMRect, "width" | "left" | "top" | "height">;
interface IState {
contextMenuPosition: PartialDOMRect | null;
selectedSpace?: Room | null;
}
const toRightOf = (rect: PartialDOMRect): MenuProps => {
return {
left: rect.width + rect.left + 8,
top: rect.top,
chevronFace: ChevronFace.None,
};
};
const below = (rect: PartialDOMRect): MenuProps => {
return {
left: rect.left,
top: rect.top + rect.height,
chevronFace: ChevronFace.None,
};
};
const ThemeSwitchButton = (): JSX.Element => {
const [onFocus, isActive, ref] = useRovingTabIndex();
const themeWatcher = useMemo(() => new ThemeWatcher(), []);
const [isHighContrast, isDark] = useTypedEventEmitterState(
themeWatcher,
ThemeWatcherEvent.Change,
(theme: string) => [isHighContrastTheme(theme), themeWatcher.isUserOnDarkTheme()],
);
const onSwitchThemeClick = (ev: ButtonEvent): void => {
ev.preventDefault();
ev.stopPropagation();
PosthogTrackers.trackInteraction("WebUserMenuThemeToggleButton", ev);
// Disable system theme matching if the user hits this button
SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, false);
let newTheme = isDark ? "light" : "dark";
if (isHighContrast) {
const hcTheme = findHighContrastTheme(newTheme);
if (hcTheme) {
newTheme = hcTheme;
}
}
SettingsStore.setValue("theme", null, SettingLevel.DEVICE, newTheme); // set at same level as Appearance tab
themeWatcher.recheck(newTheme);
};
return (
<IconButton
ref={ref}
onFocus={onFocus}
tabIndex={isActive ? 0 : -1}
className="mx_UserMenu_contextMenu_themeButton"
onClick={onSwitchThemeClick}
tooltip={isDark ? _t("user_menu|switch_theme_light") : _t("user_menu|switch_theme_dark")}
size="32px"
kind="secondary"
>
<ThemeIcon />
</IconButton>
);
};
export default class UserMenu extends React.Component<IProps, IState> {
public static contextType = SDKContext;
declare public context: React.ContextType<typeof SDKContext>;
private dispatcherRef?: string;
private themeWatcherRef?: string;
private readonly dndWatcherRef?: string;
private buttonRef = createRef<HTMLButtonElement>();
public constructor(props: IProps) {
super(props);
this.state = {
contextMenuPosition: null,
selectedSpace: SpaceStore.instance.activeSpaceRoom,
};
}
private get hasHomePage(): boolean {
return !!getHomePageUrl(SdkConfig.get(), this.context.client!);
}
public componentDidMount(): void {
OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
this.dispatcherRef = defaultDispatcher.register(this.onAction);
}
public componentWillUnmount(): void {
SettingsStore.unwatchSetting(this.themeWatcherRef);
SettingsStore.unwatchSetting(this.dndWatcherRef);
defaultDispatcher.unregister(this.dispatcherRef);
OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate);
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
}
private onProfileUpdate = async (): Promise<void> => {
// the store triggered an update, so force a layout update. We don't
// have any state to store here for that to magically happen.
this.forceUpdate();
};
private onSelectedSpaceUpdate = async (): Promise<void> => {
this.setState({
selectedSpace: SpaceStore.instance.activeSpaceRoom,
});
};
private onAction = (payload: ActionPayload): void => {
switch (payload.action) {
case Action.ToggleUserMenu:
if (this.state.contextMenuPosition) {
this.setState({ contextMenuPosition: null });
} else {
if (this.buttonRef.current) this.buttonRef.current.click();
}
break;
}
};
private onOpenMenuClick = (ev: ButtonEvent): void => {
ev.preventDefault();
ev.stopPropagation();
this.setState({ contextMenuPosition: ev.currentTarget.getBoundingClientRect() });
};
private onContextMenu = (ev: React.MouseEvent): void => {
ev.preventDefault();
ev.stopPropagation();
this.setState({
contextMenuPosition: {
left: ev.clientX,
top: ev.clientY,
width: 20,
height: 0,
},
});
};
private onCloseMenu = (): void => {
this.setState({ contextMenuPosition: null });
};
private onSettingsOpen = (ev: ButtonEvent, tabId?: string, props?: Record<string, any>): void => {
ev.preventDefault();
ev.stopPropagation();
const payload: OpenToTabPayload = { action: Action.ViewUserSettings, initialTabId: tabId, props };
defaultDispatcher.dispatch(payload);
this.setState({ contextMenuPosition: null }); // also close the menu
};
private onProvideFeedback = (ev: ButtonEvent): void => {
ev.preventDefault();
ev.stopPropagation();
Modal.createDialog(FeedbackDialog);
this.setState({ contextMenuPosition: null }); // also close the menu
};
private onSignOutClick = async (ev: ButtonEvent): Promise<void> => {
ev.preventDefault();
ev.stopPropagation();
if (await shouldShowLogoutDialog(MatrixClientPeg.safeGet())) {
Modal.createDialog(LogoutDialog);
} else {
defaultDispatcher.dispatch({ action: "logout" });
}
this.setState({ contextMenuPosition: null }); // also close the menu
};
private onSignInClick = (): void => {
defaultDispatcher.dispatch({ action: "start_login" });
this.setState({ contextMenuPosition: null }); // also close the menu
};
private onRegisterClick = (): void => {
defaultDispatcher.dispatch({ action: "start_registration" });
this.setState({ contextMenuPosition: null }); // also close the menu
};
private onHomeClick = (ev: ButtonEvent): void => {
ev.preventDefault();
ev.stopPropagation();
defaultDispatcher.dispatch<ViewHomePagePayload>({ action: Action.ViewHomePage });
this.setState({ contextMenuPosition: null }); // also close the menu
};
private renderContextMenu = (): React.ReactNode => {
if (!this.state.contextMenuPosition) return null;
let topSection: JSX.Element | undefined;
if (MatrixClientPeg.safeGet().isGuest()) {
topSection = (
<div className="mx_UserMenu_contextMenu_header mx_UserMenu_contextMenu_guestPrompts">
{_t(
"auth|sign_in_prompt",
{},
{
a: (sub) => (
<AccessibleButton kind="link_inline" onClick={this.onSignInClick}>
{sub}
</AccessibleButton>
),
},
)}
{SettingsStore.getValue(UIFeature.Registration)
? _t(
"auth|create_account_prompt",
{},
{
a: (sub) => (
<AccessibleButton kind="link_inline" onClick={this.onRegisterClick}>
{sub}
</AccessibleButton>
),
},
)
: null}
</div>
);
}
let homeButton: JSX.Element | undefined;
if (this.hasHomePage) {
homeButton = (
<IconizedContextMenuOption
icon={<HomeSolidIcon />}
label={_t("common|home")}
onClick={this.onHomeClick}
/>
);
}
let feedbackButton: JSX.Element | undefined;
if (shouldShowFeedback()) {
feedbackButton = (
<IconizedContextMenuOption
icon={<ChatSolidIcon />}
label={_t("common|feedback")}
onClick={this.onProvideFeedback}
/>
);
}
const linkNewDeviceButton = (
<IconizedContextMenuOption
icon={<QrCodeIcon />}
label={_t("user_menu|link_new_device")}
onClick={(e) => this.onSettingsOpen(e, UserTab.SessionManager, { showMsc4108QrCode: true })}
/>
);
let primaryOptionList = (
<IconizedContextMenuOptionList>
{homeButton}
{linkNewDeviceButton}
<IconizedContextMenuOption
icon={<NotificationsSolidIcon />}
label={_t("notifications|enable_prompt_toast_title")}
onClick={(e) => this.onSettingsOpen(e, UserTab.Notifications)}
/>
<IconizedContextMenuOption
icon={<LockSolidIcon />}
label={_t("room_settings|security|title")}
onClick={(e) => this.onSettingsOpen(e, UserTab.Security)}
/>
<IconizedContextMenuOption
icon={<SettingsSolidIcon />}
label={_t("user_menu|settings")}
onClick={(e) => this.onSettingsOpen(e)}
/>
{feedbackButton}
<IconizedContextMenuOption
className="mx_IconizedContextMenu_option_red"
icon={<LeaveIcon />}
label={_t("action|sign_out")}
onClick={this.onSignOutClick}
/>
</IconizedContextMenuOptionList>
);
if (MatrixClientPeg.safeGet().isGuest()) {
primaryOptionList = (
<IconizedContextMenuOptionList>
{homeButton}
<IconizedContextMenuOption
icon={<SettingsSolidIcon />}
label={_t("common|settings")}
onClick={(e) => this.onSettingsOpen(e)}
/>
{feedbackButton}
</IconizedContextMenuOptionList>
);
}
const position = this.props.isPanelCollapsed
? toRightOf(this.state.contextMenuPosition)
: below(this.state.contextMenuPosition);
const userIdentifierString = UserIdentifierCustomisations.getDisplayUserIdentifier(
MatrixClientPeg.safeGet().getSafeUserId(),
{
withDisplayName: true,
},
);
return (
<IconizedContextMenu {...position} onFinished={this.onCloseMenu} className="mx_UserMenu_contextMenu">
<div className="mx_UserMenu_contextMenu_header">
<div className="mx_UserMenu_contextMenu_name">
<span className="mx_UserMenu_contextMenu_displayName">
{OwnProfileStore.instance.displayName}
</span>
<span className="mx_UserMenu_contextMenu_userId" title={userIdentifierString || ""}>
{userIdentifierString}
</span>
</div>
<ThemeSwitchButton />
</div>
{topSection}
{primaryOptionList}
</IconizedContextMenu>
);
};
public render(): React.ReactNode {
const avatarSize = 32; // should match border-radius of the avatar
const userId = MatrixClientPeg.safeGet().getSafeUserId();
const displayName = OwnProfileStore.instance.displayName || userId;
const avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize);
let name: JSX.Element | undefined;
if (!this.props.isPanelCollapsed) {
name = <div className="mx_UserMenu_name">{displayName}</div>;
}
return (
<div className="mx_UserMenu">
<ContextMenuButton
className="mx_UserMenu_contextMenuButton"
onClick={this.onOpenMenuClick}
ref={this.buttonRef}
label={_t("a11y|user_menu")}
isExpanded={!!this.state.contextMenuPosition}
onContextMenu={this.onContextMenu}
>
<div className="mx_UserMenu_userAvatar">
<BaseAvatar
idName={userId}
name={displayName}
url={avatarUrl}
size={avatarSize + "px"}
className="mx_UserMenu_userAvatar_BaseAvatar"
/>
</div>
{name}
{this.renderContextMenu()}
</ContextMenuButton>
{this.props.children}
</div>
);
}
}

View File

@ -28,13 +28,7 @@ const UserAvatar: React.FC = () => {
return (
<div className={`mx_ShareType_option-icon ${LocationShareType.Own}`}>
<BaseAvatar
idName={userId}
name={displayName}
url={avatarUrl}
size={avatarSize}
className="mx_UserMenu_userAvatar_BaseAvatar"
/>
<BaseAvatar idName={userId} name={displayName} url={avatarUrl} size={avatarSize} />
</div>
);
};

View File

@ -1,44 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React, { type JSX } from "react";
import { VisibilityOffIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { _t } from "../../../languageHandler";
import { type IBodyProps } from "./IBodyProps";
/**
* A message hidden from the user pending moderation.
*
* Note: This component must not be used when the user is the author of the message
* or has a sufficient powerlevel to see the message.
*/
const HiddenBody = ({ mxEvent, ref }: IBodyProps): JSX.Element => {
let text;
const visibility = mxEvent.messageVisibility();
switch (visibility.visible) {
case true:
throw new Error("HiddenBody should only be applied to hidden messages");
case false:
if (visibility.reason) {
text = _t("timeline|pending_moderation_reason", { reason: visibility.reason });
} else {
text = _t("timeline|pending_moderation");
}
break;
}
return (
<span className="mx_HiddenBody" ref={ref}>
<VisibilityOffIcon />
{text}
</span>
);
};
export default HiddenBody;

View File

@ -1,24 +0,0 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React, { type PropsWithChildren, type MouseEventHandler } from "react";
import { VisibilityOnIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
interface IProps {
onClick: MouseEventHandler<HTMLButtonElement>;
}
export const HiddenMediaPlaceholder: React.FunctionComponent<PropsWithChildren<IProps>> = ({ onClick, children }) => {
return (
<button onClick={onClick} className="mx_HiddenMediaPlaceholder">
<div>
<VisibilityOnIcon />
<span>{children}</span>
</div>
</button>
);
};

View File

@ -16,6 +16,7 @@ import { ClientEvent } from "matrix-js-sdk/src/matrix";
import { type ImageContent } from "matrix-js-sdk/src/types";
import { Tooltip } from "@vector-im/compound-web";
import { ImageErrorIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { HiddenMediaPlaceholder } from "@element-hq/web-shared-components";
import Modal from "../../../Modal";
import { _t } from "../../../languageHandler";
@ -33,7 +34,6 @@ import { presentableTextForFile } from "../../../utils/FileUtils";
import { createReconnectedListener } from "../../../utils/connection";
import MediaProcessingError from "./shared/MediaProcessingError";
import { DecryptError, DownloadError } from "../../../utils/DecryptFile";
import { HiddenMediaPlaceholder } from "./HiddenMediaPlaceholder";
import { useMediaVisible } from "../../../hooks/useMediaVisible";
import { isMimeTypeAllowed } from "../../../utils/blobs.ts";
import { FileBodyFactory, renderMBody } from "./MBodyFactory";

View File

@ -1,87 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2019, 2020 , 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React, { type JSX } from "react";
import { type MatrixEvent } from "matrix-js-sdk/src/matrix";
import { LockSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { EventTileBubble } from "@element-hq/web-shared-components";
import { _t } from "../../../languageHandler";
import { getNameForEventRoom, userLabelForEventRoom } from "../../../utils/KeyVerificationStateObserver";
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
interface Props {
mxEvent: MatrixEvent;
timestamp?: JSX.Element;
}
interface MKeyVerificationRequestContent {
body?: string;
format?: string;
formatted_body?: string;
from_device: string;
methods: Array<string>;
msgtype: "m.key.verification.request";
to: string;
}
/**
* Event tile created when we receive an m.key.verification.request event.
*
* Displays a simple message saying that a verification was requested, either by
* this user or someone else.
*
* EventTileFactory has logic meaning we only display this tile if the request
* was sent to/from this user.
*/
const MKeyVerificationRequest: React.FC<Props> = ({ mxEvent, timestamp }) => {
const client = useMatrixClientContext();
if (!client) {
throw new Error("Attempting to render verification request without a client context!");
}
const myUserId = client.getSafeUserId();
const content: MKeyVerificationRequestContent = mxEvent.getContent();
const sender = mxEvent.getSender();
const receiver = content.to;
const roomId = mxEvent.getRoomId();
if (!sender) {
throw new Error("Verification request did not include a sender!");
}
if (!roomId) {
throw new Error("Verification request did not include a room ID!");
}
let title: string;
let subtitle: string;
const sentByMe = sender === myUserId;
if (sentByMe) {
title = _t("timeline|m.key.verification.request|you_started");
subtitle = userLabelForEventRoom(client, receiver, roomId);
} else {
const name = getNameForEventRoom(client, sender, roomId);
title = _t("timeline|m.key.verification.request|user_wants_to_verify", { name });
subtitle = userLabelForEventRoom(client, sender, roomId);
}
return (
<EventTileBubble
icon={<LockSolidIcon />}
className="mx_EventTileBubble mx_cryptoEvent"
title={title}
subtitle={subtitle}
>
{timestamp}
</EventTileBubble>
);
};
export default MKeyVerificationRequest;

View File

@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import mime from "mime";
import React, { createRef } from "react";
import React, { createRef, type JSX, useEffect } from "react";
import { logger } from "matrix-js-sdk/src/logger";
import {
EventType,
@ -18,10 +18,10 @@ import {
M_POLL_START,
type IContent,
} from "matrix-js-sdk/src/matrix";
import { MjolnirBodyView, UnknownBodyView, useCreateAutoDisposedViewModel } from "@element-hq/web-shared-components";
import SettingsStore from "../../../settings/SettingsStore";
import { Mjolnir } from "../../../mjolnir/Mjolnir";
import UnknownBody from "./UnknownBody";
import { type IMediaBody } from "./IMediaBody";
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
import { type IBodyProps } from "./IBodyProps";
@ -30,9 +30,9 @@ import MVoiceOrAudioBody from "./MVoiceOrAudioBody";
import MStickerBody from "./MStickerBody";
import MPollBody from "./MPollBody";
import MLocationBody from "./MLocationBody";
import MjolnirBody from "./MjolnirBody";
import MBeaconBody from "./MBeaconBody";
import { type GetRelationsForEvent, type IEventTileOps } from "../rooms/EventTile";
import { MjolnirBodyViewModel } from "../../../viewmodels/room/timeline/event-tile/body/MjolnirBodyViewModel";
import {
DecryptionFailureBodyFactory,
FileBodyFactory,
@ -80,6 +80,24 @@ const baseEvTypes = new Map<string, React.ComponentType<IBodyProps>>([
[M_BEACON_INFO.altName, MBeaconBody],
]);
function MjolnirBodyWrappedView({ mxEvent, onMessageAllowed, ref }: IBodyProps): JSX.Element {
const vm = useCreateAutoDisposedViewModel(() => new MjolnirBodyViewModel({ mxEvent, onMessageAllowed }));
useEffect(() => {
vm.setEvent(mxEvent);
}, [mxEvent, vm]);
useEffect(() => {
vm.setOnMessageAllowed(onMessageAllowed);
}, [onMessageAllowed, vm]);
return <MjolnirBodyView vm={vm} ref={ref} />;
}
function UnknownBody({ mxEvent, ref }: IBodyProps): JSX.Element {
return <UnknownBodyView text={mxEvent.getContent().body} ref={ref} className="mx_UnknownBody" />;
}
export default class MessageEvent extends React.Component<IProps> implements IMediaBody, IOperableEventTile {
private body = createRef<React.Component | IOperableEventTile>();
private mediaHelper?: MediaEventHelper;
@ -288,7 +306,7 @@ export default class MessageEvent extends React.Component<IProps> implements IMe
const serverBanned = userDomain && Mjolnir.sharedInstance().isServerBanned(userDomain);
if (userBanned || serverBanned) {
BodyType = MjolnirBody;
BodyType = MjolnirBodyWrappedView;
}
}
}

View File

@ -1,44 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2019 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 from "react";
import { _t } from "../../../languageHandler";
import AccessibleButton, { type ButtonEvent } from "../elements/AccessibleButton";
import { type IBodyProps } from "./IBodyProps";
export default class MjolnirBody extends React.Component<IBodyProps> {
private onAllowClick = (e: ButtonEvent): void => {
e.preventDefault();
e.stopPropagation();
const key = `mx_mjolnir_render_${this.props.mxEvent.getRoomId()}__${this.props.mxEvent.getId()}`;
localStorage.setItem(key, "true");
this.props.onMessageAllowed?.();
};
public render(): React.ReactNode {
return (
<div className="mx_MjolnirBody">
<i>
{_t(
"timeline|mjolnir|message_hidden",
{},
{
a: (sub) => (
<AccessibleButton kind="link_inline" onClick={this.onAllowClick}>
{sub}
</AccessibleButton>
),
},
)}
</i>
</div>
);
}
}

View File

@ -1,21 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2015, 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 JSX } from "react";
import { type IBodyProps } from "./IBodyProps";
export default ({ mxEvent, ref }: IBodyProps): JSX.Element => {
const text = mxEvent.getContent().body;
return (
<div className="mx_UnknownBody" ref={ref}>
{text}
</div>
);
};

View File

@ -39,6 +39,7 @@ import Measured from "../elements/Measured";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import { ScopedRoomContextProvider } from "../../../contexts/ScopedRoomContext.tsx";
import { RoomUploadContextProvider } from "../../../viewmodels/room/RoomUploadViewModel.tsx";
import { EventPresentationContextProvider } from "../../../utils/EventPresentationContextProvider";
interface IProps {
room: Room;
@ -198,6 +199,7 @@ export default class TimelineCard extends React.Component<IProps, IState> {
const myMembership = this.props.room.getMyMembership();
const showComposer = myMembership === KnownMembership.Join;
const layout = this.state.layout === Layout.Bubble ? Layout.Bubble : Layout.Group;
return (
<ScopedRoomContextProvider
@ -217,27 +219,29 @@ export default class TimelineCard extends React.Component<IProps, IState> {
<Measured sensor={this.card} onMeasurement={this.onMeasurement} />
<div className="mx_TimelineCard_timeline">
{jumpToBottom}
<TimelinePanel
ref={this.timelinePanel}
showReadReceipts={this.state.showReadReceipts}
manageReadReceipts={true}
manageReadMarkers={false} // No RM support in the TimelineCard
sendReadReceiptOnLoad={true}
timelineSet={this.props.timelineSet}
showUrlPreview={this.context.showUrlPreview}
// The right panel timeline (and therefore threads) don't support IRC layout at this time
layout={this.state.layout === Layout.Bubble ? Layout.Bubble : Layout.Group}
hideThreadedMessages={false}
hidden={false}
showReactions={true}
className="mx_RoomView_messagePanel"
permalinkCreator={this.props.permalinkCreator}
membersLoaded={true}
editState={this.state.editState}
eventId={this.state.initialEventId}
highlightedEventId={highlightedEventId}
onScroll={this.onScroll}
/>
<EventPresentationContextProvider layout={layout}>
<TimelinePanel
ref={this.timelinePanel}
showReadReceipts={this.state.showReadReceipts}
manageReadReceipts={true}
manageReadMarkers={false} // No RM support in the TimelineCard
sendReadReceiptOnLoad={true}
timelineSet={this.props.timelineSet}
showUrlPreview={this.context.showUrlPreview}
// The right panel timeline (and therefore threads) don't support IRC layout at this time
layout={layout}
hideThreadedMessages={false}
hidden={false}
showReactions={true}
className="mx_RoomView_messagePanel"
permalinkCreator={this.props.permalinkCreator}
membersLoaded={true}
editState={this.state.editState}
eventId={this.state.initialEventId}
highlightedEventId={highlightedEventId}
onScroll={this.onScroll}
/>
</EventPresentationContextProvider>
</div>
{isUploading && <UploadBar room={this.props.room} relation={this.props.composerRelation} />}

View File

@ -57,7 +57,6 @@ import {
ReactionsRowButtonView,
ReactionsRowView,
TileErrorView,
type TileErrorViewLayout,
useViewModel,
} from "@element-hq/web-shared-components";
@ -621,6 +620,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
/** called when the event is edited after we show it. */
private readonly onReplaced = (): void => {
this.forceUpdate();
// re-verify the event if it is replaced (the edit may not be verified)
this.verifyEvent();
};
@ -996,6 +996,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
public render(): ReactNode {
const msgtype = this.props.mxEvent.getContent().msgtype;
const eventType = this.props.mxEvent.getType();
const replacingEventId = this.props.mxEvent.replacingEventId();
const {
hasRenderer,
@ -1336,6 +1337,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
// overrides
ref: this.tile,
replacingEventId,
isSeeingThroughMessageHiddenForModeration,
// appease TS
@ -1579,24 +1581,17 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
*/
interface EventTileErrorFallbackProps {
error: Error;
layout: Layout;
mxEvent: MatrixEvent;
}
function EventTileErrorFallback({ error, layout, mxEvent }: Readonly<EventTileErrorFallbackProps>): JSX.Element {
function EventTileErrorFallback({ error, mxEvent }: Readonly<EventTileErrorFallbackProps>): JSX.Element {
const developerMode = useSettingValue("developerMode");
const vm = useCreateAutoDisposedViewModel(
() => new TileErrorViewModel({ error, layout: layout as TileErrorViewLayout, mxEvent, developerMode }),
);
const vm = useCreateAutoDisposedViewModel(() => new TileErrorViewModel({ error, mxEvent, developerMode }));
useEffect(() => {
vm.setError(error);
}, [error, vm]);
useEffect(() => {
vm.setLayout(layout as TileErrorViewLayout);
}, [layout, vm]);
useEffect(() => {
vm.setDeveloperMode(developerMode);
}, [developerMode, vm]);
@ -1606,7 +1601,6 @@ function EventTileErrorFallback({ error, layout, mxEvent }: Readonly<EventTileEr
interface EventTileErrorBoundaryProps {
children: ReactNode;
layout: Layout;
mxEvent: MatrixEvent;
}
@ -1626,13 +1620,7 @@ class EventTileErrorBoundary extends React.Component<EventTileErrorBoundaryProps
public render(): ReactNode {
if (this.state.error) {
return (
<EventTileErrorFallback
error={this.state.error}
layout={this.props.layout}
mxEvent={this.props.mxEvent}
/>
);
return <EventTileErrorFallback error={this.state.error} mxEvent={this.props.mxEvent} />;
}
return this.props.children;
@ -1642,7 +1630,7 @@ class EventTileErrorBoundary extends React.Component<EventTileErrorBoundaryProps
// 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 (
<EventTileErrorBoundary mxEvent={props.mxEvent} layout={props.layout ?? Layout.Group}>
<EventTileErrorBoundary mxEvent={props.mxEvent}>
<UnwrappedEventTile {...props} />
</EventTileErrorBoundary>
);

View File

@ -23,7 +23,7 @@ import {
StickerIcon,
TextFormattingIcon,
} from "@vector-im/compound-design-tokens/assets/web/icons";
import { UploadButton, useViewModel } from "@element-hq/web-shared-components";
import { UploadButton } from "@element-hq/web-shared-components";
import { _t } from "../../../languageHandler";
import { CollapsibleButton } from "./CollapsibleButton";

View File

@ -26,7 +26,7 @@ import { renderReplyTile } from "../../../events/EventTileFactory";
import { type GetRelationsForEvent } from "../rooms/EventTile";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { type IBodyProps } from "../messages/IBodyProps";
import { FileBodyFactory, renderMBody } from "../messages/MBodyFactory";
import { FileBodyFactory, VideoBodyFactory, renderMBody } from "../messages/MBodyFactory";
interface IProps {
mxEvent: MatrixEvent;
@ -134,9 +134,9 @@ export default class ReplyTile extends React.PureComponent<IProps> {
const msgtypeOverrides: Record<string, React.ComponentType<IBodyProps>> = {
[MsgType.Image]: MImageReplyBody,
// Override audio and video body with file body. We also hide the download/decrypt button using CSS
// Override audio body with file body. We also hide the download/decrypt button using CSS
[MsgType.Audio]: isVoiceMessage(mxEvent) ? MVoiceMessageBody : ReplyTileFileBody,
[MsgType.Video]: ReplyTileFileBody,
[MsgType.Video]: VideoBodyFactory,
};
const evOverrides: Record<string, React.ComponentType<IBodyProps>> = {
// Use MImageReplyBody so that the sticker isn't taking up a lot of space

View File

@ -54,7 +54,7 @@ import { type ActionPayload } from "../../../dispatcher/payloads";
import { decorateStartSendingTime, sendRoundTripMetric } from "../../../sendTimePerformanceMetrics";
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
import DocumentPosition from "../../../editor/position";
import { ComposerInsertPayload, ComposerType } from "../../../dispatcher/payloads/ComposerInsertPayload";
import { type ComposerInsertPayload, ComposerType } from "../../../dispatcher/payloads/ComposerInsertPayload";
import { getSlashCommand, isSlashCommand, runSlashCommand, shouldSendAnyway } from "../../../editor/commands";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { PosthogAnalytics } from "../../../PosthogAnalytics";

View File

@ -8,7 +8,6 @@ Please see LICENSE files in the repository root for full details.
import React, { type JSX, type Ref, type FunctionComponent } from "react";
import { type FormattingFunctions, type MappedSuggestion } from "@vector-im/matrix-wysiwyg";
import { logger } from "matrix-js-sdk/src/logger";
import Autocomplete from "../../Autocomplete";
import { type ICompletion } from "../../../../../autocomplete/Autocompleter";
@ -111,8 +110,6 @@ const WysiwygAutocomplete = ({
if (!room) return null;
const autoCompleteQuery = buildQuery(suggestion);
// debug for https://github.com/vector-im/element-web/issues/26037
logger.log(`## 26037 ## Rendering Autocomplete for WysiwygAutocomplete with query: "${autoCompleteQuery}"`);
// TODO - determine if we show all of the /command suggestions, there are some options in the
// list which don't seem to make sense in this context, specifically /html and /plain

View File

@ -8,8 +8,7 @@ Please see LICENSE files in the repository root for full details.
import { EMOTICON_TO_EMOJI } from "@matrix-org/emojibase-bindings";
import { type AllowedMentionAttributes, type MappedSuggestion } from "@vector-im/matrix-wysiwyg";
import { type SyntheticEvent, useState, type SetStateAction } from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { type SyntheticEvent, useState } from "react";
import { isNotNull } from "../../../../../Typeguards";
@ -57,20 +56,7 @@ export function useSuggestion(
onSelect: (event: SyntheticEvent<HTMLDivElement>) => void;
suggestion: MappedSuggestion | null;
} {
const [suggestionData, setSuggestionData0] = useState<SuggestionState>(null);
// debug for https://github.com/vector-im/element-web/issues/26037
const setSuggestionData = (suggestionData: SetStateAction<SuggestionState>): void => {
// setState allows either the data itself or a callback which returns the data
logger.log(
`## 26037 ## wysiwyg useSuggestion hook setting suggestion data to ${
suggestionData === null || suggestionData instanceof Function
? suggestionData
: suggestionData.mappedSuggestion.keyChar + suggestionData.mappedSuggestion.text
}`,
);
setSuggestionData0(suggestionData);
};
const [suggestionData, setSuggestionData] = useState<SuggestionState>(null);
// We create a `selectionchange` handler here because we need to know when the user has moved the cursor,
// we can not depend on input events only

View File

@ -15,7 +15,7 @@ import { TimelineRenderingType } from "../../../../../contexts/RoomContext";
import { useDispatcher } from "../../../../../hooks/useDispatcher";
import { focusComposer } from "./utils";
import { type ComposerFunctions } from "../types";
import { ComposerInsertPayload, ComposerType } from "../../../../../dispatcher/payloads/ComposerInsertPayload";
import { type ComposerInsertPayload, ComposerType } from "../../../../../dispatcher/payloads/ComposerInsertPayload";
import { useComposerContext } from "../ComposerContext";
import { setSelection } from "../utils/selection";
import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext.tsx";

View File

@ -63,10 +63,12 @@ const ManageAccountButton: React.FC<ManageAccountButtonProps> = ({ externalAccou
onClick={null}
element="a"
kind="primary"
data-kind="primary"
target="_blank"
rel="noreferrer noopener"
href={externalAccountManagementUrl}
data-testid="external-account-management-link"
style={{ textDecoration: "none" }}
>
<PopOutIcon className="mx_UserProfileSettings_accountmanageIcon" width="24" height="24" />
{_t("settings|general|oidc_manage_button")}

View File

@ -1,4 +1,5 @@
/*
Copyright 2026 Element Creations Ltd.
Copyright 2024 New Vector Ltd.
Copyright 2021, 2022 The Matrix.org Foundation C.I.C.
@ -18,6 +19,7 @@ import React, {
useLayoutEffect,
useRef,
useState,
useContext,
} from "react";
import { DragDropContext, Draggable, Droppable, type DroppableProvidedProps } from "react-beautiful-dnd";
import classNames from "classnames";
@ -31,6 +33,7 @@ import {
PlusIcon,
ChevronRightIcon,
} from "@vector-im/compound-design-tokens/assets/web/icons";
import { useCreateAutoDisposedViewModel, UserMenu } from "@element-hq/web-shared-components";
import { _t } from "../../../languageHandler";
import { useContextMenu } from "../../structures/ContextMenu";
@ -62,7 +65,6 @@ import { SettingLevel } from "../../../settings/SettingLevel";
import UIStore from "../../../stores/UIStore";
import QuickSettingsButton from "./QuickSettingsButton";
import { useSettingValue } from "../../../hooks/useSettings";
import UserMenu from "../../structures/UserMenu";
import IndicatorScrollbar from "../../structures/IndicatorScrollbar";
import { useDispatcher } from "../../../hooks/useDispatcher";
import defaultDispatcher from "../../../dispatcher/dispatcher";
@ -79,6 +81,9 @@ import { Landmark, LandmarkNavigation } from "../../../accessibility/LandmarkNav
import { KeyboardShortcut } from "../settings/KeyboardShortcut";
import { ModuleApi } from "../../../modules/Api.ts";
import { useModuleSpacePanelItems } from "../../../modules/ExtrasApi.ts";
import { UserMenuViewModel } from "../../../viewmodels/menus/UserMenuViewModel.ts";
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext.tsx";
import { SDKContext } from "../../../contexts/SDKContext.ts";
const useSpaces = (): [Room[], MetaSpace[], Room[], SpaceKey] => {
const invites = useEventEmitterState<Room[]>(SpaceStore.instance, UPDATE_INVITED_SPACES, () => {
@ -384,6 +389,7 @@ const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(
);
const SpacePanel: React.FC = () => {
const client = useMatrixClientContext();
const [dragging, setDragging] = useState(false);
const [isPanelCollapsed, setPanelCollapsed] = useState(true);
const ref = useRef<HTMLDivElement>(null);
@ -391,6 +397,7 @@ const SpacePanel: React.FC = () => {
if (ref.current) UIStore.instance.trackElementDimensions("SpacePanel", ref.current);
return () => UIStore.instance.stopTrackingElementDimensions("SpacePanel");
}, []);
const sdkContext = useContext(SDKContext);
useDispatcher(defaultDispatcher, (payload: ActionPayload) => {
if (payload.action === Action.ToggleSpacePanel) {
@ -400,6 +407,26 @@ const SpacePanel: React.FC = () => {
const newRoomListEnabled = useSettingValue("feature_new_room_list");
const userMenuVm = useCreateAutoDisposedViewModel(
() =>
new UserMenuViewModel(
defaultDispatcher,
client,
isPanelCollapsed,
sdkContext.oidcClientStore.accountManagementEndpoint,
),
);
useDispatcher(defaultDispatcher, (payload) => {
if (payload.action === Action.ToggleUserMenu) {
userMenuVm.setOpen(!userMenuVm.getSnapshot().open);
}
});
useEffect(() => {
userMenuVm.setExpanded(!isPanelCollapsed);
}, [userMenuVm, isPanelCollapsed]);
return (
<RovingTabIndexProvider handleHomeEnd handleUpDown={!dragging}>
{({ onKeyDownHandler, onDragEndHandler }) => (
@ -438,23 +465,22 @@ const SpacePanel: React.FC = () => {
ref={ref}
aria-label={_t("common|spaces")}
>
<UserMenu isPanelCollapsed={isPanelCollapsed}>
<AccessibleButton
className={classNames("mx_SpacePanel_toggleCollapse", {
expanded: !isPanelCollapsed,
})}
onClick={() => setPanelCollapsed(!isPanelCollapsed)}
title={isPanelCollapsed ? _t("action|expand") : _t("action|collapse")}
caption={
<KeyboardShortcut
value={{ ctrlOrCmdKey: true, shiftKey: true, key: "d" }}
className="mx_SpacePanel_Tooltip_KeyboardShortcut"
/>
}
>
<ChevronRightIcon />
</AccessibleButton>
</UserMenu>
<UserMenu vm={userMenuVm} className="mx_UserMenu" />
<AccessibleButton
className={classNames("mx_SpacePanel_toggleCollapse", {
expanded: !isPanelCollapsed,
})}
onClick={() => setPanelCollapsed(!isPanelCollapsed)}
title={isPanelCollapsed ? _t("action|expand") : _t("action|collapse")}
caption={
<KeyboardShortcut
value={{ ctrlOrCmdKey: true, shiftKey: true, key: "d" }}
className="mx_SpacePanel_Tooltip_KeyboardShortcut"
/>
}
>
<ChevronRightIcon />
</AccessibleButton>
<Droppable droppableId="top-level-spaces">
{(provided, snapshot) => (
<InnerSpacePanel

View File

@ -1,4 +1,5 @@
/*
Copyright 2026 Element Creations Ltd.
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
@ -194,5 +195,6 @@ export class SdkContextClass {
public onLoggedOut(): void {
this._UserProfilesStore = undefined;
this._OidcClientStore = undefined;
}
}

View File

@ -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 React, { type JSX } from "react";
import React, { type JSX, useEffect } from "react";
import {
type MatrixEvent,
EventType,
@ -19,6 +19,8 @@ import {
} from "matrix-js-sdk/src/matrix";
import {
EncryptionEventView,
HiddenBodyView,
MKeyVerificationRequestView,
TextualEventView,
useCreateAutoDisposedViewModel,
} from "@element-hq/web-shared-components";
@ -36,18 +38,18 @@ import { WIDGET_LAYOUT_EVENT_TYPE } from "../stores/widgets/WidgetLayoutStore";
import { ALL_RULE_TYPES } from "../mjolnir/BanList";
import { MatrixClientPeg } from "../MatrixClientPeg";
import { useMatrixClientContext } from "../contexts/MatrixClientContext";
import MKeyVerificationRequest from "../components/views/messages/MKeyVerificationRequest";
import { WidgetType } from "../widgets/WidgetType";
import MJitsiWidgetEvent from "../components/views/messages/MJitsiWidgetEvent";
import { hasText } from "../TextForEvent";
import { getMessageModerationState, MessageModerationState } from "../utils/EventUtils";
import HiddenBody from "../components/views/messages/HiddenBody";
import ViewSourceEvent from "../components/views/messages/ViewSourceEvent";
import { shouldDisplayAsBeaconTile } from "../utils/beacon/timeline";
import { type IBodyProps } from "../components/views/messages/IBodyProps";
import { ModuleApi } from "../modules/Api";
import { EncryptionEventViewModel } from "../viewmodels/room/timeline/event-tile/EncryptionEventViewModel";
import { MKeyVerificationRequestViewModel } from "../viewmodels/room/timeline/event-tile/MKeyVerificationRequestViewModel";
import { TextualEventViewModel } from "../viewmodels/room/timeline/event-tile/TextualEventViewModel";
import { HiddenBodyViewModel } from "../viewmodels/room/timeline/event-tile/body/HiddenBodyViewModel";
import { ElementCallEventType } from "../call-types";
// Subset of EventTile's IProps plus some mixins
@ -83,7 +85,7 @@ const LegacyCallEventFactory: Factory<FactoryProps & { callEventGrouper: LegacyC
const CallEventFactory: Factory = (ref, props) => <CallEvent ref={ref} {...props} />;
export const TextualEventFactory: Factory = (ref, props) => {
const vm = new TextualEventViewModel(props);
return <TextualEventView vm={vm} />;
return <TextualEventView vm={vm} className="mx_TextualEvent" />;
};
function EncryptionEventWrappedView({ mxEvent, ref }: IBodyProps): JSX.Element {
const cli = useMatrixClientContext();
@ -94,8 +96,31 @@ function EncryptionEventWrappedView({ mxEvent, ref }: IBodyProps): JSX.Element {
const EncryptionEventFactory: Factory = (ref, props) => {
return <EncryptionEventWrappedView ref={ref} {...props} />;
};
const VerificationReqFactory: Factory = (_ref, props) => <MKeyVerificationRequest {...props} />;
const HiddenEventFactory: Factory = (ref, props) => <HiddenBody ref={ref} {...props} />;
function MKeyVerificationRequestWrappedView({ mxEvent, ref }: IBodyProps): JSX.Element {
const cli = useMatrixClientContext();
if (!cli) {
throw new Error("Attempting to render verification request without a client context!");
}
const vm = useCreateAutoDisposedViewModel(() => new MKeyVerificationRequestViewModel({ mxEvent, cli }));
useEffect(() => {
vm.setEvent(mxEvent);
}, [mxEvent, vm]);
return <MKeyVerificationRequestView vm={vm} ref={ref} className="mx_EventTileBubble mx_cryptoEvent" />;
}
const VerificationReqFactory: Factory = (ref, props) => <MKeyVerificationRequestWrappedView ref={ref} {...props} />;
function HiddenBodyWrappedView({ mxEvent, ref }: IBodyProps): JSX.Element {
const vm = useCreateAutoDisposedViewModel(() => new HiddenBodyViewModel({ mxEvent }));
useEffect(() => {
vm.setEvent(mxEvent);
}, [mxEvent, vm]);
return <HiddenBodyView vm={vm} ref={ref} className="mx_HiddenBody" />;
}
const HiddenEventFactory: Factory = (ref, props) => <HiddenBodyWrappedView ref={ref} {...props} />;
// These factories are exported for reference comparison against pickFactory()
export const JitsiEventFactory: Factory = (ref, props) => <MJitsiWidgetEvent ref={ref} {...props} />;

View File

@ -15,8 +15,7 @@
"room_name": "Místnost %(name)s",
"room_status_bar": "Stavový řádek místnosti",
"seek_bar_label": "Panel posunu zvuku",
"unread_messages": "Nepřečtené zprávy.",
"user_menu": "Uživatelská nabídka"
"unread_messages": "Nepřečtené zprávy."
},
"a11y_jump_first_unread_room": "Přejít na první nepřečtenou místnost.",
"action": {
@ -341,7 +340,6 @@
"sign_in_instead_prompt": "Namísto toho se přihlásit",
"sign_in_or_register": "Přihlásit nebo vytvořit nový účet",
"sign_in_or_register_description": "Pro pokračování se přihlaste stávajícím účtem, nebo si vytvořte nový.",
"sign_in_prompt": "Máte již účet? <a>Přihlásit se</a>",
"sign_in_with_sso": "Přihlásit se přes jednotné přihlašování",
"signing_in": "Přihlašování…",
"soft_logout": {
@ -3573,7 +3571,6 @@
"created_rule_rooms": "%(senderName)s vytvořil pravidlo blokující místnosti odpovídající %(glob)s z důvodu %(reason)s",
"created_rule_servers": "%(senderName)s vytvořil pravidlo blokující servery odpovídající %(glob)s z důvodu %(reason)s",
"created_rule_users": "%(senderName)s vytvořil(a) pravidlo blokující uživatele odpovídající %(glob)s z důvodu %(reason)s",
"message_hidden": "Tohoto uživatele ignorujete, takže jsou jeho zprávy skryté. <a>Přesto zobrazit.</a>",
"removed_rule": "%(senderName)s odstranil blokující pravidlo %(glob)s",
"removed_rule_rooms": "%(senderName)s odstranil pravidlo blokující místnosti odpovídající %(glob)s",
"removed_rule_servers": "%(senderName)s odstranil pravidlo blokující servery odpovídající %(glob)s",
@ -3878,12 +3875,6 @@
"verify_button": "Ověřit uživatele",
"verify_explainer": "Pro lepší bezpečnost, ověřte uživatele zkontrolováním jednorázového kódu na vašich zařízeních."
},
"user_menu": {
"link_new_device": "Připojit nové zařízení",
"settings": "Všechna nastavení",
"switch_theme_dark": "Přepnout do tmavého režimu",
"switch_theme_light": "Přepnout do světlého režimu"
},
"voip": {
"already_in_call": "Již máte hovor",
"already_in_call_person": "S touto osobou již telefonujete.",

View File

@ -17,8 +17,7 @@
"room_name": "Ystafell %(matere)s",
"room_status_bar": "Bar statws ystafell",
"seek_bar_label": "Bar chwilio sain",
"unread_messages": "Negeseuon heb eu darllen.",
"user_menu": "Dewislen defnyddiwr"
"unread_messages": "Negeseuon heb eu darllen."
},
"a11y_jump_first_unread_room": "Symud i'r ystafell gyntaf heb ei darllen.",
"action": {
@ -343,7 +342,6 @@
"sign_in_instead_prompt": "Oes gennych chi gyfrif yn barod? <a>Mewngofnodwch yma</a>",
"sign_in_or_register": "Mewngofnodi neu Creu Cyfrif",
"sign_in_or_register_description": "Defnyddiwch eich cyfrif neu crëwch un newydd i barhau.",
"sign_in_prompt": "Oes gennych chi gyfrif? <a>Mewngofnodwch</a>",
"sign_in_with_sso": "Mewngofnodwch gyda mewngofnod sengl",
"signing_in": "Wrthi'n mewngofnodi…",
"soft_logout": {
@ -3530,7 +3528,6 @@
"created_rule_rooms": "Creodd %(senderName)s reol yn gwahardd ystafelloedd sy'n cyfateb i %(glob)s am %(reason)s",
"created_rule_servers": "Creodd %(senderName)s reol yn gwahardd gweinyddwyr sy'n cyfateb i %(glob)s am %(reason)s",
"created_rule_users": "Creodd %(senderName)s reol yn gwahardd defnyddwyr rhag paru %(glob)s am %(reason)s",
"message_hidden": "Rydych chi wedi anwybyddu'r defnyddiwr hwn, felly mae eu neges wedi'i chuddio. <a>Dangos beth bynnag.</a>",
"removed_rule": "Mae %(senderName)s wedi dileu rheol gwahardd sy'n cyfateb i %(glob)s",
"removed_rule_rooms": "Mae %(senderName)s wedi dileu'r rheol sy'n gwahardd ystafelloedd sy'n cyfateb i %(glob)s",
"removed_rule_servers": "Mae %(senderName)s wedi dileu'r rheol sy'n gwahardd gweinyddwyr sy'n cyfateb i %(glob)s",
@ -3841,12 +3838,6 @@
"verify_button": "Dilysu Defnyddiwr",
"verify_explainer": "Ar gyfer diogelwch ychwanegol, gwiriwch y defnyddiwr hwn trwy wirio cod un-amser ar eich dwy ddyfais."
},
"user_menu": {
"link_new_device": "Cysylltu'r ddyfais newydd",
"settings": "Pob gosodiad",
"switch_theme_dark": "Newid i'r modd tywyll",
"switch_theme_light": "Newid i'r modd golau"
},
"voip": {
"already_in_call": "Eisoes mewn galwad",
"already_in_call_person": "Rydych chi eisoes mewn galwad gyda'r person hwn.",

View File

@ -15,8 +15,7 @@
"room_name": "Rum %(name)s",
"room_status_bar": "Statusbjælke for rum",
"seek_bar_label": "Progressionsmarkør for lydafspiller",
"unread_messages": "Ulæste beskeder.",
"user_menu": "Brugermenu"
"unread_messages": "Ulæste beskeder."
},
"a11y_jump_first_unread_room": "Gå til første ulæste rum.",
"action": {
@ -321,7 +320,6 @@
"sign_in_instead_prompt": "Har du allerede en konto? <a>Log ind her</a>",
"sign_in_or_register": "Log ind eller Opret bruger",
"sign_in_or_register_description": "Brug din konto eller opret en ny for at fortsætte.",
"sign_in_prompt": "Har du en konto? <a>Log ind</a>",
"sign_in_with_sso": "Log ind med single sign-on",
"signing_in": "Logger ind…",
"soft_logout": {
@ -3091,7 +3089,6 @@
"created_rule_rooms": "%(senderName)s oprettede en regel, som spærrer for rum, der matcher %(glob)s af %(reason)s",
"created_rule_servers": "%(senderName)s oprettede en regel, som spærrer servere, der matcher %(glob)s af %(reason)s",
"created_rule_users": "%(senderName)s oprettede en regel, som spærrer for brugere, der matcher %(glob)s af %(reason)s",
"message_hidden": "Du har ignoreret denne bruger, så deres besked er skjult. <a>Vis alligevel.</a>",
"removed_rule": "%(senderName)s fjernede en spærringsregel der matcher %(glob)s",
"removed_rule_rooms": "%(senderName)s fjernede den regel der spærrede brugere der matcher %(glob)s",
"removed_rule_servers": "%(senderName)s fjernede den regel der spærrede servere som matcher %(glob)s",
@ -3385,10 +3382,6 @@
"verify_button": "Verificér bruger",
"verify_explainer": "For ekstra sikkerhed skal du verificere denne bruger ved at kontrollere en engangskode på begge jeres enheder."
},
"user_menu": {
"link_new_device": "Forbind ny enhed",
"settings": "Alle indstillinger"
},
"voip": {
"already_in_call": "Allerede i et opkald",
"already_in_call_person": "Du har allerede i et opkald med denne person.",

View File

@ -15,8 +15,7 @@
"room_name": "Chat %(name)s",
"room_status_bar": "Chat-Statusleiste",
"seek_bar_label": "Audio-Suchleiste",
"unread_messages": "Ungelesene Nachrichten.",
"user_menu": "Benutzermenü"
"unread_messages": "Ungelesene Nachrichten."
},
"a11y_jump_first_unread_room": "Springe zum ersten ungelesenen Chat",
"action": {
@ -341,7 +340,6 @@
"sign_in_instead_prompt": "Konto schon vorhanden? <a>Hier anmelden</a>",
"sign_in_or_register": "Anmelden oder Konto erstellen",
"sign_in_or_register_description": "Benutze dein Konto oder erstelle ein neues, um fortzufahren.",
"sign_in_prompt": "Du hast bereits ein Konto? <a>Melde dich an</a>",
"sign_in_with_sso": "Einmalanmeldung nutzen",
"signing_in": "Melde an …",
"soft_logout": {
@ -3529,7 +3527,6 @@
"created_rule_rooms": "%(senderName)s hat eine Sperr-Regel für Chats erstellt, die %(glob)s aufgrund von %(reason)s entspricht",
"created_rule_servers": "%(senderName)s erstellte eine Ausschlussregel für Server, die aufgrund von %(reason)s %(glob)s entsprechen",
"created_rule_users": "%(senderName)s hat eine Ausschlussregel für Nutzer erstellt, die aufgrund %(reason)s %(glob)s entsprechen",
"message_hidden": "Du ignorierst diesen Nutzer, so dass dessen Nachrichten verborgen werden. <a>Trotzdem anzeigen.</a>",
"removed_rule": "%(senderName)s entfernte die Ausschlussregel, die %(glob)s entspricht",
"removed_rule_rooms": "%(senderName)s hat die Sperr-Regel für Chats entfernt, die %(glob)s entsprechen",
"removed_rule_servers": "%(senderName)s entfernte die Ausschlussregel für Server, die %(glob)s entsprechen",
@ -3834,12 +3831,6 @@
"verify_button": "Nutzer verifizieren",
"verify_explainer": "Für zusätzliche Sicherheit, verifiziere diesen Nutzer, durch Vergleichen eines Einmal-Codes auf euren beiden Geräten."
},
"user_menu": {
"link_new_device": "Neues Gerät verknüpfen",
"settings": "Alle Einstellungen",
"switch_theme_dark": "Zum dunklen Thema wechseln",
"switch_theme_light": "Zum hellen Thema wechseln"
},
"voip": {
"already_in_call": "Schon im Anruf",
"already_in_call_person": "Du bist schon in einem Anruf mit dieser Person.",

View File

@ -11,8 +11,7 @@
},
"recent_rooms": "Πρόσφατες αίθουσες",
"room_name": "Αίθουσα %(name)s",
"unread_messages": "Μη αναγνωσμένα μηνύματα.",
"user_menu": "Μενού χρήστη"
"unread_messages": "Μη αναγνωσμένα μηνύματα."
},
"a11y_jump_first_unread_room": "Μετάβαση στην πρώτη μη αναγνωσμένη αίθουσα.",
"action": {
@ -107,7 +106,7 @@
"show_advanced": "Εμφάνιση προχωρημένων",
"show_all": "Εμφάνιση όλων",
"sign_in": "Σύνδεση",
"sign_out": "Αποσύνδεση",
"sign_out": "Κατάργηση αυτής της συσκευής",
"skip": "Παράβλεψη",
"start": "Έναρξη",
"start_chat": "Έναρξη συνομιλίας",
@ -300,7 +299,6 @@
"sign_in_instead_prompt": "Έχετε ήδη λογαριασμό; <a>Συνδεθείτε εδώ</a>",
"sign_in_or_register": "Συνδεθείτε ή Δημιουργήστε Λογαριασμό",
"sign_in_or_register_description": "Χρησιμοποιήστε τον λογαριασμό σας ή δημιουργήστε νέο για να συνεχίσετε.",
"sign_in_prompt": "Έχετε λογαριασμό; <a>Συνδεθείτε</a>",
"sign_in_with_sso": "Συνδεθείτε με απλή σύνδεση",
"signing_in": "Σύνδεση...",
"soft_logout": {
@ -555,12 +553,12 @@
"format_underline": "Υπογράμμιση",
"format_unordered_list": "Λίστα με κουκκκίδες",
"no_perms_notice": "Δεν έχετε δικαιώματα για να δημοσιεύσετε σε αυτό το δωμάτιο",
"placeholder": "Στείλτε ένα μήνυμα…",
"placeholder_encrypted": "Αποστολή κρυπτογραφημένου μηνύματος…",
"placeholder_reply": "Στείλτε μια απάντηση…",
"placeholder_reply_encrypted": "Αποστολή κρυπτογραφημένης απάντησης…",
"placeholder_thread": "Απάντηση στο νήμα…",
"placeholder_thread_encrypted": "Απάντηση στο κρυπτογραφημένο νήμα…",
"placeholder": "Στείλτε ένα μη κρυπτογραφημένο μήνυμα…",
"placeholder_encrypted": "Στείλτε ένα μήνυμα…",
"placeholder_reply": "Αποστολή μη κρυπτογραφημένης απάντησης…",
"placeholder_reply_encrypted": "Αποστολή απάντησης…",
"placeholder_thread": "Απάντηση σε μη κρυπτογραφημένο νήμα…",
"placeholder_thread_encrypted": "Απάντηση στο νήμα…",
"poll_button": "Ψηφοφορία",
"poll_button_no_perms_description": "Δεν έχετε άδεια να ξεκινήσετε ψηφοφορίες σε αυτήν την αίθουσα.",
"poll_button_no_perms_title": "Απαιτείται Άδεια",
@ -2030,7 +2028,7 @@
"remove_msisdn_prompt": "Κατάργηση %(phone)s;",
"spell_check_locale_placeholder": "Επιλογή τοπικών ρυθμίσεων"
},
"inline_url_previews_default": "Ενεργοποιήστε τις ενσωματωμένες προεπισκοπήσεις URL από προεπιλογή",
"inline_url_previews_default": "Ενεργοποίηση προεπισκοπήσεων",
"insert_trailing_colon_mentions": "Εισαγάγετε άνω και κάτω τελεία μετά την αναφορά του χρήστη στην αρχή ενός μηνύματος",
"jump_to_bottom_on_send": "Μεταβείτε στο τέλος του χρονολογίου όταν στέλνετε ένα μήνυμα",
"key_backup": {
@ -2212,7 +2210,7 @@
"other": "Αποσύνδεση συσκευών"
},
"confirm_sign_out_sso": {
"one": "Επιβεβαιώστε ότι αποσυνδέεστε από αυτήν τη συσκευή χρησιμοποιώντας Single Sign On για να αποδείξετε την ταυτότητά σας.",
"one": "Επιβεβαιώστε την αποσύνδεση από αυτήν τη συσκευή χρησιμοποιώντας Single Sign On για να αποδείξετε την ταυτότητά σας.",
"other": "Επιβεβαιώστε την αποσύνδεση από αυτές τις συσκευές χρησιμοποιώντας Single Sign On για να αποδείξετε την ταυτότητά σας."
},
"current_session": "Τρέχουσα συνεδρία",
@ -2708,7 +2706,7 @@
"unknown": "Ο %(senderDisplayName)s άλλαξε την πρόσβαση επισκεπτών σε %(rule)s"
},
"m.room.history_visibility": {
"invited": "Ο %(senderName)s έκανε το μελλοντικό ιστορικό του δωματίου δημόσιο όλα τα μέλη, από τη στιγμή που προσκλήθηκαν.",
"invited": "Ο/Η %(senderName)s έκανε το μελλοντικό ιστορικό της αίθουσας ορατό σε όλα τα μέλη της αίθουσας, από τη στιγμή που προσκλήθηκαν.",
"joined": "Ο %(senderName)s έκανε το μελλοντικό ιστορικό του δωματίου δημόσιο όλα τα μέλη, από τη στιγμή που συνδέθηκαν.",
"shared": "Ο %(senderName)s έκανε το μελλοντικό ιστορικό του δωματίου δημόσιο όλα τα μέλη.",
"unknown": "Ο %(senderName)s έκανε το μελλοντικό ιστορικό του δωματίου δημόσιο άγνωστο (%(visibility)s).",
@ -2725,8 +2723,8 @@
"m.room.member": {
"accepted_3pid_invite": "%(targetName)s αποδέχθηκε την πρόσκληση για %(displayName)s",
"accepted_invite": "%(targetName)s αποδέχθηκε μια πρόσκληση",
"ban": "Ο %(senderName)s απέκλεισε τον/την %(targetName)s",
"ban_reason": "Ο %(senderName)s απέκλεισε τον/την %(targetName)s: %(reason)s",
"ban": "Ο/Η %(senderName)s απέκλεισε έναν χρήστη",
"ban_reason": "Ο/Η %(senderName)s απέκλεισε έναν χρήστη: %(reason)s",
"change_avatar": "Ο %(senderName)s άλλαξε τη φωτογραφία του προφίλ του",
"change_name": "Ο/η %(oldDisplayName)s άλλαξε το εμφανιζόμενο όνομα σε %(displayName)s",
"change_name_avatar": "Ο χρήστης %(oldDisplayName)s άλλαξε το εμφανιζόμενο όνομα και την εικόνα προφίλ του",
@ -2798,7 +2796,6 @@
"created_rule_rooms": "%(senderName)s δημιούργησε έναν κανόνα που απαγορεύει την αντιστοίχιση δωματίων %(glob)s για %(reason)s",
"created_rule_servers": "%(senderName)s δημιούργησε έναν κανόνα που απαγορεύει την αντιστοίχιση διακομιστών %(glob)s για %(reason)s",
"created_rule_users": "%(senderName)s δημιούργησε έναν κανόνα που απαγορεύει την αντιστοίχιση χρηστών %(glob)s για %(reason)s",
"message_hidden": "Έχετε αγνοήσει αυτόν τον χρήστη, επομένως τα μηνύματα του είναι κρυφά. <a>Εμφάνιση ούτως ή άλλως.</a>",
"removed_rule": "%(senderName)s αφαίρεσε μια απαγόρευση αντιστοίχισης κανόνων %(glob)s",
"removed_rule_rooms": "%(senderName)s αφαίρεσε τον κανόνα που απαγορεύει την αντιστοίχιση δωματίων %(glob)s",
"removed_rule_servers": "%(senderName)s αφαίρεσε τον κανόνα που απαγορεύει την αντιστοίχιση διακομιστών %(glob)s",
@ -3075,11 +3072,6 @@
"verify_button": "Επαλήθευση Χρήστη",
"verify_explainer": "Για επιπλέον ασφάλεια, επαληθεύστε αυτόν τον χρήστη ελέγχοντας έναν κωδικό μίας χρήσης και στις δύο συσκευές σας."
},
"user_menu": {
"settings": "Όλες οι ρυθμίσεις",
"switch_theme_dark": "Αλλαγή σε σκοτεινό",
"switch_theme_light": "Αλλαγή σε φωτεινό"
},
"voip": {
"already_in_call": "Ήδη σε κλήση",
"already_in_call_person": "Είστε ήδη σε κλήση με αυτόν τον χρήστη.",

View File

@ -15,8 +15,7 @@
"room_name": "Room %(name)s",
"room_status_bar": "Room status bar",
"seek_bar_label": "Audio seek bar",
"unread_messages": "Unread messages.",
"user_menu": "User menu"
"unread_messages": "Unread messages."
},
"a11y_jump_first_unread_room": "Jump to first unread room.",
"action": {
@ -341,7 +340,6 @@
"sign_in_instead_prompt": "Already have an account? <a>Sign in here</a>",
"sign_in_or_register": "Sign In or Create Account",
"sign_in_or_register_description": "Use your account or create a new one to continue.",
"sign_in_prompt": "Got an account? <a>Sign in</a>",
"sign_in_with_sso": "Sign in with single sign-on",
"signing_in": "Signing In…",
"soft_logout": {
@ -3590,7 +3588,6 @@
"created_rule_rooms": "%(senderName)s created a rule banning rooms matching %(glob)s for %(reason)s",
"created_rule_servers": "%(senderName)s created a rule banning servers matching %(glob)s for %(reason)s",
"created_rule_users": "%(senderName)s created a rule banning users matching %(glob)s for %(reason)s",
"message_hidden": "You have ignored this user, so their message is hidden. <a>Show anyways.</a>",
"removed_rule": "%(senderName)s removed a ban rule matching %(glob)s",
"removed_rule_rooms": "%(senderName)s removed the rule banning rooms matching %(glob)s",
"removed_rule_servers": "%(senderName)s removed the rule banning servers matching %(glob)s",
@ -3895,12 +3892,6 @@
"verify_button": "Verify User",
"verify_explainer": "For extra security, verify this user by checking a one-time code on both of your devices."
},
"user_menu": {
"link_new_device": "Link new device",
"settings": "All settings",
"switch_theme_dark": "Switch to dark mode",
"switch_theme_light": "Switch to light mode"
},
"voip": {
"already_in_call": "Already in call",
"already_in_call_person": "You're already in a call with this person.",

View File

@ -10,8 +10,7 @@
"one": "1 nelegita mencio."
},
"room_name": "Ĉambro %(name)s",
"unread_messages": "Nelegitaj mesaĝoj.",
"user_menu": "Menuo de uzanto"
"unread_messages": "Nelegitaj mesaĝoj."
},
"a11y_jump_first_unread_room": "Salti al unua nelegita ĉambro.",
"action": {
@ -248,7 +247,6 @@
"sign_in_instead_prompt": "Aliĝu anstataŭe",
"sign_in_or_register": "Salutu aŭ kreu konton",
"sign_in_or_register_description": "Por daŭrigi, uzu vian konton aŭ kreu novan.",
"sign_in_prompt": "Ĉu vi havas konton? <a>Salutu</a>",
"sign_in_with_sso": "Saluti per ununura saluto",
"soft_logout": {
"clear_data_button": "Vakigi ĉiujn datumojn",
@ -2210,7 +2208,6 @@
"created_rule_rooms": "%(senderName)s kreis regulon forbarantan ĉambrojn, kiuj akordas kun %(glob)s, pro %(reason)s",
"created_rule_servers": "%(senderName)s kreis regulon forbarantan servilojn, kiuj akordas kun %(glob)s, pro %(reason)s",
"created_rule_users": "%(senderName)s kreis regulon forbarantan uzantojn, kiuj akordas kun %(glob)s, pro %(reason)s",
"message_hidden": "Vi malatentis ĉi tiun uzanton, ĝia mesaĝo estas do kaŝita. <a>Tamen montri.</a>",
"removed_rule": "%(senderName)s forigis forbaran regulon, kiu akordas kun %(glob)s",
"removed_rule_rooms": "%(senderName)s forigis la regulon forbarantan ĉambrojn, kiuj akordas kun %(glob)s",
"removed_rule_servers": "%(senderName)s forigis la regulon forbarantan servilojn, kiuj akordas kun %(glob)s",
@ -2409,11 +2406,6 @@
"verify_button": "Kontroli uzanton",
"verify_explainer": "Por plia sekureco, kontrolu ĉi tiun uzanton per unufoja kodo aperonta sur ambaŭ el viaj aparatoj."
},
"user_menu": {
"settings": "Ĉiuj agordoj",
"switch_theme_dark": "Ŝalti malhelan reĝimon",
"switch_theme_light": "Ŝalti helan reĝimon"
},
"voip": {
"already_in_call": "Jam vokanta",
"already_in_call_person": "Vi jam vokas ĉi tiun personon.",

View File

@ -15,8 +15,7 @@
"room_name": "Sala %(name)s",
"room_status_bar": "Barra de estado de la sala",
"seek_bar_label": "Barra de búsqueda de audio",
"unread_messages": "Mensajes sin leer.",
"user_menu": "Menú del Usuario"
"unread_messages": "Mensajes sin leer."
},
"a11y_jump_first_unread_room": "Saltar a la primera sala sin leer.",
"action": {
@ -341,7 +340,6 @@
"sign_in_instead_prompt": "¿Ya tienes una cuenta? <a>Inicia sesión aquí</a>",
"sign_in_or_register": "Iniciar sesión o Crear una cuenta",
"sign_in_or_register_description": "Entra con tu cuenta si ya tienes una o crea una nueva para continuar.",
"sign_in_prompt": "¿Ya tienes una cuenta? <a>Iniciar sesión</a>",
"sign_in_with_sso": "Ingresar con un Registro Único",
"signing_in": "Iniciando sesión…",
"soft_logout": {
@ -2886,7 +2884,6 @@
"created_rule_rooms": "%(senderName)s creó una regla bloqueando a salas que coinciden con %(glob)s por %(reason)s",
"created_rule_servers": "%(senderName)s creó una regla bloqueando a servidores que coinciden con %(glob)s por %(reason)s",
"created_rule_users": "%(senderName)s creó una regla bloqueando a usuarios que coinciden con %(glob)s por %(reason)s",
"message_hidden": "Ha ignorado a esta cuenta, así que su mensaje está oculto. <a>Ver de todos modos.</a>",
"removed_rule": "%(senderName)s eliminó una regla correspondiente a %(glob)s",
"removed_rule_rooms": "%(senderName)s eliminó la regla que bloquea a salas que coinciden con %(glob)s",
"removed_rule_servers": "%(senderName)s eliminó la regla que bloquea a servidores que coinciden con %(glob)s",
@ -3161,12 +3158,6 @@
"verify_button": "Verificar usuario",
"verify_explainer": "Para mayor seguridad, verifica a este usuario comprobando un código temporal vez dos de tus dispositivos."
},
"user_menu": {
"link_new_device": "Vincular un dispositivo nuevo",
"settings": "Ajustes",
"switch_theme_dark": "Cambiar al tema oscuro",
"switch_theme_light": "Cambiar al tema claro"
},
"voip": {
"already_in_call": "Ya en una llamada",
"already_in_call_person": "Ya estás en una llamada con esta persona.",

View File

@ -15,8 +15,7 @@
"room_name": "Jututuba %(name)s",
"room_status_bar": "Jututoa olekuriba",
"seek_bar_label": "Heli kerimisriba",
"unread_messages": "Lugemata sõnumid.",
"user_menu": "Kasutajamenüü"
"unread_messages": "Lugemata sõnumid."
},
"a11y_jump_first_unread_room": "Siirdu esimesse lugemata jututuppa.",
"action": {
@ -42,7 +41,7 @@
"copy_link": "Kopeeri link",
"create": "Loo",
"create_a_room": "Loo jututuba",
"create_account": "Loo konto",
"create_account": "Loo kasutajakonto",
"decline": "Keeldu",
"decline_and_block": "Keeldu ja blokeeri",
"decline_invite": "Lükka kutse tagasi",
@ -115,7 +114,7 @@
"show_advanced": "Näita lisaseadistusi",
"show_all": "Näita kõiki",
"sign_in": "Logi sisse",
"sign_out": "Logi välja",
"sign_out": "Eemalda see seade",
"skip": "Jäta vahele",
"start": "Alusta",
"start_chat": "Alusta vestlust",
@ -219,7 +218,7 @@
"incorrect_password": "Vale salasõna",
"log_in_new_account": "<a>Logi sisse</a> oma uuele kasutajakontole.",
"logout_dialog": {
"description": "Kas sa oled kindel, et soovid välja logida?",
"description": "Kas sa oled kindel, et soovid selle seadme eemaldada?",
"megolm_export": "Ekspordi võtmed käsitsi",
"setup_key_backup_title": "Sa kaotad ligipääsu oma krüptitud sõnumitele",
"setup_secure_backup_description_1": "Krüptitud sõnumid kasutavad läbivat krüptimist. Ainult sinul ja saaja(te)l on võtmed selliste sõnumite lugemiseks.",
@ -297,7 +296,7 @@
"reset_password": {
"confirm_new_password": "Kinnita oma uus salasõna",
"devices_logout_success": "Sa oled kõikidest seadmetest välja logitud ning enam ei saa tõuketeavitusi. Nende taaskuvamiseks logi sisse igas oma soovitud seadmetes.",
"other_devices_logout_warning_1": "Kõikide sinu seadmete võrgust välja logimine kustutab ka nendes salvestatud krüptovõtmed ja sellega muutuvad ka krüptitud vestlused loetamatuteks.",
"other_devices_logout_warning_1": "Kõikide sinu seadmete eemaldamine kustutab ka nendes salvestatud krüptovõtmed ja sellega muutuvad ka krüptitud vestlused loetamatuteks.",
"other_devices_logout_warning_2": "Kui sa soovid ligipääsu varasematele krüptitud vestlustele, palun seadista võtmete varundus või enne jätkamist ekspordi mõnest seadmest krüptovõtmed.",
"password_not_entered": "Palun sisesta uus salasõna.",
"passwords_mismatch": "Uued salasõnad peavad omavahel klappima.",
@ -328,8 +327,8 @@
"server_picker_title": "Logi sisse oma koduserverisse",
"server_picker_title_default": "Serveri seadistused",
"server_picker_title_registration": "Sinu kasutajakontot teenindab",
"session_logged_out_description": "Turvalisusega seotud põhjustel on see sessioon välja logitud. Palun logi uuesti sisse.",
"session_logged_out_title": "Välja logitud",
"session_logged_out_description": "Turvalisusega seotud põhjustel on see sessioon eemaldatud. Palun logi uuesti sisse.",
"session_logged_out_title": "Sessioon on eemaldatud",
"set_email": {
"description": "See võimaldab sul luua uue salasõna ning saada teavitusi.",
"verification_pending_description": "Palun vaata oma e-kirju ning klõpsi meie saadetud kirjas leiduvat linki. Kui see on tehtud, siis vajuta Jätka-nuppu.",
@ -341,7 +340,6 @@
"sign_in_instead_prompt": "Sul juba on kasutajakonto olemas? <a>Siis logi siin sisse</a>",
"sign_in_or_register": "Logi sisse või loo uus konto",
"sign_in_or_register_description": "Jätkamaks kasuta oma kontot või loo uus konto.",
"sign_in_prompt": "Sul on kasutajakonto olemas? <a>Siis logi sisse</a>",
"sign_in_with_sso": "Logi sisse ühekordse sisselogimise abil",
"signing_in": "Login sisse…",
"soft_logout": {
@ -476,6 +474,7 @@
"description": "Kirjeldus",
"deselect_all": "Eemalda kõik valikud",
"device": "Seade",
"disabled_by_homeserver": "Koduserveri poolt keelatud",
"edited": "muudetud",
"email_address": "E-posti aadress",
"emoji": "Emotikon",
@ -666,7 +665,7 @@
"join_rule_public_label": "Kõik saavad seda jututuba leida ja temaga liituda.",
"join_rule_public_parent_space_label": "Mitte ainult <SpaceName/> kogukonna liikmed, vaid kõik saavad seda jututuba leida ja võivad temaga liituda.",
"join_rule_restricted": "Nähtav kogukonnakeskuse liikmetele",
"join_rule_restricted_label": "Kõik <SpaceName/> kogukonna liikmed saavad seda jututuba leida ning võivad temaga liituda.",
"join_rule_restricted_label": "Kõik <SpaceName/> kogukonna liikmed liituda.",
"name_validation_required": "Palun sisesta jututoa nimi",
"room_visibility_label": "Jututoa nähtavus",
"state_encrypted_warning": "Sellega võtad kasutusele olekusündmuste krüptimise, mis võimaldab serverist peita metateabe, nagu jututubade nimed ja teemad. See metateave pole nähtav ka jututoaga hiljem liitujatele ning neile, kelle kliendid ei toeta MSC4362 spetsifikatsiooni.",
@ -810,6 +809,8 @@
"event_id": "Sündmuse tunnus: %(eventId)s",
"event_sent": "Sündmus on saadetud!",
"event_type": "Sündmuse tüüp",
"expired": "Aegunud",
"expires_in": "Aegub",
"explore_account_data": "Uuri konto andmeid",
"explore_room_account_data": "Uuri kasutajakonto olekut",
"explore_room_state": "Uuri jututoa olekut",
@ -822,7 +823,7 @@
"invalid_json": "See ei tundu olema korrektse json-andmestikuna.",
"level": "Tase",
"low_bandwidth_mode": "Vähese ribalaiusega režiim",
"low_bandwidth_mode_description": "Eeldab, et koduserver toetab sellist funktsionaalsust.",
"low_bandwidth_mode_description": "Lülitab välja krüpteerimise, olekuteated, tunnuspildid, lugemisteatised ja kirjutamisteatised",
"main_timeline": "Peamine ajajoon",
"manual_device_verification": "Seadme käsitsi verifitseerimine",
"no_receipt_found": "Lugemisteatist ei leidu",
@ -957,7 +958,7 @@
"event_shield_reason_unverified_identity": "Krüptitud verifitseerimata kasutaja poolt.",
"export_unsupported": "Sinu brauser ei toeta vajalikke krüptoteeke",
"forgot_recovery_key": "Kas unustasid taastevõtme?",
"identity_needs_reset_description": "Sõnumite ajaloole juurdepääsu tagamiseks pead oma krüptograafilise võrguidentiteedi lähtestama.",
"identity_needs_reset_description": "Sõnumite ajaloole juurdepääsu tagamiseks pead oma krüptograafilise võrguidentiteedi lähtestama",
"import_invalid_keyfile": "See ei ole sobilik võtmefail %(brand)s'i jaoks",
"import_invalid_passphrase": "Autentimine ebaõnnestus: kas salasõna pole õige?",
"key_storage_out_of_sync": "Sinu krüptovõtmete hoidla pole sünkroonis.",
@ -985,7 +986,7 @@
"title": "Taastemeetod on eemaldatud",
"warning": "Kui sa ei ole ise taastamise meetodeid eemaldanud, siis võib olla tegemist ründega sinu konto vastu. Palun vaheta koheselt oma kasutajakonto salasõna ning määra seadistustes uus taastemeetod."
},
"set_up_recovery": "Seadista krüptovõtmete taastamine",
"set_up_recovery": "Varunda oma vestlused",
"set_up_recovery_toast_description": "Kui peaksid kaotama ligipääsu oma seadmetele, siis siinloodava taastevõtmega saad taastada ligipääsu oma krüptitud sõnumitele.",
"set_up_toast_title": "Võta kasutusele turvaline varundus",
"setup_secure_backup": {
@ -1102,7 +1103,7 @@
"verification_requested_toast_title": "Verifitseerimistaotlus on saadetud",
"verified_identity_changed": "Kasutaja %(displayName)s (<b>%(userId)s</b>) verifitseeritud võrguidentiteet on muutunud. <a>Lisateave</a>",
"verified_identity_changed_no_displayname": "Kasutaja <b>%(userId)s</b> verifitseeritud identiteet on muutunud. <a>Lisateave</a>",
"verify_toast_description": "Teised kasutajad ei pruugi seda usaldada",
"verify_toast_description": "Alates 2026. aasta oktoobri lõpust ei saa verifitseerimata seadmed enam sõnumeid saata ega vastu võtta. <a>Lisateave</a>",
"verify_toast_title": "Verifitseeri see seade",
"withdraw_verification_action": "Eemalda verifitseerimine"
},
@ -1128,11 +1129,11 @@
"non_urgent_echo_failure_toast": "Sinu koduserver ei vasta mõnedele <a>päringutele</a>.",
"resource_limits": "See koduserver ületanud ühe oma ressursipiirangutest.",
"session_restore": {
"clear_storage_button": "Tühjenda andmeruum ja logi välja",
"clear_storage_button": "Eemalda see seade",
"clear_storage_description": "Logi välja ja eemalda krüptimisvõtmed?",
"description_1": "Meil tekkis eelmise sessiooni taastamisel viga.",
"description_2": "Kui sa varem oled kasutanud uuemat %(brand)s'i versiooni, siis sinu pragune sessioon ei pruugi olla sellega ühilduv. Sulge see aken ja jätka selle uuema versiooni kasutamist.",
"description_3": "Brauseri andmeruumi tühjendamine võib selle vea lahendada, kui samas logid sa ka välja ning kogu krüptitud vestlusajalugu muutub loetamatuks.",
"description_3": "Brauseri andmeruumi tühjendamine võib selle vea lahendada, kui samas eemaldad ka selle seadme ning kogu krüptitud vestlusajalugu muutub loetamatuks.",
"title": "Sessiooni taastamine ei õnnestunud"
},
"something_went_wrong": "Midagi läks nüüd valesti!",
@ -1350,6 +1351,9 @@
"impossible_dialog_title": "Lõimingute kasutamine ei ole lubatud"
},
"invite": {
"confirm_unknown_users": {
"start_chat_title_multiple_users": "Kas alustad vestlust nende uute kontaktidega?"
},
"email_caption": "Saada kutse e-kirjaga",
"email_limit_one": "Kutseid saad e-posti teel saata vaid ükshaaval",
"email_use_default_is": "E-posti teel kutse saatmiseks kasuta isikutuvastusserverit. Võid kasutada <default>vaikimisi serverit (%(defaultIdentityServerName)s)</default> või määrata muud serverid <settings>seadistustes</settings>.",
@ -1513,6 +1517,9 @@
"experimental_section": "Varased arendusjärgud",
"extended_profiles_msc_support": "See eeldab, et koduserver toetab MSC4133 spetsifikatsiooni",
"feature_disable_call_per_sender_encryption": "Lülita Element Call'i kasutamisel krüptimine kasutajakohaselt välja",
"feature_user_status": {
"display_name": "Kasutaja olek"
},
"feature_wysiwyg_composer_description": "Sõnumite kirjutamisel kasuta Markdown'i asemel täisfunktsionaalset küljendust.",
"group_calls": "Uus rühmakõnede lahendus",
"group_developer": "Arendajad",
@ -1552,7 +1559,7 @@
"report_to_moderators_description": "Kui jututoas on modereerimine kasutusel, siis nupust „Teata sisust“ avaneva vormi abil saad jututoa reegleid rikkuvast sisust teatada moderaatoritele.",
"sliding_sync": "Järkjärgulise sünkroniseerimise režiim",
"sliding_sync_description": "Aktiivselt arendamisel ega ole võimalik välja lülitada.",
"sliding_sync_disabled_notice": "Väljalülitamiseks logi Matrix'i võrgust välja ja seejärel tagasi",
"sliding_sync_disabled_notice": "Väljalülitamiseks logi uuesti sisse",
"sliding_sync_server_no_support": "Selle funktsionaalsuse tugi on sinu koduserveris puudu!",
"under_active_development": "Aktiivselt arendamisel.",
"unrealiable_e2e": "Krüptitud jututubades pole see töökindel",
@ -1976,6 +1983,7 @@
"error_join_incompatible_version_1": "Vabandust, sinu koduserver on siin osalemiseks liiga vana.",
"error_join_incompatible_version_2": "Palun võta ühendust koduserveri haldajaga.",
"error_join_title": "Liitumine ei õnnestunud",
"error_join_unknown": "Tekkis tundmatu viga.",
"error_jump_to_date": "Päringu vastus koduserverilt: viga %(statusCode)s, veakood %(errorCode)s",
"error_jump_to_date_connection": "Valitud kuupäeva vaate otsimisel ja avamisel tekkis võrguühenduse viga. Kas näiteks sinu koduserver hetkel ei tööta või on ajutisi katkestusi sinu internetiühenduses. Palun proovi mõne aja pärast uuesti. Kui viga kordub veel hiljemgi, siis palun suhtle oma koduserveri haldajaga.",
"error_jump_to_date_details": "Vea teave",
@ -2141,6 +2149,11 @@
"other": "Kustutame sõnumeid %(count)s jututoas",
"one": "Kustutame sõnumeid %(count)s jututoas"
},
"section": {
"chats": "Vestlused",
"favourites": "Lemmikud",
"low_priority": "Vähetähtis"
},
"show_less": "Näita vähem",
"show_n_more": {
"one": "Näita veel %(count)s vestlust",
@ -2341,7 +2354,7 @@
"join_rule_public_description": "Kõik võivad jututoaga liituda.",
"join_rule_restricted": "Kogukonna liikmed",
"join_rule_restricted_description": "Kõik kogukonnakeskuse liikmed saavad jututuba leida ja sellega liituda. <a>Muuda lubatud kogukonnakeskuste loendit.</a>",
"join_rule_restricted_description_active_space": "Kõik <spaceName/> kogukonnakeskuse liikmed saavad leida ja liituda. Sa võid valida ka muid kogukonnakeskuseid.",
"join_rule_restricted_description_active_space": "Kõik <spaceName/> kogukonna liikmed võivad liituda.",
"join_rule_restricted_description_prompt": "Kõik kogukonnakeskuse liikmed saavad leida ja liituda. Sa võid valida ka mitu kogukonnakeskust.",
"join_rule_restricted_description_spaces": "Ligipääsuga kogukonnakeskused",
"join_rule_restricted_dialog_description": "Vali missugustel kogukonnakeskustel on sellele jututoale ligipääs. Kui kogukonnakeskus on valitud, siis selle liikmed saavad <RoomName/> jututuba leida ja temaga liituda.",
@ -2385,7 +2398,7 @@
"upload_avatar_label": "Laadi üles profiilipilt ehk avatar",
"visibility": {
"alias_section": "Aadress",
"error_failed_save": "Kogukonnakeskuse nähtavust ei õnnestunud uuendada",
"error_failed_save": "Selle kogukonna seadistuste uuendamine ei õnnestunud",
"error_update_guest_access": "Ei õnnestunud selle kogukonnakekuse külaliste ligipääsureegleid uuendada",
"error_update_history_visibility": "Ei õnnestunud selle kogukonnakekuse ajaloo loetavust uuendada",
"guest_access_disabled": "Sul pole õigust muuta külaliskasutajate ligipääsuvõimalusi.",
@ -2665,7 +2678,7 @@
"unable_to_load_msisdns": "Telefoninumbrite laadimine ei õnnestu",
"username": "Kasutajanimi"
},
"inline_url_previews_default": "Luba URL'ide vaikimisi eelvaated",
"inline_url_previews_default": "Luba eelvaated",
"insert_trailing_colon_mentions": "Mainimiste järel näita sõnumi alguses koolonit",
"invite_controls": {
"default_label": "Luba kasutajatel sind kutsida jututubadesse"
@ -2803,6 +2816,7 @@
"enable_tray_icon": "Näita süsteemisalve ikooni ja Element'i akna sulgemisel minimeeri ta salve",
"keyboard_heading": "Kiirklahvid",
"keyboard_view_shortcuts_button": "<a>Vaata siit</a> kõiki kiirklahve.",
"link_previews_heading": "Linkide eelvaated",
"media_heading": "Pildid, gif'id ja videod",
"presence_description": "Jaga teistega oma olekut ja tegevusi.",
"publish_timezone": "Avalda oma ajavööd oma avalikus profiilis",
@ -2863,16 +2877,16 @@
"best_security_note": "Parima turvalisuse nimel verifitseeri kõik oma sessioonid ning logi välja neist, mida sa enam ei kasuta või ei tunne ära.",
"browser": "Brauser",
"confirm_sign_out": {
"one": "Kinnita selle seadme väljalogimine",
"other": "Kinnita nende seadmete väljalogimine"
"one": "Kinnita selle seadme eemaldamine",
"other": "Kinnita nende seadmete eemaldamine"
},
"confirm_sign_out_body": {
"one": "Kinnitamaks selle seadme väljalogimine klõpsi järgnevat nuppu.",
"other": "Kinnitamaks nende seadmete väljalogimine klõpsi järgnevat nuppu."
"one": "Kinnitamaks selle seadme eemaldamine klõpsi järgnevat nuppu.",
"other": "Kinnitamaks nende seadmete eemaldamine klõpsi järgnevat nuppu."
},
"confirm_sign_out_continue": {
"other": "Logi seadmed võrgust välja",
"one": "Logi seade võrgust välja"
"one": "Eemalda seade",
"other": "Eemalda seadmed"
},
"confirm_sign_out_sso": {
"one": "Kasutades ühekordse sisselogimisega oma isiku tõestamist kinnita selle seadme väljalogimine.",
@ -2899,7 +2913,7 @@
"inactive_sessions": "Mitteaktiivsed sessioonid",
"inactive_sessions_explainer_1": "Mitteaktiivsed seansid on seansid, mida sa ei ole mõnda aega kasutanud, kuid neil jätkuvalt lubatakse laadida krüptimisvõtmeid.",
"inactive_sessions_explainer_2": "Mitteaktiivsete seansside eemaldamine parandab turvalisust ja jõudlust ning lihtsustab võimalike kahtlaste seansside tuvastamist.",
"inactive_sessions_list_description": "Võimalusel logi välja vanadest seanssidest (%(inactiveAgeDays)s päeva või vanemad), mida sa enam ei kasuta.",
"inactive_sessions_list_description": "Võimalusel eemalda vanad sessioonid (%(inactiveAgeDays)s päeva või vanemad), mida sa enam ei kasuta.",
"ip": "IP-aadress",
"last_activity": "Viimati kasutusel",
"manage": "Halda seda sessiooni",
@ -2929,12 +2943,12 @@
"sign_in_with_qr": "Seosta uus seade",
"sign_in_with_qr_button": "Näita QR-koodi",
"sign_in_with_qr_description": "Kasuta QR-koodi teise seadmesse sisse logimiseks ja turvalise sõnumivahetuse seadistamiseks.",
"sign_in_with_qr_unsupported": "Seda võimalust ei toeta sinu teenusepakkuja",
"sign_out": "Logi sellest sessioonist välja",
"sign_out_all_other_sessions": "Logi kõikidest ülejäänud sessioonidest välja: %(otherSessionsCount)s sessioon(i)",
"sign_in_with_qr_unsupported": "Sinu teenusepakkuja ei toeta seda võimalust",
"sign_out": "Eemalda see sessioon",
"sign_out_all_other_sessions": "Eemalda kõik muud sessioonid: %(otherSessionsCount)s sessioon(i)",
"sign_out_confirm_description": {
"one": "Kas sa oled kindel et soovid %(count)s sessiooni võrgust välja logida?",
"other": "Kas sa oled kindel et soovid %(count)s sessiooni võrgust välja logida?"
"one": "Kas sa oled kindel et soovid %(count)s sessiooni eemaldada?",
"other": "Kas sa oled kindel et soovid %(count)s sessiooni eemaldada?"
},
"sign_out_n_sessions": {
"one": "Eemalda %(count)s sessioon",
@ -2949,13 +2963,13 @@
"unverified_sessions": "Verifitseerimata sessioonid",
"unverified_sessions_explainer_1": "Kontrollimata sessioonid on sessioonid, kuhu on sinu volitustega sisse logitud, kuid mida ei ole risttuvastamisega kontrollitud.",
"unverified_sessions_explainer_2": "Kuna nende näol võib olla tegemist võimaliku konto volitamata kasutamisega, siis palun tee kindlaks, et need sessioonid on sulle tuttavad.",
"unverified_sessions_list_description": "Turvalise sõnumvahetuse nimel verifitseeri kõik oma sessioonid ning logi neist välja, mida sa enam ei kasuta või ei tunne enam ära.",
"unverified_sessions_list_description": "Turvalise sõnumvahetuse nimel verifitseeri kõik oma sessioonid ning eemalda need, mida sa enam ei kasuta või ei tunne enam ära.",
"url": "URL",
"verified_session": "Verifitseeritud sessioon",
"verified_sessions": "Verifitseeritud sessioonid",
"verified_sessions_explainer_1": "Verifitseeritud sessioonideks loetakse Element'is või mõnes muus Matrix'i rakenduses selliseid sessioone, kus sa kas oled sisestanud oma salafraasi või tuvastanud end mõne teise oma verifitseeritud sessiooni abil.",
"verified_sessions_explainer_2": "See tähendab, et selles sessioonis on ka kõik vajalikud võtmed krüptitud sõnumite lugemiseks ja teistele kasutajatele kinnitamiseks, et sa usaldad seda sessiooni.",
"verified_sessions_list_description": "Parima turvalisuse nimel logi välja neist sessioonidest, mida sa enam ei kasuta või ei tunne ära.",
"verified_sessions_list_description": "Parima turvalisuse nimel eemalda need sessioonid, mida sa enam ei kasuta või ei tunne ära.",
"verify_session": "Verifitseeri sessioon",
"web_session": "Veebirakendus"
},
@ -3102,6 +3116,10 @@
"server_error_detail": "Server pole kas saadaval, on ülekoormatud või midagi muud läks viltu.",
"shrug": "Lisa ¯\\_(ツ)_/¯ smaili vormindamata teksti algusesse",
"spoiler": "Saadab selle sõnumi rõõmurikkujana",
"status": {
"description": "Määratle oma praegune olek",
"too_long_text": "Sinu sisestatud tekst on liiga pikk."
},
"tableflip": "Lisab vormindamata sõnumi ette (╯°□°)╯︵ ┻━┻",
"topic": "Otsib või määrab jututoa teema",
"topic_none": "Sellel jututoal puudub teema.",
@ -3453,8 +3471,8 @@
"m.room.member": {
"accepted_3pid_invite": "%(targetName)s võttis vastu kutse %(displayName)s nimel",
"accepted_invite": "%(targetName)s võttis kutse vastu",
"ban": "%(senderName)s keelas ligipääsu kasutajale %(targetName)s",
"ban_reason": "%(senderName)s keelas ligipääsu kasutajale %(targetName)s: %(reason)s",
"ban": "%(senderName)s keelas ligipääsu kasutajale",
"ban_reason": "%(senderName)s keelas ligipääsu kasutajale: %(reason)s",
"change_avatar": "%(senderName)s muutis oma profiilipilti",
"change_name": "%(oldDisplayName)s muutis oma kuvatava nime %(displayName)s-ks",
"change_name_avatar": "%(oldDisplayName)s muutis oma kuvatavat nime ja tunnuspilti",
@ -3529,7 +3547,6 @@
"created_rule_rooms": "%(senderName)s määras %(reason)s tõttu jututubade ligipääsukeelu reegli, mis vastas tingimusele %(glob)s",
"created_rule_servers": "%(senderName)s määras %(reason)s tõttu serverite ligipääsukeelu reegli, mis vastas tingimusele %(glob)s",
"created_rule_users": "%(senderName)s määras %(reason)s tõttu kasutajate ligipääsukeelu reegli, mis vastas tingimusele %(glob)s",
"message_hidden": "Sa oled seda kasutajat eiranud ja seega tema sõnum on peidetud. <a>Näita seda ikkagi.</a>",
"removed_rule": "%(senderName)s eemaldas ligipääsukeelu reegli, mis vastas tingimusele %(glob)s",
"removed_rule_rooms": "%(senderName)s eemaldas jututubade ligipääsukeelu reegli, mis vastas tingimusele %(glob)s",
"removed_rule_servers": "%(senderName)s eemaldas serverite ligipääsukeelu reegli, mis vastas tingimusele %(glob)s",
@ -3834,12 +3851,6 @@
"verify_button": "Verifitseeri kasutaja",
"verify_explainer": "Lisaturvalisus mõttes verifitseeri see kasutaja võrreldes selleks üheks korraks loodud koodi mõlemas seadmes."
},
"user_menu": {
"link_new_device": "Seo uus seade",
"settings": "Kõik seadistused",
"switch_theme_dark": "Kasuta tumedat kujundust",
"switch_theme_light": "Kasuta heledat kujundust"
},
"voip": {
"already_in_call": "Kõne on juba pooleli",
"already_in_call_person": "Sinul juba kõne käsil selle osapoolega.",
@ -3855,6 +3866,16 @@
"call_held": "%(peerName)s pani kõne ootele",
"call_held_resume": "Sa panid kõne ootele. <a>Jätka kõnet</a>",
"call_held_switch": "Sa panid kõne ootele <a>Lülita tagasi</a>",
"call_members": {
"exhaustive": {
"one": "Vestluses on <avatars/>",
"other": "Vestluses on <avatars/>"
},
"overflow": {
"one": "Kõnes osalevad <avatars/> + veel %(overflowCount)s osaleja",
"other": "Kõnes osalevad <avatars/> + veel %(overflowCount)s osalejat"
}
},
"call_toast_unknown_room": "Teadmata jututuba",
"camera_disabled": "Sinu seadme kaamera on välja lülitatud",
"camera_enabled": "Sinu seadme kaamera on jätkuvalt kasutusel",
@ -3877,10 +3898,12 @@
"enable_microphone": "Eemalda mikrofoni summutamine",
"expand": "Pöördu tagasi kõne juurde",
"get_call_link": "Jaga kõne linki",
"group_call_started": "Grupikõne algas",
"hangup": "Katkesta kõne",
"hide_sidebar_button": "Peida külgpaan",
"input_devices": "Sisendseadmed",
"jitsi_call": "Jitsi-põhine kõne",
"join_with_video": "Liitu oma videovooga",
"legacy_call": "Vana lahendusega kõne",
"maximise": "Täida ekraan",
"maximise_call": "Tee kõneaken suureks",
@ -3942,6 +3965,11 @@
"you_are_presenting": "Sina esitad"
},
"web_default_device_name": "%(appName)s: %(browserName)s operatsioonisüsteemis %(osName)s",
"welcome": {
"tagline_element": "Kiiruse ja lihtsuse huvides ülivõimas.",
"title_element": "Ole oma elemendis",
"title_generic": "Tere tulemast, see on %(brand)s"
},
"widget": {
"added_by": "Vidina lisaja",
"capabilities_dialog": {
@ -3963,6 +3991,7 @@
"change_name_this_room": "Muuda selle jututoa nime",
"change_topic_active_room": "Muuda oma aktiivse jututoa teemat",
"change_topic_this_room": "Muuda selle jututoa teemat",
"download_file": "Laadi failid alla meediumihoidlast",
"receive_membership_active_room": "Näita, millal teised sinu aktiivse toaga liituvad, sealt lahkuvad või sellesse tuppa kutsutakse",
"receive_membership_this_room": "Näita, millal inimesed toaga liituvad, lahkuvad või siia tuppa kutsutakse",
"remove_ban_invite_leave_active_room": "Aktiivsest jututoast inimeste eemaldamine, väljamüksamine, keelamine või tuppa kutsumine",

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