Merge remote-tracking branch 'origin/develop' into hs/refactor-upload-logic

This commit is contained in:
Will Hunt 2026-04-30 16:51:34 +01:00
commit 178587ff88
189 changed files with 2815 additions and 1047 deletions

View File

@ -93,7 +93,7 @@ jobs:
if: env.ENABLE_COVERAGE == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: coverage-${{ matrix.runner }}
name: coverage-jest-${{ matrix.runner }}
path: |
apps/web/coverage
!apps/web/coverage/lcov-report
@ -124,9 +124,10 @@ jobs:
name: Vitest
strategy:
matrix:
package:
- shared-components
- module-api
path:
- apps/desktop
- packages/shared-components
- packages/module-api
runs-on: ubuntu-24.04
steps:
- name: Checkout code
@ -149,30 +150,39 @@ jobs:
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: |
packages/${{ matrix.package }}/node_modules/.cache
packages/${{ matrix.package }}/node_modules/.vite/vitest
key: ${{ hashFiles('pnpm-lock.yaml') }}
${{ matrix.path }}/node_modules/.cache
${{ matrix.path }}/node_modules/.vite/vitest
key: ${{ matrix.path }}-${{ hashFiles('pnpm-lock.yaml') }}
- name: Setup playwright
uses: ./.github/actions/setup-playwright
if: matrix.package == 'shared-components'
if: matrix.path == 'packages/shared-components'
with:
write-cache: ${{ github.event_name != 'merge_group' }}
- name: Run tests
working-directory: "packages/${{ matrix.package }}"
working-directory: ${{ matrix.path }}
run: pnpm test:unit --coverage=$ENABLE_COVERAGE
# Dump the disk usage on failure, because this job seems to fail with disk fills sometimes
- name: df
run: df
run: df -h && df -i
if: ${{ failure() }}
- name: Calculate artifact name
if: env.ENABLE_COVERAGE == 'true'
id: artifact
run: |
NAME=$(basename "$MATRIX_PATH")
echo "name=$NAME" >> $GITHUB_OUTPUT
env:
MATRIX_PATH: ${{ matrix.path }}
- name: Upload Artifact
if: env.ENABLE_COVERAGE == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: coverage-${{ matrix.package }}
name: coverage-${{ steps.artifact.outputs.name }}
path: |
packages/${{ matrix.package }}/coverage
!packages/${{ matrix.package }}/coverage/lcov-report
${{ matrix.path }}/coverage
!${{ matrix.path }}/coverage/lcov-report

View File

@ -62,4 +62,10 @@ CHANGELOG.md
/packages/shared-components/typedoc/
/packages/shared-components/storybook-static/
# These files are generated by running `pnpm -r lint:types` and do not adhere to prettier's requirements.
# All of them are .gitignored within their parent directory.
/packages/playwright-common/lib/
/packages/module-api/lib/
/packages/module-api/temp/
/.nx/

View File

@ -1,3 +1,10 @@
Changes in [1.12.17](https://github.com/element-hq/element-web/releases/tag/v1.12.17) (2026-04-30)
==================================================================================================
## 🐛 Bug Fixes
* [Backport] Fix OIDC login callback handling on Element Desktop ([#33337](https://github.com/element-hq/element-web/pull/33337)). Contributed by @t3chguy.
Changes in [1.12.16](https://github.com/element-hq/element-web/releases/tag/v1.12.16) (2026-04-28)
==================================================================================================
## 🦖 Deprecations

View File

@ -86,5 +86,12 @@ module.exports = {
"@typescript-eslint/no-non-null-assertion": "off",
},
},
{
files: ["src/**/*.test.ts", "electron-builder.ts", "vitest.config.ts"],
extends: ["plugin:matrix-org/typescript"],
parserOptions: {
project: ["tsconfig.node.json"],
},
},
],
};

View File

@ -52,6 +52,7 @@ interface Variant extends Metadata {
}
type Writable<T> = NonNullable<
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
T extends Function ? T : T extends object ? { -readonly [K in keyof T]: Writable<T[K]> } : T
>;
@ -74,7 +75,7 @@ if (process.env.VARIANT_PATH) {
}
for (const key in variant) {
console.log(`${key}: ${variant[key]}`);
console.log(`${key}: ${variant[key as keyof Variant]}`);
}
interface Configuration extends BaseConfiguration {

View File

@ -3,7 +3,7 @@
"productName": "Element",
"main": "lib/electron-main.js",
"exports": "./lib/electron-main.js",
"version": "1.12.16",
"version": "1.12.17",
"description": "Element: the future of secure communication",
"author": {
"name": "Element",
@ -32,8 +32,9 @@
"lint": "pnpm lint:types && pnpm lint:js",
"lint:js": "eslint --max-warnings 0 src hak playwright scripts",
"lint:js-fix": "eslint --fix --max-warnings 0 src hak playwright scripts && prettier --log-level=warn --write .",
"lint:types": "pnpm lint:types:src && pnpm lint:types:test && pnpm lint:types:scripts && pnpm lint:types:hak",
"lint:types": "pnpm lint:types:src && pnpm lint:types:node && pnpm lint:types:test && pnpm lint:types:scripts && pnpm lint:types:hak",
"lint:types:src": "tsc --noEmit",
"lint:types:node": "tsc --noEmit -p tsconfig.node.json",
"lint:types:test": "tsc --noEmit -p playwright/tsconfig.json",
"lint:types:scripts": "tsc --noEmit -p scripts/tsconfig.json",
"lint:types:hak": "tsc --noEmit -p hak/tsconfig.json",
@ -49,6 +50,7 @@
"docker:install": "scripts/in-docker.sh pnpm install",
"clean": "rimraf webapp.asar dist packages deploys lib",
"hak": "node scripts/hak/index.ts",
"test:unit": "vitest",
"test:playwright": "nx test:playwright --",
"test:playwright:open": "nx test:playwright -- --ui",
"test:playwright:screenshots": "nx test:playwright:screenshots --",
@ -70,6 +72,7 @@
"@babel/preset-typescript": "^7.18.6",
"@electron/asar": "4.2.0",
"@electron/fuses": "^2.1.1",
"@element-hq/vite-common": "workspace:*",
"@playwright/test": "catalog:",
"@stylistic/eslint-plugin": "^5.0.0",
"@types/auto-launch": "^5.0.1",
@ -79,6 +82,7 @@
"@types/pacote": "^11.1.1",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"@vitest/coverage-v8": "catalog:",
"app-builder-lib": "26.9.0",
"chokidar": "^5.0.0",
"detect-libc": "^2.0.0",
@ -95,17 +99,20 @@
"eslint-plugin-unicorn": "^56.0.0",
"glob": "^13.0.0",
"matrix-web-i18n": "catalog:",
"memfs": "^4.57.2",
"mkdirp": "^3.0.0",
"pacote": "^21.0.0",
"prettier": "^3.0.0",
"rimraf": "^6.0.0",
"tar": "^7.5.8",
"typescript": "6.0.3"
"typescript": "6.0.3",
"vitest": "catalog:",
"vitest-sonar-reporter": "catalog:"
},
"hakDependencies": {
"matrix-seshat": "4.2.0"
},
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319",
"packageManager": "pnpm@10.33.2+sha512.a90faf6feeab71ad6c6e57f94e0fe1a12f5dcc22cd754db40ae9593eb6a3e0b6b12e3540218bb37ae083404b1f2ce6db2a4121e979829b4aff94b99f49da1cf8",
"nx": {
"includedScripts": []
}

View File

@ -0,0 +1,87 @@
/*
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 ProtocolHandler from "./protocol.js";
const TEST_PROTOCOL = "test.proto";
const TEST_SESSION_ID = "test_session_id";
const USER_DATA_DIR = "/Users/name/Library/Application Support/Element";
vi.mock("node:fs", () => ({ default: memfs }));
vi.mock("electron", () => ({
app: {
getPath: vi.fn().mockReturnValue("/Users/name/Library/Application Support/Element"),
on: vi.fn(),
},
ipcMain: {
handle: vi.fn(),
},
}));
beforeEach(() => {
// Reset the state of the in-memory fs
vol.reset();
});
describe("ProtocolHandler", () => {
describe("getProfileFromDeeplink", () => {
const handler = new ProtocolHandler(TEST_PROTOCOL);
beforeEach(() => {
vol.fromJSON(
{
"./sso-sessions.json": JSON.stringify({ [TEST_SESSION_ID]: USER_DATA_DIR }),
},
USER_DATA_DIR,
);
});
it("should handle legacy SSO URIs", () => {
expect(
handler.getProfileFromDeeplink([
"Element.app",
`element://vector/webapp/?element-desktop-ssoid=${TEST_SESSION_ID}`,
]),
).toBe(USER_DATA_DIR);
});
it("should handle OIDC URIs with response_mode=query", () => {
expect(
handler.getProfileFromDeeplink([
"Element.app",
`${TEST_PROTOCOL}:/vector/webapp/?no_universal_links=true&code=DEADBEEF&state=foobar:element-desktop-ssoid:${TEST_SESSION_ID}`,
]),
).toBe(USER_DATA_DIR);
});
it("should handle OIDC URIs with response_mode=fragment", () => {
expect(
handler.getProfileFromDeeplink([
"Element.app",
`${TEST_PROTOCOL}:/vector/webapp/?no_universal_links=true#code=DEADBEEF&state=foobar:element-desktop-ssoid:${TEST_SESSION_ID}`,
]),
).toBe(USER_DATA_DIR);
});
it("should handle malformed OIDC URIs gracefully", () => {
expect(
handler.getProfileFromDeeplink([
"Element.app",
`${TEST_PROTOCOL}:/vector/webapp/?no_universal_links=true#code=DEADBEEF:element-desktop-ssoid:${TEST_SESSION_ID}`,
]),
).toBeUndefined();
});
it("should handle unrelated URIs gracefully", () => {
expect(handler.getProfileFromDeeplink(["Element.app", `${TEST_PROTOCOL}:/vector/webapp/`])).toBeUndefined();
expect(handler.getProfileFromDeeplink(["Element.app", `test.unrelated:/vector/webapp/`])).toBeUndefined();
});
});
});

View File

@ -97,7 +97,8 @@ export default class ProtocolHandler {
const s = fs.readFileSync(storePath, { encoding: "utf8" });
const o = JSON.parse(s);
return typeof o === "object" ? o : {};
} catch {
} catch (e) {
console.warn("Unable to read protocol store, starting with empty store: ", e);
return {};
}
}
@ -130,10 +131,26 @@ export default class ProtocolHandler {
let sessionId = parsedUrl.searchParams.get(SEARCH_PARAM);
if (!sessionId) {
// In OIDC, we must shuttle the value in the `state` param rather than `element-desktop-ssoid`
// We encode it as a suffix like `:element-desktop-ssoid:XXYYZZ`
sessionId = parsedUrl.searchParams.get("state")!.split(`:${SEARCH_PARAM}:`)[1];
// We encode it as a suffix like `:element-desktop-ssoid:XXYYZZ`.
// The OIDC flow may have used response_mode=fragment or query, so we need to handle both cases.
let searchParams = parsedUrl.searchParams;
if (parsedUrl.hash.includes("=")) {
const [params] = parsedUrl.hash.substring(1).split("?", 2);
searchParams = new URLSearchParams(params);
}
const state = searchParams.get("state");
if (state) {
sessionId = state.split(`:${SEARCH_PARAM}:`)[1];
}
}
console.log("Forwarding to profile: ", store[sessionId]);
if (!sessionId) {
console.warn("Unable to read session ID in deeplink url:", deeplinkUrl);
return undefined;
}
console.log("Forwarding to profile:", store[sessionId]);
return store[sessionId];
}
}

View File

@ -14,5 +14,6 @@
"lib": ["es2022", "es2024.promise"],
"strict": true
},
"include": ["./src/**/*.ts", "./src/**/*.cts"]
"include": ["./src/**/*.ts", "./src/**/*.cts"],
"exclude": ["./src/**/*.test.ts"]
}

View File

@ -0,0 +1,15 @@
{
"compilerOptions": {
"resolveJsonModule": true,
"module": "nodenext",
"moduleResolution": "NodeNext",
"target": "es2022",
"sourceMap": false,
"typeRoots": [],
"types": [],
"skipLibCheck": true,
"noEmit": true,
"strict": true
},
"include": ["./electron-builder.ts", "./vitest.config.ts", "./src/**/*.d.ts", "./src/**/*.test.ts"]
}

View File

@ -0,0 +1,23 @@
/*
Copyright 2026 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { defineConfig, mergeConfig } from "vitest/config";
import baseConfig from "@element-hq/vite-common/vite.config.js";
export default mergeConfig(
baseConfig,
defineConfig({
test: {
coverage: {
// The coverage report currently chokes on this file as it doesn't process it as TypeScript
exclude: ["src/preload.cts"],
},
include: ["src/**/*.test.ts"],
},
}),
true,
);

View File

@ -15,7 +15,7 @@ WORKDIR /src
COPY --parents package.json pnpm-lock.yaml pnpm-workspace.yaml patches scripts **/package.json /src/
RUN corepack enable
RUN --mount=type=bind,source=.git,target=/src/.git /src/scripts/docker-link-repos.sh
RUN pnpm install
RUN pnpm install --frozen-lockfile
# Build
COPY --link --exclude=.git --exclude=apps/web/docker . /src

View File

@ -1,6 +1,6 @@
{
"name": "element-web",
"version": "1.12.16",
"version": "1.12.17",
"description": "Element: the future of secure communication",
"author": "New Vector Ltd.",
"repository": {
@ -244,6 +244,6 @@
"engines": {
"node": ">=22.18"
},
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319",
"packageManager": "pnpm@10.33.2+sha512.a90faf6feeab71ad6c6e57f94e0fe1a12f5dcc22cd754db40ae9593eb6a3e0b6b12e3540218bb37ae083404b1f2ce6db2a4121e979829b4aff94b99f49da1cf8",
"private": true
}

View File

@ -258,6 +258,47 @@ test.describe("Room list custom sections", () => {
});
});
test.describe("Collapse and expand all sections", () => {
test("should collapse all sections when 'Collapse all sections' button is clicked", async ({ page, app }) => {
await app.client.createRoom({ name: "my room" });
await createCustomSection(page, "Work");
const roomList = getRoomList(page);
const header = getRoomListHeader(page);
await expect(getSectionHeader(page, "Chats")).toBeVisible();
await expect(getSectionHeader(page, "Work")).toBeVisible();
const collapseButton = header.getByRole("button", { name: "Collapse all sections" });
await expect(collapseButton).toBeVisible();
await expect(roomList.getByRole("row", { name: "Open room my room" })).toBeVisible();
await collapseButton.click();
await expect(getSectionHeader(page, "Chats")).toHaveAttribute("aria-expanded", "false");
await expect(getSectionHeader(page, "Work")).toHaveAttribute("aria-expanded", "false");
});
test("should expand all sections when 'Expand all sections' button is clicked", async ({ page, app }) => {
await app.client.createRoom({ name: "my room" });
await createCustomSection(page, "Work");
const roomList = getRoomList(page);
const header = getRoomListHeader(page);
await expect(getSectionHeader(page, "Chats")).toBeVisible();
await header.getByRole("button", { name: "Collapse all sections" }).click();
await expect(roomList.getByRole("row", { name: "Open room my room" })).not.toBeVisible();
await header.getByRole("button", { name: "Expand all sections" }).click();
await expect(getSectionHeader(page, "Chats")).toHaveAttribute("aria-expanded", "true");
await expect(getSectionHeader(page, "Work")).toHaveAttribute("aria-expanded", "true");
});
});
test.describe("Adding a room to a custom section", () => {
test("should add a room to a custom section via the More Options menu", async ({ page, app }) => {
await app.client.createRoom({ name: "my room" });

View File

@ -751,7 +751,7 @@ test.describe("Timeline", () => {
await expect(page.locator(".mx_EventTile[data-layout=irc] .mx_ViewSourceEvent_expanded")).toBeVisible();
});
test("should render file size in kibibytes on a file tile", async ({ page, room }) => {
test("should render file size in kibibytes on a file tile", async ({ page, app, room }) => {
await page.goto(`/#/room/${room.roomId}`);
await expect(
page
@ -760,6 +760,7 @@ test.describe("Timeline", () => {
).toBeVisible();
// Upload a file from the message composer
app.
await page
.locator(".mx_MessageComposer_actions input[type='file']")
.setInputFiles(getSampleFilePath("matrix-org-client-versions.json"));

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 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:034500c4797287bfcdc4d13304e89ac65ce44a0fa33664836aea6e42c33535fb";
"ghcr.io/element-hq/matrix-authentication-service:main@sha256:c765fb602f78e77eccaa8e020e56c39eef99eccbabc9cb0df2c5705f60ca899e";
/**
* 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:b2fec2c9460f5b297a3a4ce78037902590240a1978301ed1d4bc97918c451041";
"ghcr.io/element-hq/synapse:develop@sha256:53b1c81dc161be1d999344ca727a520972b912fc24681dfafe25fca4b766b2a5";
/**
* SynapseContainer which freezes the docker digest to stabilise tests,

View File

@ -163,9 +163,10 @@ b {
font-weight: bold;
}
a:hover,
a:link,
a:visited {
/* Keep the legacy link colour without overriding Compound anchors. */
a:where(:not([data-kind])):hover,
a:where(:not([data-kind])):link,
a:where(:not([data-kind])):visited {
color: $accent-alt;
}

View File

@ -1,3 +1,6 @@
/* Shared cascade order: Compound tokens, Compound Web, shared components, then app overrides. */
@layer compound-tokens, compound-web, shared-components, app-web;
/* Modules bundled with compound apply compound lastly. In order to catch issue due to css class ordering, we put compound at the end */
@import url("@vector-im/compound-design-tokens/assets/web/css/compound-design-tokens.css") layer(compound);
@import url("@vector-im/compound-web/dist/style.css");
@import url("@vector-im/compound-design-tokens/assets/web/css/compound-design-tokens.css") layer(compound-tokens);
@import url("@vector-im/compound-web/dist/style.css") layer(compound-web);

View File

@ -11,9 +11,8 @@ Please see LICENSE files in the repository root for full details.
/* Container for live recording and playback controls */
.mx_MediaBody.mx_VoiceMessagePrimaryContainer {
/* The waveform (right) has a 1px padding on it that we want to account for, otherwise */
/* inherit from mx_MediaBody */
padding-right: 11px;
/* Match mx_MediaBody spacing, offsetting the waveform's 1px internal right padding. */
padding: 6px 11px 6px 12px;
/* Cheat at alignment a bit */
display: flex;

View File

@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
.mx_CompleteSecurityBody {
color: $authpage-primary-color;
background-color: $background;
border-radius: 4px;
border-radius: 24px;
padding: 20px 20px 60px 20px;
box-sizing: border-box;

View File

@ -7,7 +7,6 @@ Please see LICENSE files in the repository root for full details.
*/
.mx_MFileBody [data-type="download"] {
color: $accent;
height: var(--cpd-space-9x);
& object {

View File

@ -17,6 +17,8 @@ Please see LICENSE files in the repository root for full details.
color: $secondary-content;
font: var(--cpd-font-body-md-regular);
line-height: $font-24px;
padding: 6px 12px;
}
.mx_MAudioBody > .mx_MediaBody {
border-radius: var(--MBody-border-radius);
}

View File

@ -19,3 +19,12 @@ Please see LICENSE files in the repository root for full details.
opacity: unset; /* Unset the opacity value specified above on the search results panel */
}
}
.mx_TextualBody_urlPreviews {
/* Let shared-components own preview link colours instead of the app-wide anchor colour. */
a:where(:not([data-kind])):hover,
a:where(:not([data-kind])):link,
a:where(:not([data-kind])):visited {
color: revert-layer;
}
}

View File

@ -26,12 +26,22 @@ Please see LICENSE files in the repository root for full details.
&.mx_ThemeChoicePanel_themeSelector_disabled {
border-color: var(--cpd-color-border-disabled);
.mx_ThemeChoicePanel_themeSelector_Label {
color: var(--cpd-color-text-disabled);
cursor: not-allowed;
}
}
.mx_ThemeChoicePanel_themeSelector_Label {
color: var(--cpd-color-text-primary);
font: var(--cpd-font-body-md-semibold);
}
&:not(.mx_ThemeChoicePanel_themeSelector_disabled) {
.mx_ThemeChoicePanel_themeSelector_Label {
color: var(--cpd-color-text-primary);
}
}
}
}

View File

@ -12,7 +12,8 @@ Please see LICENSE files in the repository root for full details.
color: $primary-content;
a {
/* Compound links carry data-kind and provide their own colour. */
a:not([data-kind]) {
color: $links;
}

View File

@ -10,7 +10,8 @@ Please see LICENSE files in the repository root for full details.
color: $primary-content;
a {
/* Compound links carry data-kind and provide their own colour. */
a:not([data-kind]) {
color: $links;
}

View File

@ -1,9 +1,11 @@
@import "../../../../res/css/_font-sizes.pcss";
@import "../../legacy-light/css/_fonts.pcss";
@import "../../legacy-light/css/_legacy-light.pcss";
@import "../../legacy-dark/css/_legacy-dark.pcss";
@import "../../light-custom/css/_custom.pcss";
@import "../../../../res/css/_components.pcss";
@layer compound-tokens, compound-web, shared-components, app-web;
@import "../../../../res/css/_font-sizes.pcss" layer(app-web);
@import "../../legacy-light/css/_fonts.pcss" layer(app-web);
@import "../../legacy-light/css/_legacy-light.pcss" layer(app-web);
@import "../../legacy-dark/css/_legacy-dark.pcss" layer(app-web);
@import "../../light-custom/css/_custom.pcss" layer(app-web);
@import "../../../../res/css/_components.pcss" layer(app-web);
@import "../../../../res/css/_compound.pcss";
@import url("highlight.js/styles/atom-one-light.min.css");
@import url("github-markdown-css/github-markdown-dark.css");
@import url("highlight.js/styles/atom-one-light.min.css") layer(app-web);
@import url("github-markdown-css/github-markdown-dark.css") layer(app-web);

View File

@ -1,9 +1,11 @@
@import "../../../../res/css/_font-sizes.pcss";
@import "../../light/css/_fonts.pcss";
@import "../../light/css/_light.pcss";
@import "_dark.pcss";
@import "../../light/css/_mods.pcss";
@import "../../../../res/css/_components.pcss";
@layer compound-tokens, compound-web, shared-components, app-web;
@import "../../../../res/css/_font-sizes.pcss" layer(app-web);
@import "../../light/css/_fonts.pcss" layer(app-web);
@import "../../light/css/_light.pcss" layer(app-web);
@import "_dark.pcss" layer(app-web);
@import "../../light/css/_mods.pcss" layer(app-web);
@import "../../../../res/css/_components.pcss" layer(app-web);
@import "../../../../res/css/_compound.pcss";
@import url("highlight.js/styles/atom-one-dark.min.css");
@import url("github-markdown-css/github-markdown-dark.css");
@import url("highlight.js/styles/atom-one-dark.min.css") layer(app-web);
@import url("github-markdown-css/github-markdown-dark.css") layer(app-web);

View File

@ -1,8 +1,10 @@
@import "../../../../res/css/_font-sizes.pcss";
@import "../../legacy-light/css/_fonts.pcss";
@import "../../legacy-light/css/_legacy-light.pcss";
@import "_legacy-dark.pcss";
@import "../../../../res/css/_components.pcss";
@layer compound-tokens, compound-web, shared-components, app-web;
@import "../../../../res/css/_font-sizes.pcss" layer(app-web);
@import "../../legacy-light/css/_fonts.pcss" layer(app-web);
@import "../../legacy-light/css/_legacy-light.pcss" layer(app-web);
@import "_legacy-dark.pcss" layer(app-web);
@import "../../../../res/css/_components.pcss" layer(app-web);
@import "../../../../res/css/_compound.pcss";
@import url("highlight.js/styles/atom-one-dark.min.css");
@import url("github-markdown-css/github-markdown-dark.css");
@import url("highlight.js/styles/atom-one-dark.min.css") layer(app-web);
@import url("github-markdown-css/github-markdown-dark.css") layer(app-web);

View File

@ -1,7 +1,9 @@
@import "../../../../res/css/_font-sizes.pcss";
@import "_fonts.pcss";
@import "_legacy-light.pcss";
@import "../../../../res/css/_components.pcss";
@layer compound-tokens, compound-web, shared-components, app-web;
@import "../../../../res/css/_font-sizes.pcss" layer(app-web);
@import "_fonts.pcss" layer(app-web);
@import "_legacy-light.pcss" layer(app-web);
@import "../../../../res/css/_components.pcss" layer(app-web);
@import "../../../../res/css/_compound.pcss";
@import url("highlight.js/styles/atom-one-light.min.css");
@import url("github-markdown-css/github-markdown-light.css");
@import url("highlight.js/styles/atom-one-light.min.css") layer(app-web);
@import url("github-markdown-css/github-markdown-light.css") layer(app-web);

View File

@ -25,16 +25,17 @@ $panels: var(--panels, var(--cpd-color-gray-600));
$panel-actions: var(--panels-actions, var(--cpd-color-gray-300));
/* --timeline-background-color */
$button-secondary-bg-color: var(--timeline-background-color);
$lightbox-border-color: var(--timeline-background-color);
$menu-bg-color: var(--timeline-background-color);
$message-action-bar-bg-color: var(--timeline-background-color);
$background: var(--timeline-background-color);
$custom-theme-background: var(--timeline-background-color, var(--cpd-color-bg-canvas-default));
$button-secondary-bg-color: $custom-theme-background;
$lightbox-border-color: $custom-theme-background;
$menu-bg-color: $custom-theme-background;
$message-action-bar-bg-color: $custom-theme-background;
$background: $custom-theme-background;
$togglesw-ball-color: var(--cpd-color-bg-action-primary-rest);
$togglesw-off-color: var(--togglesw-off-color);
$droptarget-bg-color: var(--timeline-background-color-50pct); /* still needs alpha at .5 */
$authpage-modal-bg-color: var(--timeline-background-color-50pct); /* still needs alpha at .59 */
$roomheader-bg-color: var(--timeline-background-color);
$roomheader-bg-color: $custom-theme-background;
/* --roomlist-highlights-color */
$panel-actions: var(--roomlist-highlights-color);

View File

@ -1,8 +1,10 @@
@import "../../../../res/css/_font-sizes.pcss";
@import "../../legacy-light/css/_fonts.pcss";
@import "../../legacy-light/css/_legacy-light.pcss";
@import "_custom.pcss";
@import "../../../../res/css/_components.pcss";
@layer compound-tokens, compound-web, shared-components, app-web;
@import "../../../../res/css/_font-sizes.pcss" layer(app-web);
@import "../../legacy-light/css/_fonts.pcss" layer(app-web);
@import "../../legacy-light/css/_legacy-light.pcss" layer(app-web);
@import "_custom.pcss" layer(app-web);
@import "../../../../res/css/_components.pcss" layer(app-web);
@import "../../../../res/css/_compound.pcss";
@import url("highlight.js/styles/atom-one-light.min.css");
@import url("github-markdown-css/github-markdown-light.css");
@import url("highlight.js/styles/atom-one-light.min.css") layer(app-web);
@import url("github-markdown-css/github-markdown-light.css") layer(app-web);

View File

@ -1,9 +1,11 @@
@import "../../../../res/css/_font-sizes.pcss";
@import "../../light/css/_fonts.pcss";
@import "../../light/css/_light.pcss";
@import "_light-high-contrast.pcss";
@import "../../light/css/_mods.pcss";
@import "../../../../res/css/_components.pcss";
@layer compound-tokens, compound-web, shared-components, app-web;
@import "../../../../res/css/_font-sizes.pcss" layer(app-web);
@import "../../light/css/_fonts.pcss" layer(app-web);
@import "../../light/css/_light.pcss" layer(app-web);
@import "_light-high-contrast.pcss" layer(app-web);
@import "../../light/css/_mods.pcss" layer(app-web);
@import "../../../../res/css/_components.pcss" layer(app-web);
@import "../../../../res/css/_compound.pcss";
@import url("highlight.js/styles/atom-one-light.min.css");
@import url("github-markdown-css/github-markdown-light.css");
@import url("highlight.js/styles/atom-one-light.min.css") layer(app-web);
@import url("github-markdown-css/github-markdown-light.css") layer(app-web);

View File

@ -1,8 +1,10 @@
@import "../../../../res/css/_font-sizes.pcss";
@import "_fonts.pcss";
@import "_light.pcss";
@import "_mods.pcss";
@import "../../../../res/css/_components.pcss";
@layer compound-tokens, compound-web, shared-components, app-web;
@import "../../../../res/css/_font-sizes.pcss" layer(app-web);
@import "_fonts.pcss" layer(app-web);
@import "_light.pcss" layer(app-web);
@import "_mods.pcss" layer(app-web);
@import "../../../../res/css/_components.pcss" layer(app-web);
@import "../../../../res/css/_compound.pcss";
@import url("highlight.js/styles/atom-one-light.min.css");
@import url("github-markdown-css/github-markdown-light.css");
@import url("highlight.js/styles/atom-one-light.min.css") layer(app-web);
@import url("github-markdown-css/github-markdown-light.css") layer(app-web);

View File

@ -25,9 +25,9 @@ export interface MediaPreviewConfig extends Record<string, unknown> {
/**
* Media preview setting for thumbnails of media in rooms.
*/
media_previews: MediaPreviewValue;
media_previews?: MediaPreviewValue;
/**
* Media preview settings for avatars of rooms we have been invited to.
*/
invite_avatars: MediaPreviewValue.On | MediaPreviewValue.Off;
invite_avatars?: MediaPreviewValue.On | MediaPreviewValue.Off;
}

View File

@ -725,7 +725,7 @@ export default class LegacyCallHandler extends TypedEventEmitter<LegacyCallHandl
);
finished.then(([allow]) => {
SettingsStore.setValue("fallbackICEServerAllowed", null, SettingLevel.DEVICE, allow);
SettingsStore.setValue("fallbackICEServerAllowed", null, SettingLevel.DEVICE, allow ?? null);
});
}

View File

@ -297,6 +297,9 @@ class MatrixClientPegClass implements IMatrixClientPeg {
opts.lazyLoadMembers = true;
opts.clientWellKnownPollPeriod = 2 * 60 * 60; // 2 hours
opts.threadSupport = true;
if (SettingsStore.getValue("feature_user_status")) {
opts.unstableMSC4429SyncUserProfileFields = ["org.matrix.msc4426.status"];
}
if (SettingsStore.getValue("feature_sliding_sync")) {
throw new UserFriendlyError("sliding_sync_legacy_no_longer_supported");

View File

@ -142,6 +142,16 @@ interface EmittedEvents {
[NotifierEvent.NotificationHiddenChange]: (hidden: boolean) => void;
}
/**
* Type representing a notification sound setting
*/
export type NotificationSound = {
url: string;
name?: string;
type?: string;
size?: number;
};
class NotifierClass extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents> {
private notifsByRoom: Record<string, Notification[]> = {};
@ -223,12 +233,7 @@ class NotifierClass extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents
}
}
public getSoundForRoom(roomId: string): {
url: string;
name: string;
type: string;
size: number;
} | null {
public getSoundForRoom(roomId: string): NotificationSound | null {
// We do no caching here because the SDK caches setting
// and the browser will cache the sound.
const content = SettingsStore.getValue("notificationSound", roomId);

View File

@ -19,7 +19,7 @@ import SettingsStore from "./settings/SettingsStore";
import { type ScreenName } from "./PosthogTrackers";
import { type ActionPayload } from "./dispatcher/payloads";
import { Action } from "./dispatcher/actions";
import { type SettingUpdatedPayload } from "./dispatcher/payloads/SettingUpdatedPayload";
import { isSettingUpdatedPayload, type SettingUpdatedPayload } from "./dispatcher/payloads/SettingUpdatedPayload";
import dis from "./dispatcher/dispatcher";
import { Layout } from "./settings/enums/Layout";
@ -199,8 +199,8 @@ export class PosthogAnalytics {
const settingsPayload = payload as SettingUpdatedPayload;
if (["layout", "useCompactLayout"].includes(settingsPayload.settingName)) {
this.onLayoutUpdated();
} else if (settingsPayload.settingName === "urlPreviewsEnabled" && !settingsPayload.roomId) {
this.onUrlPreviewSettingUpdated(settingsPayload.newValue as boolean);
} else if (isSettingUpdatedPayload(settingsPayload, "urlPreviewsEnabled") && !settingsPayload.roomId) {
this.onUrlPreviewSettingUpdated(settingsPayload.newValue);
}
};

View File

@ -122,7 +122,7 @@ export default class ManageEventIndexDialog extends React.Component<IProps, ISta
private onCrawlerSleepTimeChange = (e: ChangeEvent<HTMLInputElement>): void => {
this.setState({ crawlerSleepTime: parseInt(e.target.value, 10) });
SettingsStore.setValue("crawlerSleepTime", null, SettingLevel.DEVICE, e.target.value);
SettingsStore.setValue("crawlerSleepTime", null, SettingLevel.DEVICE, e.target.valueAsNumber);
};
public render(): React.ReactNode {

View File

@ -1794,11 +1794,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
SettingsStore.watchSetting(
"blacklistUnverifiedDevices",
null,
(_settingName, _roomId, atLevel, blacklistEnabled: boolean) => {
(_settingName, _roomId, atLevel, blacklistEnabled) => {
if (atLevel != SettingLevel.DEVICE) {
return;
}
crypto.globalBlacklistUnverifiedDevices = blacklistEnabled;
crypto.globalBlacklistUnverifiedDevices = !!blacklistEnabled;
},
);
}

View File

@ -9,9 +9,8 @@ Please see LICENSE files in the repository root for full details.
import React from "react";
import {
ClientRendezvousFailureReason,
linkNewDeviceByGeneratingQR,
MSC4108FailureReason,
MSC4108RendezvousSession,
MSC4108SecureChannel,
MSC4108SignInWithQR,
RendezvousError,
type RendezvousFailureReason,
@ -55,6 +54,7 @@ export type FailureReason = RendezvousFailureReason | LoginWithQRFailureReason;
*/
export default class LoginWithQR extends React.Component<IProps, IState> {
private finished = false;
private abortController?: AbortController;
public constructor(props: IProps) {
super(props);
@ -69,35 +69,31 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
}
public componentDidMount(): void {
this.updateMode(this.props.mode).then(() => {});
void this.updateMode(this.props.mode);
}
public componentDidUpdate(prevProps: Readonly<IProps>): void {
if (prevProps.mode !== this.props.mode) {
this.updateMode(this.props.mode).then(() => {});
void this.updateMode(this.props.mode);
}
}
private async updateMode(mode: Mode, showLoading = true): Promise<void> {
if (this.state.rendezvous) {
const rendezvous = this.state.rendezvous;
rendezvous.onFailure = undefined;
this.setState({ rendezvous: undefined });
}
this.abortController?.abort();
this.abortController = new AbortController();
this.setState({ rendezvous: undefined });
if (showLoading) {
this.setState({ phase: Phase.Loading });
}
if (mode === Mode.Show) {
await this.generateAndShowCode();
await this.generateAndShowCode(this.abortController);
}
}
public componentWillUnmount(): void {
if (this.state.rendezvous && !this.finished) {
// eslint-disable-next-line react/no-direct-mutation-state
this.state.rendezvous.onFailure = undefined;
// calling cancel will call close() as well to clean up the resources
this.state.rendezvous.cancel(MSC4108FailureReason.UserCancelled);
if (!this.finished) {
this.abortController?.abort();
}
}
@ -106,24 +102,18 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
this.props.onFinished(success);
}
private generateAndShowCode = async (): Promise<void> => {
private generateAndShowCode = async (abortController: AbortController): Promise<void> => {
let rendezvous: MSC4108SignInWithQR;
try {
const transport = new MSC4108RendezvousSession({
onFailure: this.onFailure,
client: this.props.client,
});
await transport.send("");
const channel = new MSC4108SecureChannel(transport, undefined, this.onFailure);
rendezvous = new MSC4108SignInWithQR(channel, false, this.props.client, this.onFailure);
await rendezvous.generateCode();
rendezvous = await linkNewDeviceByGeneratingQR(this.props.client, this.onFailure, abortController.signal);
if (abortController.signal.aborted) return;
this.setState({
phase: Phase.ShowingQR,
rendezvous,
failureReason: undefined,
});
} catch (e) {
if (abortController.signal.aborted) return;
logger.error("Error whilst generating QR code", e);
this.setState({ phase: Phase.Error, failureReason: ClientRendezvousFailureReason.HomeserverLacksSupport });
return;
@ -142,8 +132,9 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
// we ask the user to confirm that the channel is secure
} catch (e: RendezvousError | unknown) {
if (abortController.signal.aborted) return;
logger.error("Error whilst approving login", e);
await rendezvous?.cancel(
await rendezvous.cancel(
e instanceof RendezvousError ? (e.code as MSC4108FailureReason) : ClientRendezvousFailureReason.Unknown,
);
}
@ -210,6 +201,7 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
};
public reset(): void {
this.abortController?.abort();
this.setState({
rendezvous: undefined,
verificationUri: undefined,

View File

@ -13,6 +13,7 @@ import { useCreateAutoDisposedViewModel, DisambiguatedProfileView } from "@eleme
import { DisambiguatedProfileViewModel } from "../../../viewmodels/room/timeline/event-tile/DisambiguatedProfileViewModel";
import { useRoomMemberProfile } from "../../../hooks/room/useRoomMemberProfile";
import { useUserStatus } from "../../../hooks/useUserStatus";
interface IProps {
mxEvent: MatrixEvent;
@ -27,6 +28,7 @@ export default function SenderProfile({ mxEvent, onClick, withTooltip }: IProps)
userId: sender,
member: mxEvent.sender,
});
const userStatus = useUserStatus(sender);
const disambiguatedProfileVM = useCreateAutoDisposedViewModel(
() =>
@ -37,9 +39,13 @@ export default function SenderProfile({ mxEvent, onClick, withTooltip }: IProps)
colored: true,
emphasizeDisplayName: true,
withTooltip,
userStatus,
}),
);
useEffect(() => {
disambiguatedProfileVM.setUserStatus(userStatus);
}, [disambiguatedProfileVM, userStatus]);
useEffect(() => {
disambiguatedProfileVM.setMember(sender ?? "", member);
}, [disambiguatedProfileVM, member, sender]);

View File

@ -210,7 +210,7 @@ export function TextualBodyFactory(props: Readonly<IBodyProps>): JSX.Element {
vm={textualBodyVm}
body={<EventContentBodyView vm={eventContentBodyVm} as={willHaveWrapper ? "span" : "div"} />}
bodyRef={contentRef}
urlPreviews={<UrlPreviewGroupView vm={urlPreviewVm} />}
urlPreviews={<UrlPreviewGroupView vm={urlPreviewVm} className="mx_TextualBody_urlPreviews" />}
className={getTextualBodyClassName(content.msgtype as MsgType | undefined)}
/>
);

View File

@ -38,8 +38,8 @@ function LayoutSelector(): JSX.Element {
<Root
className="mx_LayoutSwitcher_LayoutSelector"
onChange={async (evt) => {
// We don't have any file in the form, we can cast it as string safely
const newLayout = new FormData(evt.currentTarget).get("layout") as string | null;
// We don't have any file in the form, we can cast it as Layout safely
const newLayout = new FormData(evt.currentTarget).get("layout") as Layout;
await SettingsStore.setValue("layout", null, SettingLevel.DEVICE, newLayout);
}}
>

View File

@ -7,48 +7,35 @@ Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import {
type IServerVersions,
type OidcClientConfig,
type MatrixClient,
DEVICE_CODE_SCOPE,
} from "matrix-js-sdk/src/matrix";
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
import QrCodeIcon from "@vector-im/compound-design-tokens/assets/web/icons/qr-code";
import { Text } from "@vector-im/compound-web";
import { isSignInWithQRAvailable } from "matrix-js-sdk/src/rendezvous";
import { _t } from "../../../../languageHandler";
import AccessibleButton from "../../elements/AccessibleButton";
import { SettingsSubsection } from "../shared/SettingsSubsection";
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext";
import { useAsyncMemo } from "../../../../hooks/useAsyncMemo";
interface IProps {
onShowQr: () => void;
versions?: IServerVersions;
oidcClientConfig?: OidcClientConfig;
isCrossSigningReady?: boolean;
}
export function shouldShowQr(
cli: MatrixClient,
isCrossSigningReady: boolean,
oidcClientConfig?: OidcClientConfig,
versions?: IServerVersions,
): boolean {
const msc4108Supported = !!versions?.unstable_features?.["org.matrix.msc4108"];
export async function shouldShowQrForLinkNewDevice(cli: MatrixClient, isCrossSigningReady: boolean): Promise<boolean> {
const doesServerHaveSupport = await isSignInWithQRAvailable(cli);
const deviceAuthorizationGrantSupported = oidcClientConfig?.grant_types_supported.includes(DEVICE_CODE_SCOPE);
return (
!!deviceAuthorizationGrantSupported &&
msc4108Supported &&
!!cli.getCrypto()?.exportSecretsBundle &&
isCrossSigningReady
);
return doesServerHaveSupport && !!cli.getCrypto()?.exportSecretsBundle && isCrossSigningReady;
}
const LoginWithQRSection: React.FC<IProps> = ({ onShowQr, versions, oidcClientConfig, isCrossSigningReady }) => {
const LoginWithQRSection: React.FC<IProps> = ({ onShowQr, isCrossSigningReady }) => {
const cli = useMatrixClientContext();
const offerShowQr = shouldShowQr(cli, !!isCrossSigningReady, oidcClientConfig, versions);
const offerShowQr = useAsyncMemo(
() => shouldShowQrForLinkNewDevice(cli, !!isCrossSigningReady),
[cli, isCrossSigningReady],
false,
);
return (
<SettingsSubsection heading={_t("settings|sessions|sign_in_with_qr")}>

View File

@ -68,8 +68,6 @@ export default class AppearanceUserSettingsTab extends React.Component<EmptyObje
evt.stopPropagation();
}}
>
<SettingsFlag name="useCompactLayout" level={SettingLevel.DEVICE} />
<SettingsFlag
name="useBundledEmojiFont"
level={SettingLevel.DEVICE}

View File

@ -211,17 +211,17 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
private onAutocompleteDelayChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({ autocompleteDelay: e.target.value });
SettingsStore.setValue("autocompleteDelay", null, SettingLevel.DEVICE, e.target.value);
SettingsStore.setValue("autocompleteDelay", null, SettingLevel.DEVICE, e.target.valueAsNumber);
};
private onReadMarkerInViewThresholdMs = (e: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({ readMarkerInViewThresholdMs: e.target.value });
SettingsStore.setValue("readMarkerInViewThresholdMs", null, SettingLevel.DEVICE, e.target.value);
SettingsStore.setValue("readMarkerInViewThresholdMs", null, SettingLevel.DEVICE, e.target.valueAsNumber);
};
private onReadMarkerOutOfViewThresholdMs = (e: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({ readMarkerOutOfViewThresholdMs: e.target.value });
SettingsStore.setValue("readMarkerOutOfViewThresholdMs", null, SettingLevel.DEVICE, e.target.value);
SettingsStore.setValue("readMarkerOutOfViewThresholdMs", null, SettingLevel.DEVICE, e.target.valueAsNumber);
};
private renderGroup(settingIds: BooleanSettingKey[], level = SettingLevel.ACCOUNT): JSX.Element {

View File

@ -162,14 +162,6 @@ const SessionManagerTab: React.FC<{
const disableMultipleSignout = !!accountManagement?.endpoint;
const userId = matrixClient?.getUserId();
const currentUserMember = (userId && matrixClient?.getUser(userId)) || undefined;
const clientVersions = useAsyncMemo(() => matrixClient.getVersions(), [matrixClient]);
const oidcClientConfig = useAsyncMemo(async () => {
try {
return await matrixClient?.getAuthMetadata();
} catch (e) {
logger.error("Failed to discover OIDC metadata", e);
}
}, [matrixClient]);
const isCrossSigningReady = useAsyncMemo(
async () => matrixClient.getCrypto()?.isCrossSigningReady() ?? false,
[matrixClient],
@ -279,12 +271,7 @@ const SessionManagerTab: React.FC<{
return (
<SettingsTab>
<SettingsSection>
<LoginWithQRSection
onShowQr={onShowQrClicked}
versions={clientVersions}
oidcClientConfig={oidcClientConfig}
isCrossSigningReady={isCrossSigningReady}
/>
<LoginWithQRSection onShowQr={onShowQrClicked} isCrossSigningReady={isCrossSigningReady} />
<SecurityRecommendations
devices={devices}
goToFilteredList={onGoToFilteredList}

View File

@ -340,7 +340,7 @@ export class DeviceListener {
});
}
private onRecordClientInformationSettingChange: CallbackFn = (
private onRecordClientInformationSettingChange: CallbackFn<"deviceClientInformationOptIn"> = (
_originalSettingName,
_roomId,
_level,

View File

@ -403,4 +403,20 @@ export enum Action {
* or keyboard event).
*/
UserActivity = "user_activity",
/**
* Fired to request collapsing all room list sections.
*/
RoomListCollapseAllSections = "room_list_collapse_all_sections",
/**
* Fired to request expanding all room list sections.
*/
RoomListExpandAllSections = "room_list_expand_all_sections",
/**
* Fired to report the collapse state of a given room list section.
* Payload: {@link RoomListSectionsCollapseStateChangedPayload}
*/
RoomListSectionsCollapseStateChanged = "room_list_sections_collapse_state_changed",
}

View File

@ -0,0 +1,20 @@
/*
* Copyright 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import { type CollapseSectionsOption } from "@element-hq/web-shared-components";
import { type ActionPayload } from "../payloads";
import { type Action } from "../actions";
export interface RoomListSectionsCollapseStateChangedPayload extends ActionPayload {
action: Action.RoomListSectionsCollapseStateChanged;
/**
* The new collapse state for the room list sections.
* If undefined, the feature is disabled.
*/
collapseSections?: CollapseSectionsOption;
}

View File

@ -9,14 +9,26 @@ Please see LICENSE files in the repository root for full details.
import { type ActionPayload } from "../payloads";
import { type Action } from "../actions";
import { type SettingLevel } from "../../settings/SettingLevel";
import { type SettingValueType } from "../../settings/Settings";
import { type SettingKey, type Settings } from "../../settings/Settings";
export interface SettingUpdatedPayload extends ActionPayload {
export interface SettingUpdatedPayload<S extends SettingKey = SettingKey> extends ActionPayload {
action: Action.SettingUpdated;
settingName: string;
settingName: S;
roomId: string | null;
level: SettingLevel;
newValueAtLevel: SettingLevel;
newValue: SettingValueType;
newValueAtLevel: Settings[S]["default"];
newValue: Settings[S]["default"];
}
/**
* Type guard to check if a payload is a SettingUpdatedPayload for a specific setting.
* @param payload the payload to assert
* @param settingName the setting name to check for
*/
export function isSettingUpdatedPayload<S extends SettingKey>(
payload: SettingUpdatedPayload<any>,
settingName: S,
): payload is SettingUpdatedPayload<S> {
return payload.settingName === settingName;
}

View File

@ -29,7 +29,7 @@ const STORAGE_LIMIT = 100;
function migrate(): void {
const data: ILegacyFormat = JSON.parse(window.localStorage.mx_reaction_count || "{}");
const sorted = Object.entries(data).sort(([, [count1, date1]], [, [count2, date2]]) => date2 - date1);
const newFormat = sorted.map(([emoji, [count, date]]) => [emoji, count]);
const newFormat = sorted.map<RecentEmojiData[number]>(([emoji, [count, date]]) => [emoji, count]);
SettingsStore.setValue(SETTING_NAME, null, SettingLevel.ACCOUNT, newFormat.slice(0, STORAGE_LIMIT));
}
@ -41,7 +41,7 @@ export function add(emoji: string): void {
const recents = getRecentEmoji();
const i = recents.findIndex(([e]) => e === emoji);
let newEntry;
let newEntry: RecentEmojiData[number];
if (i >= 0) {
// first remove the existing tuple so that we can increment it and push it to the front
[newEntry] = recents.splice(i, 1);

View File

@ -0,0 +1,98 @@
/**
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 { useEffect, useState } from "react";
import { ClientEvent, MatrixError } from "matrix-js-sdk/src/matrix";
import { logger as rootLogger } from "matrix-js-sdk/src/logger";
import { useMatrixClientContext } from "../contexts/MatrixClientContext";
import { useTypedEventEmitter } from "./useEventEmitter";
import { useFeatureEnabled } from "./useSettings";
const logger = rootLogger.getChild("useUserStatus");
export interface UserStatus {
emoji: string;
text: string;
}
const MAX_STATUS_TEXT_BYTES = 256;
export function userStatusTextWithinMaxLength(text: string): boolean {
const textEncoder = new TextEncoder();
return textEncoder.encode(text).length <= MAX_STATUS_TEXT_BYTES;
}
/**
* Hook to get the MSC4426 user status for a given user ID. Returns undefined if the feature is disabled,
* the user does not have a status, or if there was an error fetching the status.
*
* @param userId The ID of the user whose status is being fetched.
* @returns The user's status, or undefined if not available.
*/
export function useUserStatus(userId: string | undefined): UserStatus | undefined {
const isEnabled = useFeatureEnabled("feature_user_status");
const matrixClient = useMatrixClientContext();
const [rawUserStatus, setRawUserStatus] = useState<unknown>();
useTypedEventEmitter(matrixClient, ClientEvent.UserProfileUpdate, (syncedUserId, syncProfile) => {
if (syncedUserId !== userId) {
return;
}
if (syncProfile["org.matrix.msc4426.status"]) {
setRawUserStatus(syncProfile["org.matrix.msc4426.status"]);
}
});
useEffect(() => {
(async () => {
if (!isEnabled) {
return;
}
if (!userId) {
setRawUserStatus(undefined);
return;
}
if ((await matrixClient.doesServerSupportExtendedProfiles()) === false) {
setRawUserStatus(undefined);
return;
}
try {
const result = await matrixClient.getExtendedProfileProperty(userId, "org.matrix.msc4426.status");
setRawUserStatus(result);
} catch (ex) {
if (ex instanceof MatrixError && ex.errcode === "M_NOT_FOUND") {
setRawUserStatus(undefined);
} else {
logger.warn(`Failed to get userStatus for ${userId}`, ex);
}
}
})();
}, [isEnabled, userId, matrixClient]);
if (!isEnabled) {
return;
}
if (typeof rawUserStatus !== "object" || rawUserStatus === null) {
logger.warn(`value of "org.matrix.msc4426.status" was not an object for ${userId}`);
return;
}
if ("emoji" in rawUserStatus === false || typeof rawUserStatus.emoji !== "string" || !rawUserStatus.emoji) {
logger.warn(`"emoji" property was not a valid string for ${userId}`);
return;
}
if ("text" in rawUserStatus === false || typeof rawUserStatus.text !== "string" || !rawUserStatus.text) {
logger.warn(`"text" property was not a valid string for ${userId}`);
return;
}
return {
emoji: rawUserStatus.emoji,
text: userStatusTextWithinMaxLength(rawUserStatus.text)
? rawUserStatus.text
: `${rawUserStatus.text.slice(0, MAX_STATUS_TEXT_BYTES)}`,
};
}

View File

@ -588,7 +588,6 @@
"video": "Video",
"video_room": "Video místnost",
"view_message": "Zobrazit zprávu",
"voice": "Hlas",
"warning": "Upozornění"
},
"composer": {
@ -3909,7 +3908,6 @@
"connection_lost": "Došlo ke ztrátě připojení k serveru",
"connection_lost_description": "Bez připojení k serveru nelze uskutečňovat hovory.",
"consulting": "Konzultace s %(transferTarget)s. <a>Převod na %(transferee)s</a>",
"decline_call": "Odmítnout",
"default_device": "Výchozí zařízení",
"dial": "Vytočit",
"dialpad": "Číselník",
@ -3960,7 +3958,6 @@
"show_sidebar_button": "Zobrazit postranní panel",
"silence": "Ztlumit zvonění",
"silenced": "Oznámení ztlumena",
"skip_lobby_toggle_option": "Připojte se ihned",
"start_screenshare": "Začít sdílet obrazovku",
"stop_screenshare": "Ukončit sdílení obrazovky",
"too_many_calls": "Přiliš mnoho hovorů",
@ -3982,7 +3979,6 @@
"user_is_presenting": "%(sharerName)s prezentuje",
"video_call": "Videohovor",
"video_call_incoming": "Příchozí videohovor",
"video_call_started": "Videohovor byl zahájen",
"video_call_using": "Videohovor pomocí:",
"voice_call": "Hlasový hovor",
"voice_call_incoming": "Příchozí hlasový hovor",

View File

@ -591,7 +591,6 @@
"video": "Fideo",
"video_room": "Ystafell fideo",
"view_message": "Gweld neges",
"voice": "Llais",
"warning": "Rhybudd"
},
"composer": {
@ -3872,7 +3871,6 @@
"connection_lost": "Mae cysylltedd â'r gweinydd wedi'i golli",
"connection_lost_description": "Allwch chi ddim osod galwadau heb gysylltiad â'r gweinydd.",
"consulting": "Ymgynghori â %(transferTarget)s. <a>Trosglwyddo i %(transferee)s</a>",
"decline_call": "Gwrthod",
"default_device": "Dyfais Rhagosodedig",
"dial": "Deial",
"dialpad": "Pad Deialu",
@ -3923,7 +3921,6 @@
"show_sidebar_button": "Dangos y bar ochr",
"silence": "Distewi galwad",
"silenced": "Hysbysiadau wedi'u distewi",
"skip_lobby_toggle_option": "Ymunwch ar unwaith",
"start_screenshare": "Dechreuwch rannu'ch sgrin",
"stop_screenshare": "Stopiwch rannu'ch sgrin",
"too_many_calls": "Gormod o Alwadau",
@ -3945,7 +3942,6 @@
"user_is_presenting": "Mae %(sharerName)s yn cyflwyno",
"video_call": "Galwad fideo",
"video_call_incoming": "Galwad fideo i mewn",
"video_call_started": "Galwad fideo wedi dechrau",
"video_call_using": "Galwad fideo gan ddefnyddio:",
"voice_call": "Galwad llais",
"voice_call_incoming": "Galwad llais i mewn",

View File

@ -3412,7 +3412,6 @@
"connection_lost": "Forbindelsen til serveren er tabt",
"connection_lost_description": "Du kan ikke lave et opkald uden en forbindelse til serveren.",
"consulting": "Konfererer med %(transferTarget)s. <a> Overfør til %(transferee)s</a>",
"decline_call": "Afvis",
"default_device": "Standardenhed",
"dial": "Ring",
"dialpad": "Tastatur",
@ -3480,7 +3479,6 @@
"user_busy_description": "Brugeren du ringede til er optaget.",
"user_is_presenting": "%(sharerName)s præsenterer",
"video_call": "Videoopkald",
"video_call_started": "Videoopkald startet",
"video_call_using": "Videoopkald med:",
"voice_call": "Stemmeopkald",
"you_are_presenting": "Du præsenterer"

View File

@ -588,7 +588,6 @@
"video": "Video",
"video_room": "Videochat",
"view_message": "Nachricht anzeigen",
"voice": "Sprachanruf",
"warning": "Warnung"
},
"composer": {
@ -3865,7 +3864,6 @@
"connection_lost": "Verbindung zum Server unterbrochen",
"connection_lost_description": "Du kannst ohne Verbindung zum Server keine Anrufe tätigen.",
"consulting": "%(transferTarget)s wird angefragt. <a>Übertragung zu %(transferee)s</a>",
"decline_call": "Ablehnen",
"default_device": "Standardgerät",
"dial": "Wählen",
"dialpad": "Telefontastatur",
@ -3916,7 +3914,6 @@
"show_sidebar_button": "Seitenleiste anzeigen",
"silence": "Anruf stummschalten",
"silenced": "Benachrichtigungen stummgeschaltet",
"skip_lobby_toggle_option": "Sofort beitreten",
"start_screenshare": "Bildschirmfreigabe starten",
"stop_screenshare": "Bildschirmfreigabe beenden",
"too_many_calls": "Zu viele Anrufe",
@ -3938,7 +3935,6 @@
"user_is_presenting": "%(sharerName)s präsentiert",
"video_call": "Videoanruf",
"video_call_incoming": "Eingehender Videoanruf",
"video_call_started": "Videoanruf hat begonnen",
"video_call_using": "Videoanruf mit:",
"voice_call": "Sprachanruf",
"voice_call_incoming": "Eingehender Anruf",

View File

@ -3164,7 +3164,6 @@
"user_busy_description": "Ο χρήστης που καλέσατε είναι απασχολημένος.",
"user_is_presenting": "%(sharerName)s παρουσιάζει",
"video_call": "Βιντεοκλήση",
"video_call_started": "Ξεκίνησε η βιντεοκλήση",
"voice_call": "Φωνητική κλήση",
"you_are_presenting": "Παρουσιάζετε"
},

View File

@ -476,6 +476,7 @@
"description": "Description",
"deselect_all": "Deselect all",
"device": "Device",
"disabled_by_homeserver": "Disabled by homeserver",
"edited": "edited",
"email_address": "Email address",
"emoji": "Emoji",
@ -1540,6 +1541,11 @@
"experimental_section": "Early previews",
"extended_profiles_msc_support": "Requires your server to support MSC4133",
"feature_disable_call_per_sender_encryption": "Disable per-sender encryption for Element Call",
"feature_user_status": {
"description": "Enables being able to see and set a current status.",
"display_name": "User status",
"required_msc_support": "Requires MSC4429 (Profile Updates for Legacy Sync)"
},
"feature_wysiwyg_composer_description": "Use rich text instead of Markdown in the message composer.",
"group_calls": "New group call experience",
"group_developer": "Developer",
@ -3147,6 +3153,14 @@
"server_error_detail": "Server unavailable, overloaded, or something else went wrong.",
"shrug": "Prepends ¯\\_(ツ)_/¯ to a plain-text message",
"spoiler": "Sends the given message as a spoiler",
"status": {
"description": "Set your current status",
"no_args": "No arguments provided. You should supply an emoij and an optional text component.",
"no_emoji": "You did not provide an emoji",
"no_text": "You did not provide any status text",
"too_long_emoji": "The first argument must be an emoji",
"too_long_text": "The text you provided was too long."
},
"tableflip": "Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message",
"topic": "Gets or sets the room topic",
"topic_none": "This room has no topic.",

View File

@ -2485,7 +2485,6 @@
"user_busy_description": "La uzanto, kiun vi vokis, estas okupata.",
"user_is_presenting": "%(sharerName)s prezentas",
"video_call": "Vidvoko",
"video_call_started": "Videovoko komenciĝis",
"voice_call": "Voĉvoko",
"you_are_presenting": "Vi prezentas"
},

View File

@ -3250,7 +3250,6 @@
"user_busy_description": "La persona a la que has llamado está ocupada.",
"user_is_presenting": "%(sharerName)s está presentando",
"video_call": "Llamada de vídeo",
"video_call_started": "Videollamada iniciada",
"voice_call": "Llamada de voz",
"you_are_presenting": "Estás presentando"
},

View File

@ -588,7 +588,6 @@
"video": "Video",
"video_room": "Videotuba",
"view_message": "Vaata sõnumit",
"voice": "Hääl",
"warning": "Hoiatus"
},
"composer": {
@ -3865,7 +3864,6 @@
"connection_lost": "Ühendus sinu serveriga on katkenud",
"connection_lost_description": "Kui ühendus sinu serveriga on katkenud, siis sa ei saa helistada.",
"consulting": "Suhtlen teise osapoolega %(transferTarget)s. <a>Saadan andmeid kasutajale %(transferee)s</a>",
"decline_call": "Keeldu",
"default_device": "Vaikimisi seade",
"dial": "Helista",
"dialpad": "Numbriklahvistik",
@ -3916,7 +3914,6 @@
"show_sidebar_button": "Näita külgpaani",
"silence": "Vaigista kõne",
"silenced": "Teavitused on summutatud",
"skip_lobby_toggle_option": "Liitu kohe",
"start_screenshare": "Alusta oma seadme ekraani jagamist",
"stop_screenshare": "Lõpeta oma seadme ekraani jagamine",
"too_many_calls": "Liiga palju kõnesid",
@ -3938,7 +3935,6 @@
"user_is_presenting": "%(sharerName)s esitab",
"video_call": "Videokõne",
"video_call_incoming": "Saabuv videokõne",
"video_call_started": "Videokõne algas",
"video_call_using": "Videokõne, kus on kasutusel:",
"voice_call": "Häälkõne",
"voice_call_incoming": "Saaduv häälkõne",

View File

@ -2187,7 +2187,6 @@
"user_busy": "کاربر مشغول",
"user_busy_description": "کاربر موردنظر مشغول است.",
"video_call": "تماس تصویری",
"video_call_started": "تماس تصویری شروع شد",
"voice_call": "تماس صوتی"
},
"web_default_device_name": "%(appName)s: %(browserName)s: روی %(osName)s",

View File

@ -3364,7 +3364,6 @@
"user_busy_description": "Käyttäjä, jolle soitit, on varattu.",
"user_is_presenting": "%(sharerName)s esittää",
"video_call": "Videopuhelu",
"video_call_started": "Videopuhelu aloitettu",
"video_call_using": "Videopuhelu käyttäen:",
"voice_call": "Äänipuhelu",
"you_are_presenting": "Esität parhaillaan"

View File

@ -588,7 +588,6 @@
"video": "Vidéo",
"video_room": "Salon vidéo",
"view_message": "Afficher le message",
"voice": "Voix",
"warning": "Attention"
},
"composer": {
@ -1368,6 +1367,14 @@
"impossible_dialog_title": "Les intégrations ne sont pas autorisées"
},
"invite": {
"confirm_unknown_users": {
"invite_subtitle": "Vous n'avez actuellement aucune discussion avec ces contacts. Veuillez confirmer leur invitation à rejoindre ce salon avant de continuer.",
"invite_title": "Inviter de nouveaux contacts dans ce salon ?",
"start_chat_subtitle_multiple_users": "Vous n'avez actuellement aucune discussion avec ces personnes. Veuillez confirmer leur invitation avant de continuer.",
"start_chat_subtitle_one_user": "Vous n'avez actuellement aucune discussion avec cette personne. Confirmez son invitation avant de continuer.",
"start_chat_title_multiple_users": "Démarrer une discussion avec ces nouveaux contacts ?",
"start_chat_title_one_user": "Démarrer une discussion avec ce nouveau contact ?"
},
"email_caption": "Inviter par e-mail",
"email_limit_one": "Les invitations par e-mail ne peuvent être envoyées quune par une",
"email_use_default_is": "Utilisez un serveur didentité pour inviter avec un e-mail. <default>Utilisez le serveur par défaut (%(defaultIdentityServerName)s)</default> ou gérez-le dans les <settings>Paramètres</settings>.",
@ -3886,6 +3893,16 @@
"call_held": "%(peerName)s a mis lappel en attente",
"call_held_resume": "Vous avez mis lappel en attente <a>Reprendre</a>",
"call_held_switch": "Vous avez mis lappel en attente <a>Basculer</a>",
"call_members": {
"exhaustive": {
"one": "<avatars/>participant à l'appel",
"other": "<avatars/>participants à l'appel"
},
"overflow": {
"one": "<avatars/>+ %(overflowCount)s participant à l'appel",
"other": "<avatars/>+ %(overflowCount)s participants à l'appel"
}
},
"call_toast_unknown_room": "Salon inconnu",
"camera_disabled": "Votre caméra est éteinte",
"camera_enabled": "Votre caméra est toujours allumée",
@ -3895,7 +3912,6 @@
"connection_lost": "La connexion au serveur a été perdue",
"connection_lost_description": "Vous ne pouvez pas passer dappels sans connexion au serveur.",
"consulting": "Consultation avec %(transferTarget)s. <a>Transfert à %(transferee)s</a>",
"decline_call": "Refuser",
"default_device": "Appareil par défaut",
"dial": "Composer",
"dialpad": "Pavé numérique",
@ -3909,10 +3925,12 @@
"enable_microphone": "Activer le microphone",
"expand": "Revenir à lappel",
"get_call_link": "Partager le lien de l'appel",
"group_call_started": "L'appel de groupe a commencé",
"hangup": "Raccrocher",
"hide_sidebar_button": "Masquer la barre latérale",
"input_devices": "Périphériques dentrée",
"jitsi_call": "Conférence Jitsi",
"join_with_video": "Rejoigner en vidéo",
"legacy_call": "Appel vidéo",
"maximise": "Remplir lécran",
"maximise_call": "Plein écran",
@ -3946,7 +3964,6 @@
"show_sidebar_button": "Afficher la barre latérale",
"silence": "Mettre lappel en sourdine",
"silenced": "Notifications silencieuses",
"skip_lobby_toggle_option": "Rejoignez immédiatement",
"start_screenshare": "Commencer à partager mon écran",
"stop_screenshare": "Arrêter de partager mon écran",
"too_many_calls": "Trop dappels",
@ -3968,7 +3985,6 @@
"user_is_presenting": "%(sharerName)s est à lécran",
"video_call": "Appel vidéo",
"video_call_incoming": "Appel vidéo entrant",
"video_call_started": "Appel vidéo commencé",
"video_call_using": "Appel vidéo utilisant :",
"voice_call": "Appel audio",
"voice_call_incoming": "Appel vocal entrant",
@ -3976,6 +3992,11 @@
"you_are_presenting": "Vous êtes à lécran"
},
"web_default_device_name": "%(appName)s : %(browserName)s pour %(osName)s",
"welcome": {
"tagline_element": "Conçu pour la vitesse et la simplicité.",
"title_element": "Soyez dans votre Element",
"title_generic": "Bienvenue sur %(brand)s"
},
"widget": {
"added_by": "Widget ajouté par",
"capabilities_dialog": {

View File

@ -593,7 +593,6 @@
"video": "Videozapis",
"video_room": "Videosoba",
"view_message": "Prikaži poruku",
"voice": "Glas",
"warning": "Upozorenje"
},
"composer": {
@ -3951,7 +3950,6 @@
"connection_lost": "Veza s poslužiteljem je prekinuta",
"connection_lost_description": "Ne možete upućivati pozive ako niste povezani s poslužiteljem.",
"consulting": "Savjetovanje s %(transferTarget)s. <a>Prijenos na %(transferee)s</a>",
"decline_call": "Odbij",
"default_device": "Zadani uređaj",
"dial": "Biranje",
"dialpad": "Brojčanik",
@ -4003,7 +4001,6 @@
"show_sidebar_button": "Prikaži bočnu traku",
"silence": "Utišaj poziv",
"silenced": "Obavijesti su utišane",
"skip_lobby_toggle_option": "Pridruži se odmah",
"start_screenshare": "Započni dijeljenje zaslona",
"stop_screenshare": "Zaustavi dijeljenje zaslona",
"too_many_calls": "Previše poziva",
@ -4025,7 +4022,6 @@
"user_is_presenting": "%(sharerName)s dijeli zaslon",
"video_call": "Videopoziv",
"video_call_incoming": "Dolazni videopoziv",
"video_call_started": "Videopoziv je započeo",
"video_call_using": "Videopoziv putem:",
"voice_call": "Glasovni poziv",
"voice_call_incoming": "Dolazni glasovni poziv",

View File

@ -586,7 +586,6 @@
"video": "Videó",
"video_room": "Videószoba",
"view_message": "Üzenet megjelenítése",
"voice": "Hang",
"warning": "Figyelmeztetés"
},
"composer": {
@ -3860,7 +3859,6 @@
"connection_lost": "Megszakadt a kapcsolat a kiszolgálóval",
"connection_lost_description": "Nem kezdeményezhet hívást a kiszolgálóval való kapcsolat nélkül.",
"consulting": "Egyeztetés vele: %(transferTarget)s. <a>Átadás ide: %(transferee)s</a>",
"decline_call": "Elutasítás",
"default_device": "Alapértelmezett eszköz",
"dial": "Tárcsázás",
"dialpad": "Tárcsázó",
@ -3910,7 +3908,6 @@
"show_sidebar_button": "Oldalsáv megjelenítése",
"silence": "Hívás némítása",
"silenced": "Értesítések némítva",
"skip_lobby_toggle_option": "Csatlakozás azonnal",
"start_screenshare": "Képernyőmegosztás bekapcsolása",
"stop_screenshare": "Képernyőmegosztás kikapcsolása",
"too_many_calls": "Túl sok hívás",
@ -3932,7 +3929,6 @@
"user_is_presenting": "%(sharerName)s tartja a bemutatót",
"video_call": "Videóhívás",
"video_call_incoming": "Bejövő videóhívás",
"video_call_started": "A videóhívás elindult",
"video_call_using": "Videóhívás:",
"voice_call": "Hanghívás",
"voice_call_incoming": "Bejövő hanghívás",

View File

@ -3839,7 +3839,6 @@
"user_busy_description": "Ձեր զանգահարած օգտատերը զբաղված է։",
"user_is_presenting": "%(sharerName)s-ը ներկայացնում է",
"video_call": "Տեսազանգ",
"video_call_started": "Տեսազանգը սկսվեց",
"video_call_using": "Տեսազանգ՝ օգտագործելով՝",
"voice_call": "Ձայնային զանգ",
"you_are_presenting": "Դուք ներկայացնում եք"

View File

@ -588,7 +588,6 @@
"video": "Video",
"video_room": "Ruangan video",
"view_message": "Tampilkan pesan",
"voice": "Suara",
"warning": "Peringatan"
},
"composer": {
@ -3852,7 +3851,6 @@
"connection_lost": "Koneksi ke server telah hilang",
"connection_lost_description": "Anda tidak dapat membuat panggilan tanpa terhubung ke server.",
"consulting": "Mengkonsultasi dengan %(transferTarget)s. <a>Transfer ke %(transferee)s</a>",
"decline_call": "Tolak",
"default_device": "Perangkat Bawaan",
"dial": "Panggil",
"dialpad": "Tombol Penyetel",
@ -3903,7 +3901,6 @@
"show_sidebar_button": "Tampilkan sisi bilah",
"silence": "Diamkan panggilan",
"silenced": "Notifikasi dibisukan",
"skip_lobby_toggle_option": "Segera bergabung",
"start_screenshare": "Mulai membagikan layar Anda",
"stop_screenshare": "Berhenti membagikan layar Anda",
"too_many_calls": "Terlalu Banyak Panggilan",
@ -3925,7 +3922,6 @@
"user_is_presenting": "%(sharerName)s sedang mempresentasi",
"video_call": "Panggilan video",
"video_call_incoming": "Panggilan video masuk",
"video_call_started": "Panggilan video dimulai",
"video_call_using": "Panggilan video menggunakan:",
"voice_call": "Panggilan suara",
"voice_call_incoming": "Panggilan suara masuk",

View File

@ -2824,7 +2824,6 @@
"user_busy_description": "Notandinn sem þú hringdir í er upptekinn.",
"user_is_presenting": "%(sharerName)s er að kynna",
"video_call": "Myndsímtal",
"video_call_started": "Myndsímtal er byrjað",
"voice_call": "Raddsímtal",
"you_are_presenting": "Þú ert að kynna"
},

View File

@ -3412,7 +3412,6 @@
"user_busy_description": "L'utente che hai chiamato è occupato.",
"user_is_presenting": "%(sharerName)s sta presentando",
"video_call": "Videochiamata",
"video_call_started": "Videochiamata iniziata",
"video_call_using": "Videochiamata usando:",
"voice_call": "Telefonata",
"you_are_presenting": "Stai presentando"

View File

@ -3136,7 +3136,6 @@
"user_busy_description": "呼び出したユーザーは通話中です。",
"user_is_presenting": "%(sharerName)sが画面を共有しています",
"video_call": "ビデオ通話",
"video_call_started": "ビデオ通話を開始しました",
"voice_call": "音声通話",
"you_are_presenting": "あなたが画面を共有しています"
},

View File

@ -2669,7 +2669,6 @@
"user_busy_description": "მომხმარებელი, რომელსაც დაურეკე, დაკავებულია.",
"user_is_presenting": "%(sharerName)sწარმოადგენს",
"video_call": "ვიდეო ზარი",
"video_call_started": "ვიდეო ზარი დაიწყო",
"voice_call": "ხმოვანი ზარი",
"you_are_presenting": "წარმოგიდგენთ"
},

View File

@ -583,7 +583,6 @@
"video": "비디오",
"video_room": "비디오 room",
"view_message": "메시지 보기",
"voice": "음성",
"warning": "경고"
},
"composer": {
@ -3785,7 +3784,6 @@
"connection_lost": "서버와의 연결이 끊어졌습니다.",
"connection_lost_description": "서버에 연결되지 않으면 전화를 걸 수 없습니다.",
"consulting": "%(transferTarget)s와 상담 중입니다. <a>%(transferee)s에게 통화 이전</a>",
"decline_call": "거부",
"default_device": "기본 기기",
"dial": "전화걸기",
"dialpad": "다이얼패드",
@ -3835,7 +3833,6 @@
"show_sidebar_button": "사이드바 표시",
"silence": "통화 음소거",
"silenced": "알림 음소거됨",
"skip_lobby_toggle_option": "즉시 가입하세요",
"start_screenshare": "화면 공유를 시작하세요",
"stop_screenshare": "화면 공유 중지",
"too_many_calls": "전화가 너무 많아요",
@ -3857,7 +3854,6 @@
"user_is_presenting": "%(sharerName)s님이 화면을 공유중입니다.",
"video_call": "영상 통화",
"video_call_incoming": "수신 영상 통화",
"video_call_started": "영상통화가 시작되었습니다",
"video_call_using": "사용 중인 영상 통화:",
"voice_call": "음성 통화",
"voice_call_incoming": "수신 음성 통화",

View File

@ -94,7 +94,7 @@
"show_advanced": "Rodyti išplėstinius",
"show_all": "Rodyti viską",
"sign_in": "Prisijungti",
"sign_out": "Atsijungti",
"sign_out": "Šalinti šį įrenginį",
"skip": "Praleisti",
"start": "Pradėti",
"start_chat": "Pradėti pokalbį",
@ -208,7 +208,7 @@
"set_email": {
"description": "Tai jums leis iš naujo nustatyti slaptažodį ir gauti pranešimus.",
"verification_pending_description": "Patikrinkite savo el. laišką ir spustelėkite jame esančią nuorodą. Kai tai padarysite, spauskite tęsti.",
"verification_pending_title": "Laukiama Patikrinimo"
"verification_pending_title": "Laukiama patvirtinimo"
},
"set_email_prompt": "Ar norite nustatyti el. pašto adresą?",
"sign_in_instead_prompt": "Jau turite paskyrą? <a>Prisijunkite čia</a>",
@ -402,8 +402,13 @@
"format_code_block": "Kodo blokas",
"format_inline_code": "Kodas",
"format_insert_link": "Įterpti nuorodą",
"format_italic": "Kursyvas",
"format_italics": "Kursyvas",
"format_link": "Nuoroda",
"format_ordered_list": "Sunumeruotas sąrašas",
"format_strikethrough": "Perbrauktas",
"format_underline": "Pabraukimas",
"format_unordered_list": "Suženklintasis sąrašas",
"no_perms_notice": "Jūs neturite leidimų rašyti šiame kambaryje",
"placeholder": "Siųsti žinutę…",
"placeholder_encrypted": "Siųsti šifruotą žinutę…",
@ -424,6 +429,8 @@
"voice_message_button": "Balso žinutė"
},
"create_room": {
"action_create_room": "Kurti kambarį",
"action_create_video_room": "Kurti vaizdo kambarį",
"encryption_forced": "Jūsų serveris reikalauja, kad šifravimas būtų įjungtas privačiuose kambariuose.",
"encryption_label": "Įjungti visapusį šifravimą",
"error_title": "Nepavyko sukurti kambario",
@ -431,6 +438,7 @@
"name_validation_required": "Įveskite kambario pavadinimą",
"title_private_room": "Sukurti privatų kambarį",
"title_public_room": "Sukurti viešą kambarį",
"title_video_room": "Kurti vaizdo pokalbių kambarį",
"topic_label": "Tema (nebūtina)",
"unfederated": "Blokuoti bet ką, kas nėra iš %(serverName)s, niekada nebeleidžiant prisijungti prie šio kambario.",
"unfederated_label_default_off": "Jūs galite tai įjungti, jei kambarys bus naudojamas tik bendradarbiavimui su vidinėmis komandomis jūsų serveryje. Tai negali būti vėliau pakeista.",
@ -461,16 +469,33 @@
"event_content": "Įvykio turinys",
"event_sent": "Įvykis išsiųstas!",
"event_type": "Įvykio tipas",
"explore_account_data": "Naršyti paskyros duomenis",
"explore_room_account_data": "Naršyti kambario paskyros duomenis",
"explore_room_state": "Naršyti kambario būseną",
"failed_to_find_widget": "Įvyko klaida ieškant šio valdiklio.",
"failed_to_send": "Nepavyko išsiųsti įvykio.",
"invalid_json": "Neatrodo kaip tinkamas JSON.",
"level": "Lygis",
"no_receipt_found": "Nerasta kvito.",
"notifications_debug": "Pranešimų derinimas",
"original_event_source": "Originalus įvykio šaltinis",
"room_id": "Kambario ID: %(roomId)s",
"send_custom_account_data_event": "Siųsti pasirinktinius paskyros duomenis įvykį",
"send_custom_room_account_data_event": "Siųsti pasirinktinius kambario paskyros duomenis įvykį",
"send_custom_timeline_event": "Siųsti pasirinktinės laiko skalės įvykį",
"server_info": "Serverio informacija",
"setting_colon": "Nustatymas:",
"setting_id": "Nustatymo ID",
"settings_explorer": "Nustatymų naršyklė",
"show_hidden_events": "Rodyti paslėptus įvykius laiko juostoje",
"state_key": "Būklės raktas",
"thread_root_id": "Gijos šaknies ID: %(threadRootId)s",
"title": "Kūrėjo įrankiai",
"toolbox": "Įrankinė",
"user_read_up_to": "Naudotojas perskaitė iki: ",
"user_read_up_to_ignore_synthetic": "Naudotojas perskaitė iki (ignoreSynthetic): ",
"value": "Reikšmė",
"view_servers_in_room": "Peržiūrėti serverius kambaryje",
"widget_screenshots": "Įjungti valdiklių ekrano kopijas palaikomuose valdikliuose"
},
"dialog_close_label": "Uždaryti dialogą",
@ -581,7 +606,7 @@
"unverified_session_toast_title": "Naujas prisijungimas. Ar tai jūs?",
"unverified_sessions_toast_description": "Peržiūrėkite, ar jūsų paskyra yra saugi",
"unverified_sessions_toast_reject": "Vėliau",
"verification_dialog_title_user": "Patikrinimo Užklausa",
"verification_dialog_title_user": "Patvirtinimo prašymas",
"verify_emoji": "Patvirtinti naudojant jaustukus",
"verify_emoji_prompt": "Patvirtinti palyginant unikalius jaustukus.",
"verify_emoji_prompt_qr": "Jei nuskaityti aukščiau esančio kodo negalite, patvirtinkite palygindami unikalius jaustukus.",
@ -591,7 +616,8 @@
"waiting_other_user": "Laukiama kol %(displayName)s patvirtins…"
},
"verify_toast_description": "Kiti vartotojai gali nepasitikėti",
"verify_toast_title": "Patvirtinti šį seansą"
"verify_toast_title": "Patvirtinti šį seansą",
"withdraw_verification_action": "Atšaukti patvirtinimą"
},
"error": {
"admin_contact_short": "Susisiekite su savo <a>serverio administratoriumi</a>.",
@ -798,7 +824,9 @@
},
"keyboard": {
"activate_button": "Aktyvuoti pasirinktą mygtuką",
"alt": "Alt",
"autocomplete_cancel": "Atšaukti automatinį užbaigimą",
"backspace": "Naikinimo klavišas",
"cancel_reply": "Atšaukti atsakymą į žinutę",
"category_autocomplete": "Autorašymas",
"category_calls": "Skambučiai",
@ -809,29 +837,42 @@
"composer_toggle_bold": "Perjungti paryškinimą",
"composer_toggle_italics": "Perjungti kursyvą",
"composer_toggle_quote": "Perjungti citatą",
"control": "Vald",
"dismiss_read_marker_and_jump_bottom": "Atsisakyti skaitymo žymeklio ir nušokti į apačią",
"end": "Pab",
"enter": "Įvesti",
"escape": "Gr",
"home": "Pradžia",
"jump_room_search": "Nušokti į kambarių paiešką",
"jump_to_read_marker": "Nušokti iki seniausios neperskaitytos žinutės",
"number": "[skaičius]",
"page_down": "Puslapis žemyn",
"page_up": "Puslapis aukštyn",
"room_list_collapse_section": "Sutraukti kambarių sąrašo skyrių",
"room_list_expand_section": "Išplėsti kambarių sąrašo skyrių",
"room_list_select_room": "Pasirinkti kambarį iš kambarių sąrašo",
"search": "Paieška (turi būti įjungta)",
"shift": "Lyg2",
"space": "Tarpas",
"toggle_microphone_mute": "Perjungti mikrofono nutildymą",
"toggle_right_panel": "Perjungti dešinį skydelį",
"toggle_top_left_menu": "Perjungti viršutinį kairės pusės meniu",
"upload_file": "Įkelti failą"
},
"labs": {
"ask_to_join": "Įjungti prašymą jungtis",
"beta_feedback_leave_button": "Norėdami išeiti iš beta versijos, apsilankykite savo nustatymuose.",
"bridge_state": "Rodyti informaciją apie tiltus kambario nustatymuose",
"bridge_state_channel": "Kanalas: <channelLink/>",
"bridge_state_creator": "Šis tiltas buvo parūpintas <user />.",
"bridge_state_manager": "Šis tiltas yra tvarkomas <user />.",
"bridge_state_workspace": "Darbo aplinka: <networkLink/>",
"currently_experimental": "Šiuo metu eksperimentinė.",
"custom_themes": "Palaikykite pridėdami pasirinktines temas",
"dynamic_room_predecessors": "Dinaminiai kambario pirmtakai",
"element_call_video_rooms": "Element skambučio vaizdo kambariai",
"feature_wysiwyg_composer_description": "Naudoti raiškųjį tekstą vietoj ženklinimo žinučių rengyklėje.",
"group_calls": "Nauja grupinio skambučio patirtis",
"group_developer": "Kūrėjas",
"group_encryption": "Šifravimas",
"group_experimental": "Eksperimentinis",
@ -843,13 +884,21 @@
"group_themes": "Temos",
"group_voip": "Garsas ir Vaizdas",
"group_widgets": "Valdikliai",
"hidebold": "Slėpti pranešimų tašką (rodyti tik skaičių žymes)",
"html_topic": "Rodyti kambarių temų HTML atvaizdavimą",
"jump_to_date": "Pereiti prie datos (prideda /jumptodate ir perėjimo prie datos antraštes)",
"jump_to_date_msc_support": "Privaloma, kad jūsų serveris palaikytų MSC3030.",
"latex_maths": "Atvaizduoti LaTeX matematikas žinutėse",
"leave_beta": "Palikti beta versiją",
"location_share_live": "Tiesioginis vietos bendrinimas",
"location_share_live_description": "Laikinas įgyvendinimas. Vietos išlieka kambario istorijoje.",
"mjolnir": "Nauji būdai ignoruoti žmones",
"msc3531_hide_messages_pending_moderation": "Leisti moderatoriams slėpti žinutes, laukiančias moderavimo.",
"notification_settings": "Nauji pranešimų nustatymai",
"report_to_moderators": "Pranešti prižiūrėtojams",
"sliding_sync": "Slankiojo sinchronizavimo režimas",
"sliding_sync_description": "Aktyviai kuriama, negalima išjungti. Šiuo metu nesuderinama su „Element“ skambučiais.",
"under_active_development": "Šiuo metu aktyviai kuriama.",
"video_rooms": "Vaizdo kambariai",
"video_rooms_a_new_way_to_chat": "Naujas būdas kalbėtis balsu ir vaizdu per %(brand)s.",
"video_rooms_always_on_voip_channels": "Vaizdo kambariai - tai visada veikiantys VoIP kanalai, įterpti į %(brand)s kambarį.",
@ -951,6 +1000,7 @@
},
"powered_by_matrix": "Veikia su Matrix",
"presence": {
"away": "Pasitraukę",
"busy": "Užsiėmęs",
"idle": "Neveiklus",
"idle_for": "Neveiklus %(duration)s",
@ -1414,8 +1464,10 @@
"big_emoji": "Įjungti didelius jaustukus pokalbiuose",
"code_block_expand_default": "Išplėsti kodo blokus pagal nutylėjimą",
"code_block_line_numbers": "Rodyti eilučių numerius kodo blokuose",
"disable_historical_profile": "Rodyti dabartinę profilio nuotrauką ir vardą naudotojams žinučių istorijoje",
"emoji_autocomplete": "Įjungti Jaustukų pasiūlymus rašant",
"enable_markdown": "Įjungti Markdown",
"enable_markdown_description": "Pradėkite žinutes su <code>/plain</code> norint siųsti be ženklinimo.",
"general": {
"account_management_section": "Paskyros tvarkymas",
"account_section": "Paskyra",
@ -1466,7 +1518,7 @@
"remove_email_prompt": "Pašalinti %(email)s?",
"remove_msisdn_prompt": "Pašalinti %(phone)s?"
},
"inline_url_previews_default": "Įjungti URL nuorodų peržiūras kaip numatytasias",
"inline_url_previews_default": "Įjungti peržiūras",
"insert_trailing_colon_mentions": "Įterpti dvitaškį po naudotojo paminėjimų žinutės pradžioje",
"jump_to_bottom_on_send": "Peršokti į laiko juostos apačią, kai siunčiate žinutę",
"key_backup": {
@ -1624,10 +1676,12 @@
"verified_sessions_list_description": "Geriausiam saugumui, atsijunkite iš bet kurios sesijos, kurios neatpažįstate arba nebenaudojate.",
"verify_session": "Patvirtinti seansą"
},
"show_avatar_changes": "Rodyti profilio nuotraukos pakeitimus",
"show_breadcrumbs": "Rodyti neseniai peržiūrėtų kambarių nuorodas virš kambarių sąrašo",
"show_chat_effects": "Rodyti pokalbių efektus (animaciją, kai gaunate, pvz., konfeti)",
"show_displayname_changes": "Rodyti rodomo vardo pakeitimus",
"show_join_leave": "Rodyti prisijungimo/išėjimo žinutes (kvietimai/pašalinimai/blokavimai neturi įtakos)",
"show_nsfw_content": "Rodyti NSD turinį",
"show_read_receipts": "Rodyti kitų vartotojų siųstus perskaitymo kvitus",
"show_redaction_placeholder": "Rodyti pašalintų žinučių žymeklį",
"show_stickers_button": "Rodyti lipdukų mygtuką",
@ -1905,8 +1959,8 @@
"m.room.member": {
"accepted_3pid_invite": "%(targetName)s priėmė kvietimą, skirtą %(displayName)s",
"accepted_invite": "%(targetName)s priėmė kvietimą",
"ban": "%(senderName)s uždraudė %(targetName)s",
"ban_reason": "%(senderName)s uždraudė %(targetName)s%(targetName)s: %(reason)s",
"ban": "%(senderName)s uždraudė naudotoją",
"ban_reason": "%(senderName)s uždraudė naudotoją: %(reason)s",
"change_avatar": "%(senderName)s pakeitė savo profilio nuotrauką",
"change_name": "%(oldDisplayName)s pasikeitė savo rodomą vardą į %(displayName)s",
"change_name_avatar": "%(oldDisplayName)s pakeitė savo rodomą vardą ir profilio nuotrauką",
@ -1994,7 +2048,8 @@
"pending_moderation": "Žinutė laukia moderavimo",
"pending_moderation_reason": "Žinutė laukia moderavimo: %(reason)s",
"reactions": {
"add_reaction_prompt": "Pridėti reakciją"
"add_reaction_prompt": "Pridėti reakciją",
"label": "%(reactors)s sureagavo su %(content)s"
},
"read_receipt_title": {
"one": "Matė %(count)s žmogus",

View File

@ -3354,7 +3354,6 @@
"user_busy_description": "Lietotājs, kuram zvanāt, ir aizņemts.",
"user_is_presenting": "%(sharerName)s prezentē",
"video_call": "Video zvans",
"video_call_started": "Videozvans uzsākts",
"voice_call": "Balss zvans",
"you_are_presenting": "Jūs prezentējat"
},

View File

@ -3394,7 +3394,6 @@
"user_busy_description": "Mbola Sahirana ny mpampiasa niantsoanao.",
"user_is_presenting": "%(sharerName)sIzao",
"video_call": "Antso an-tsary",
"video_call_started": "Manomboka ny antso an-tsary",
"video_call_using": "Antso an-tsary mampiasa:",
"voice_call": "Antso an-tariby",
"you_are_presenting": "Mamkahalala anareo"

View File

@ -588,7 +588,6 @@
"video": "Video",
"video_room": "Videorom",
"view_message": "Se melding",
"voice": "Tale",
"warning": "Advarsel"
},
"composer": {
@ -3859,7 +3858,6 @@
"connection_lost": "Mistet forbindelsen til serveren",
"connection_lost_description": "Du kan ikke ringe uten tilkobling til serveren.",
"consulting": "Rådføring med %(transferTarget)s. <a>Overfør til %(transferee)s</a>",
"decline_call": "Avslå",
"default_device": "Standardenhet",
"dial": "Ring",
"dialpad": "Nummerpanel",
@ -3910,7 +3908,6 @@
"show_sidebar_button": "Vis sidepanel",
"silence": "Demp samtale",
"silenced": "Varslinger er dempet",
"skip_lobby_toggle_option": "Bli med umiddelbart",
"start_screenshare": "Begynn å dele skjermen din",
"stop_screenshare": "Slutt å dele skjermen din",
"too_many_calls": "For mange samtaler",
@ -3932,7 +3929,6 @@
"user_is_presenting": "%(sharerName)s presenterer",
"video_call": "Videosamtale",
"video_call_incoming": "Innkommende videosamtale",
"video_call_started": "Videosamtale startet",
"video_call_using": "Videosamtale ved hjelp av:",
"voice_call": "Stemmesamtale",
"voice_call_incoming": "Innkommende taleanrop",

View File

@ -2960,7 +2960,6 @@
"user_busy_description": "De persoon die je belde is bezet.",
"user_is_presenting": "%(sharerName)s is aan het presenteren",
"video_call": "Video-oproep",
"video_call_started": "Videogesprek gestart",
"voice_call": "Spraakoproep",
"you_are_presenting": "Je bent aan het presenteren"
},

View File

@ -3829,7 +3829,6 @@
"connection_lost": "Połączenie z serwerem zostało przerwane",
"connection_lost_description": "Nie możesz wykonywać rozmów bez połączenia z serwerem.",
"consulting": "Konsultowanie z %(transferTarget)s. <a>Transfer do %(transferee)s</a>",
"decline_call": "Odrzuć",
"default_device": "Urządzenie domyślne",
"dial": "Wybierz numer",
"dialpad": "Klawiatura telefoniczna",
@ -3881,7 +3880,6 @@
"show_sidebar_button": "Pokaż pasek boczny",
"silence": "Wycisz rozmowę",
"silenced": "Wyciszono powiadomienia",
"skip_lobby_toggle_option": "Dołącz teraz",
"start_screenshare": "Udostępnij ekran",
"stop_screenshare": "Przestań udostępniać ekran",
"too_many_calls": "Zbyt wiele połączeń",
@ -3902,7 +3900,6 @@
"user_busy_description": "Użytkownik, do którego zadzwoniłeś jest zajęty.",
"user_is_presenting": "%(sharerName)s prezentuje",
"video_call": "Rozmowa wideo",
"video_call_started": "Rozpoczęto rozmowę wideo",
"video_call_using": "Połączenie wideo przy użyciu:",
"voice_call": "Rozmowa głosowa",
"you_are_presenting": "Prezentujesz"

View File

@ -3750,7 +3750,6 @@
"user_busy_description": "O utilizador para o qual tentou ligar está ocupado.",
"user_is_presenting": "%(sharerName)s está apresentando",
"video_call": "Chamada de vídeo",
"video_call_started": "Chamada de vídeo iniciada",
"video_call_using": "Video-chamada usando:",
"voice_call": "Chamada de voz",
"you_are_presenting": "Estás a apresentar"

View File

@ -587,7 +587,6 @@
"video": "Vídeo",
"video_room": "Sala de vídeo",
"view_message": "Ver mensagem",
"voice": "Voz",
"warning": "Atenção"
},
"composer": {
@ -3834,7 +3833,6 @@
"connection_lost": "A conectividade com o servidor foi perdida",
"connection_lost_description": "Você não pode fazer chamadas sem uma conexão com o servidor.",
"consulting": "Consultar com %(transferTarget)s. Tranferir para <a> %(transferee)s</a>",
"decline_call": "Recusar",
"default_device": "Aparelho padrão",
"dial": "Discar",
"dialpad": "Teclado de discagem",
@ -3885,7 +3883,6 @@
"show_sidebar_button": "Exibir a barra lateral",
"silence": "Silenciar chamado",
"silenced": "Notificações silenciadas",
"skip_lobby_toggle_option": "Junte-se imediatamente",
"start_screenshare": "Começar a compartilhar sua tela",
"stop_screenshare": "Parar de compartilhar sua tela",
"too_many_calls": "Muitas chamadas",
@ -3907,7 +3904,6 @@
"user_is_presenting": "%(sharerName)s está apresentando",
"video_call": "Chamada de vídeo",
"video_call_incoming": "Chamada de vídeo recebida",
"video_call_started": "Videochamada iniciada",
"video_call_using": "Chamada de vídeo usando:",
"voice_call": "Chamada de voz",
"voice_call_incoming": "Chamada de voz recebida",

View File

@ -589,7 +589,6 @@
"video": "Видео",
"video_room": "Видеочат",
"view_message": "Посмотреть сообщение",
"voice": "Голос",
"warning": "Внимание"
},
"composer": {
@ -681,6 +680,11 @@
"unfederated_label_default_on": "Вы можете отключить это, если комната будет использоваться для совместной работы с внешними командами, у которых есть собственный домашний сервер. Это не может быть изменено позже.",
"unsupported_version": "Сервер не поддерживает указанную версию чата."
},
"create_section_dialog": {
"create_section": "Новый раздел",
"label": "Название раздела",
"title": "Новый раздел"
},
"create_space": {
"add_details_prompt": "Добавьте некоторые подробности, чтобы помочь людям узнать его.",
"add_details_prompt_2": "Вы можете изменить их в любое время.",
@ -3900,7 +3904,6 @@
"connection_lost": "Соединение с сервером потеряно",
"connection_lost_description": "Вы не можете совершать вызовы без подключения к серверу.",
"consulting": "Общение с %(transferTarget)s. <a>Перевод на %(transferee)s</a>",
"decline_call": "Отклонить",
"default_device": "Устройство по умолчанию",
"dial": "Набор",
"dialpad": "Панель набора номера",
@ -3951,7 +3954,6 @@
"show_sidebar_button": "Показать боковую панель",
"silence": "Тихий вызов",
"silenced": "Оповещения приглушены",
"skip_lobby_toggle_option": "Присоединиться прямо сейчас",
"start_screenshare": "Начать делиться экраном",
"stop_screenshare": "Перестать делиться экраном",
"too_many_calls": "Слишком много звонков",
@ -3973,7 +3975,6 @@
"user_is_presenting": "%(sharerName)s показывает",
"video_call": "Видеовызов",
"video_call_incoming": "Входящий видеозвонок",
"video_call_started": "Начался видеозвонок",
"video_call_using": "Видеозвонок с использованием:",
"voice_call": "Голосовой вызов",
"voice_call_incoming": "Входящий голосовой вызов",

View File

@ -593,7 +593,6 @@
"video": "Video",
"video_room": "Video miestnosť",
"view_message": "Zobraziť správu",
"voice": "Hlas",
"warning": "Upozornenie"
},
"composer": {
@ -3938,7 +3937,6 @@
"connection_lost": "Spojenie so serverom bolo prerušené",
"connection_lost_description": "Bez pripojenia k serveru nie je možné uskutočňovať hovory.",
"consulting": "Konzultovanie s %(transferTarget)s. <a>Presmerovanie na %(transferee)s</a>",
"decline_call": "Zamietnuť",
"default_device": "Predvolené zariadenie",
"dial": "Vytočiť číslo",
"dialpad": "Číselník",
@ -3990,7 +3988,6 @@
"show_sidebar_button": "Zobraziť bočný panel",
"silence": "Stlmiť hovor",
"silenced": "Oznámenia stlmené",
"skip_lobby_toggle_option": "Pripojiť sa okamžite",
"start_screenshare": "Spustiť zdieľanie vašej obrazovky",
"stop_screenshare": "Zastaviť zdieľanie vašej obrazovky",
"too_many_calls": "Príliš veľa hovorov",
@ -4012,7 +4009,6 @@
"user_is_presenting": "%(sharerName)s prezentuje",
"video_call": "Video hovor",
"video_call_incoming": "Prichádzajúci videohovor",
"video_call_started": "Videohovor bol spustený",
"video_call_using": "Videohovor pomocou:",
"voice_call": "Hlasový hovor",
"voice_call_incoming": "Prichádzajúci hlasový hovor",

View File

@ -3196,7 +3196,6 @@
"user_busy_description": "Përdoruesi që thirrët është i zënë.",
"user_is_presenting": "%(sharerName)s përfaqëson",
"video_call": "Thirrje video",
"video_call_started": "Nisi thirrje me video",
"voice_call": "Thirrje audio",
"you_are_presenting": "Përfaqësoni"
},

View File

@ -3836,7 +3836,6 @@
"user_busy_description": "Användaren du ringde är upptagen.",
"user_is_presenting": "%(sharerName)s presenterar",
"video_call": "Videosamtal",
"video_call_started": "Videosamtal startat",
"video_call_using": "Videosamtal med hjälp av:",
"voice_call": "Röstsamtal",
"you_are_presenting": "Du presenterar"

View File

@ -3736,7 +3736,6 @@
"user_busy_description": "Aradığınız kullanıcı meşgul.",
"user_is_presenting": "%(sharerName)s sunum yapıyor",
"video_call": "Görüntülü arama",
"video_call_started": "Görüntülü arama başlatıldı",
"video_call_using": "Görüntülü arama kullanılıyor:",
"voice_call": "Sesli arama",
"you_are_presenting": "Sunum yapıyorsunuz"

View File

@ -589,7 +589,6 @@
"video": "Відео",
"video_room": "Відеокімната",
"view_message": "Переглянути повідомлення",
"voice": "Голос",
"warning": "Попередження"
},
"composer": {
@ -3870,7 +3869,6 @@
"connection_lost": "Втрачено зʼєднання з сервером",
"connection_lost_description": "Неможливо здійснювати виклики без з'єднання з сервером.",
"consulting": "Консультація з %(transferTarget)s. <a>Переадресація на %(transferee)s</a>",
"decline_call": "Відхилити",
"default_device": "Уставний пристрій",
"dial": "Виклик",
"dialpad": "Номеронабирач",
@ -3922,7 +3920,6 @@
"show_sidebar_button": "Показати бічну панель",
"silence": "Тихий виклик",
"silenced": "Сповіщення стишено",
"skip_lobby_toggle_option": "Приєднатися негайно",
"start_screenshare": "Почати показ екрана",
"stop_screenshare": "Вимкнути показ екрана",
"too_many_calls": "Забагато викликів",
@ -3944,7 +3941,6 @@
"user_is_presenting": "%(sharerName)s показує",
"video_call": "Відеовиклик",
"video_call_incoming": "Вхідний відеовиклик",
"video_call_started": "Відеовиклик розпочато",
"video_call_using": "Відеодзвінок за допомогою:",
"voice_call": "Голосовий виклик",
"voice_call_incoming": "Вхідний голосовий виклик",

View File

@ -3137,7 +3137,6 @@
"user_busy_description": "Người dùng bạn vừa gọi hiện đang bận.",
"user_is_presenting": "%(sharerName)s đang trình bày",
"video_call": "Gọi video",
"video_call_started": "Cuộc gọi truyền hình đã bắt đầu",
"voice_call": "Gọi thoại",
"you_are_presenting": "Bạn đang trình bày"
},

View File

@ -219,7 +219,7 @@
"incorrect_password": "密码不正确",
"log_in_new_account": "<a>登录</a>到你的新账户。",
"logout_dialog": {
"description": "你确定要注销吗",
"description": "你确定要移除此设备",
"megolm_export": "手动导出密钥",
"setup_key_backup_title": "你将失去加密消息的访问权",
"setup_secure_backup_description_1": "加密消息采用端到端加密技术确保安全。只有你与收件人拥有读取这些消息的密钥。",
@ -588,7 +588,6 @@
"video": "视频",
"video_room": "视频房间",
"view_message": "查看消息",
"voice": "语音",
"warning": "警告"
},
"composer": {
@ -684,8 +683,10 @@
"create_section_dialog": {
"create_section": "创建区域",
"description": "区域仅对你可见",
"edit_section": "编辑区域",
"label": "区域名称",
"title": "创建区域"
"title": "创建区域",
"title_edition": "编辑区域"
},
"create_space": {
"add_details_prompt": "添加一些信息以便人们识别。",
@ -1116,7 +1117,7 @@
"waiting_other_device_details": "等待你在另一台设备上验证,%(deviceName)s%(deviceId)s…",
"waiting_other_user": "正在等待 %(displayName)s 验证…"
},
"verification_requested_toast_title": "请求验证",
"verification_requested_toast_title": "请求验证",
"verified_identity_changed": "%(displayName)s (<b>%(userId)s</b>) 的数字身份已重置。<a>了解更多</a>",
"verified_identity_changed_no_displayname": "<b>%(userId)s</b>的数字身份已重置。<a>了解更多</a>",
"verify_toast_description": "可能不受其他用户信任",
@ -1433,7 +1434,7 @@
},
"keyboard": {
"activate_button": "激活选择的按钮",
"alt": "Alt",
"alt": "",
"autocomplete_cancel": "取消自动补全",
"autocomplete_force": "强制完成",
"autocomplete_navigate_next": "下一个自动补全建议",
@ -1457,11 +1458,11 @@
"composer_toggle_link": "切换链接",
"composer_toggle_quote": "切换引用",
"composer_undo": "撤消编辑",
"control": "Ctrl",
"control": "",
"dismiss_read_marker_and_jump_bottom": "忽略已读标记并跳转到底部",
"end": "结束",
"end": "",
"enter": "",
"escape": "Esc",
"escape": "",
"go_home_view": "转到主页视图",
"home": "",
"jump_first_message": "跳转到首个消息",
@ -1493,7 +1494,7 @@
"scroll_up_timeline": "向上滚动时间线",
"search": "搜索(要使其生效必须启用相关功能)",
"send_sticker": "发送贴纸",
"shift": "Shift",
"shift": "",
"space": "空格",
"switch_to_space": "使用数字切换空间",
"toggle_hidden_events": "切换隐藏事件的可见性",
@ -1843,6 +1844,12 @@
"ongoing": "正在移除…",
"reason_label": "理由(可选)"
},
"remove_section_dialog": {
"confirmation": "你确定要移除此区域?",
"description": "此区域中的聊天仍将显示在聊天列表。",
"remove_section": "移除区域",
"title": "移除此区域?"
},
"report_content": {
"description": "举报此消息会将其唯一的“事件 ID”发送给服务器管理员。如果此房间中的消息已加密则服务器管理员将无法查看消息文本、任何文件或图像。",
"disagree": "不同意",
@ -2452,7 +2459,7 @@
"description_3": "浏览器扩展阻止了该请求。",
"description_4": "此服务器已离线。",
"description_5": "服务器已拒绝你的请求。",
"description_6": "你所在的在连接 Internet 时遇到困难。",
"description_6": "你所在的区在连接 Internet 时遇到困难。",
"description_7": "尝试联系服务器时发生连接错误。",
"description_8": "服务器的配置未能说明问题原因CORS。",
"empty_timeline": "你已阅读所有消息",
@ -2573,7 +2580,7 @@
"dialog_title": "<strong>设置:</strong>加密",
"key_storage": {
"allow_key_storage": "允许密钥存储",
"description": "这将允许你在任意新设备上查看聊天历史,这对备份聊天和数字身份是必需的。<a了解更多</a>",
"description": "这将允许你在任意新设备上查看聊天历史,这对备份聊天和数字身份是必需的。<a>了解更多</a>",
"title": "密钥存储"
},
"recovery": {
@ -3741,7 +3748,7 @@
"other": "%(names)s 与其他 %(count)s 个人正在输入…"
},
"one_user": "%(displayName)s 正在输入…",
"two_users": "%(names)s 与其他 %(lastPerson)s 个人正在输入…"
"two_users": "%(names)s 与 %(lastPerson)s 正在输入…"
},
"undecryptable_tooltip": "此消息无法解密"
},
@ -3894,7 +3901,6 @@
"connection_lost": "与服务器的连接已丢失",
"connection_lost_description": "在未连接到服务器的情况下,你无法拨打电话。",
"consulting": "正在与 %(transferTarget)s 协商。<a>转接到 %(transferee)s</a>",
"decline_call": "拒绝",
"default_device": "默认设备",
"dial": "拨号",
"dialpad": "拨号盘",
@ -3945,7 +3951,6 @@
"show_sidebar_button": "显示边栏",
"silence": "静音通话",
"silenced": "通知已静音",
"skip_lobby_toggle_option": "立即加入",
"start_screenshare": "开始分享屏幕",
"stop_screenshare": "停止分享屏幕",
"too_many_calls": "呼叫频繁",
@ -3967,7 +3972,6 @@
"user_is_presenting": "%(sharerName)s 正在分享",
"video_call": "视频通话",
"video_call_incoming": "视频通话来电",
"video_call_started": "已开始视频通话",
"video_call_using": "视频通话时使用:",
"voice_call": "语音通话",
"voice_call_incoming": "语音通话来电",

View File

@ -3399,7 +3399,6 @@
"user_busy_description": "您想要通話的使用者目前忙碌中。",
"user_is_presenting": "%(sharerName)s 正在投影",
"video_call": "視訊通話",
"video_call_started": "視訊通話已開始",
"voice_call": "語音通話",
"you_are_presenting": "您正在投影"
},

View File

@ -11,7 +11,7 @@ import { logger } from "matrix-js-sdk/src/logger";
import { MatrixClientPeg } from "../MatrixClientPeg";
import { ALL_RULE_TYPES, BanList } from "./BanList";
import SettingsStore from "../settings/SettingsStore";
import SettingsStore, { type CallbackFn } from "../settings/SettingsStore";
import { _t } from "../languageHandler";
import dis from "../dispatcher/dispatcher";
import { SettingLevel } from "../settings/SettingLevel";
@ -38,7 +38,7 @@ export class Mjolnir {
}
public start(): void {
this.mjolnirWatchRef = SettingsStore.watchSetting("mjolnirRooms", null, this.onListsChanged.bind(this));
this.mjolnirWatchRef = SettingsStore.watchSetting("mjolnirRooms", null, this.onListsChanged);
this.dispatcherRef = dis.register(this.onAction);
dis.dispatch<DoAfterSyncPreparedPayload<ActionPayload>>({
@ -130,15 +130,10 @@ export class Mjolnir {
this.updateLists(this._roomIds);
};
private onListsChanged(
settingName: string,
roomId: string | null,
atLevel: SettingLevel,
newValue: string[],
): void {
private onListsChanged: CallbackFn<"mjolnirRooms"> = (settingName, roomId, atLevel, newValue): void => {
// We know that ban lists are only recorded at one level so we don't need to re-eval them
this.updateLists(newValue);
}
this.updateLists(newValue ?? []);
};
private updateLists(listRoomIds: string[]): void {
if (!MatrixClientPeg.get()) return;

View File

@ -1,4 +1,5 @@
/*
Copyright 2026 Element Creations Ltd.
Copyright 2024, 2025 New Vector Ltd.
Copyright 2018-2024 The Matrix.org Foundation C.I.C.
Copyright 2017 Travis Ralston
@ -53,6 +54,7 @@ import { type ComputedInviteConfig } from "../@types/invite-rules.ts";
import BlockInvitesConfigController from "./controllers/BlockInvitesConfigController.ts";
import RequiresSettingsController from "./controllers/RequiresSettingsController.ts";
import { type OrderedCustomSections, type CustomSectionsData } from "../stores/room-list-v3/section.ts";
import { type NotificationSound } from "../Notifier.ts";
export const defaultWatchManager = new WatchManager();
@ -228,6 +230,7 @@ export interface Settings {
"feature_ask_to_join": IFeature;
"feature_notifications": IFeature;
"feature_msc4362_encrypted_state_events": IFeature;
"feature_user_status": IFeature;
// These are in the feature namespace but aren't actually features
"feature_hidebold": IBaseSetting<boolean>;
@ -309,15 +312,7 @@ export interface Settings {
"urlPreviewsEnabled_e2ee": IBaseSetting<boolean>;
"notificationsEnabled": IBaseSetting<boolean>;
"deviceNotificationsEnabled": IBaseSetting<boolean>;
"notificationSound": IBaseSetting<
| {
name: string;
type: string;
size: number;
url: string;
}
| false
>;
"notificationSound": IBaseSetting<NotificationSound | false>;
"notificationBodyEnabled": IBaseSetting<boolean>;
"audioNotificationsEnabled": IBaseSetting<boolean>;
"enableWidgetScreenshots": IBaseSetting<boolean>;
@ -789,6 +784,30 @@ export const SETTINGS: Settings = {
shouldWarn: true,
default: false,
},
"feature_user_status": {
isFeature: true,
labsGroup: LabGroup.Profile,
displayName: _td("labs|feature_user_status|display_name"),
description: _td("labs|feature_user_status|description"),
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG_PRIORITISED,
supportedLevelsAreOrdered: true,
controller: new ServerSupportUnstableFeatureController(
"feature_user_status",
defaultWatchManager,
[["org.matrix.msc4429"], ["org.matrix.msc4429.stable"]],
undefined,
_td("labs|feature_user_status|required_msc_support"),
false,
// We have to assume it's available during early startup because of a race:
// The feature is used to enable extra sync filters during MatrixClient setup
// and we can't check for serverside support until the client has finished setting up.
// Once the client has setup, (so by the time the user actually opens the labs menu) we can
// enforce proper checks.
true,
true,
),
default: false,
},
"useCompactLayout": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
displayName: _td("settings|preferences|compact_modern"),
@ -1126,7 +1145,13 @@ export const SETTINGS: Settings = {
supportedLevelsAreOrdered: true,
displayName: _td("settings|inline_url_previews_default"),
default: true,
controller: new UIFeatureController(UIFeature.URLPreviews),
controller: new RequiresSettingsController([UIFeature.URLPreviews], false, (c) => {
if (c["io.element.msc4452.preview_url"]?.enabled !== false) {
// If the capability is not listed, or explicitly true then do not disable.
return false;
}
return _t("common|disabled_by_homeserver");
}),
},
"urlPreviewsEnabled_e2ee": {
// Can only be enabled per-device to ensure neither the homeserver nor client config

View File

@ -79,7 +79,7 @@ export const LEVEL_ORDER = [
SettingLevel.DEFAULT,
];
function getLevelOrder(setting: ISetting): SettingLevel[] {
function getLevelOrder(setting: Settings[keyof Settings]): SettingLevel[] {
// Settings which support only a single setting level are inherently ordered
if (setting.supportedLevelsAreOrdered || setting.supportedLevels.length === 1) {
// return a copy to prevent callers from modifying the array
@ -88,12 +88,12 @@ function getLevelOrder(setting: ISetting): SettingLevel[] {
return LEVEL_ORDER;
}
export type CallbackFn = (
settingName: SettingKey,
export type CallbackFn<S extends SettingKey> = (
settingName: S,
roomId: string | null,
atLevel: SettingLevel,
newValAtLevel: any,
newVal: any,
newValAtLevel: Settings[S]["default"] | null,
newVal: Settings[S]["default"] | null,
) => void;
type HandlerMap = Partial<{
@ -167,7 +167,11 @@ export default class SettingsStore {
* if the change in value is worthwhile enough to react upon.
* @returns {string} A reference to the watcher that was employed.
*/
public static watchSetting(settingName: SettingKey, roomId: string | null, callbackFn: CallbackFn): string {
public static watchSetting<S extends SettingKey>(
settingName: S,
roomId: string | null,
callbackFn: CallbackFn<S>,
): string {
const setting = SETTINGS[settingName];
if (!setting) throw new Error(`${settingName} is not a setting`);
@ -175,7 +179,11 @@ export default class SettingsStore {
const watcherId = `${new Date().getTime()}_${SettingsStore.watcherCount++}_${finalSettingName}_${roomId}`;
const localizedCallback = (changedInRoomId: string | null, atLevel: SettingLevel, newValAtLevel: any): void => {
const localizedCallback = (
changedInRoomId: string | null,
atLevel: SettingLevel,
newValAtLevel: Settings[S]["default"],
): void => {
if (!SettingsStore.doesSettingSupportLevel(settingName, atLevel)) {
logger.warn(
`Setting handler notified for an update of an invalid setting level: ` +
@ -220,7 +228,7 @@ export default class SettingsStore {
* @param {string} settingName The setting name to monitor.
* @param {String} roomId The room ID to monitor for changes in. Use null for all rooms.
*/
public static monitorSetting(settingName: SettingKey, roomId: string | null): void {
public static monitorSetting<S extends SettingKey>(settingName: S, roomId: string | null): void {
roomId = roomId || null; // the thing wants null specifically to work, so appease it.
if (!this.monitors.has(settingName)) this.monitors.set(settingName, new Map());
@ -228,7 +236,7 @@ export default class SettingsStore {
const registerWatcher = (): void => {
this.monitors.get(settingName)!.set(
roomId,
SettingsStore.watchSetting(
SettingsStore.watchSetting<S>(
settingName,
roomId,
(settingName, inRoomId, level, newValueAtLevel, newValue) => {
@ -449,11 +457,10 @@ export default class SettingsStore {
/**
* Gets the default value of a setting.
* @param {string} settingName The name of the setting to read the value of.
* @param {String} roomId The room ID to read the setting value in, may be null.
* @return {*} The default value
* @param settingName The name of the setting to read the value of.
* @return The default value
*/
public static getDefaultValue(settingName: SettingKey): any {
public static getDefaultValue<S extends SettingKey>(settingName: S): Settings[S]["default"] {
// Verify that the setting is actually a setting
if (!SETTINGS[settingName]) {
throw new Error("Setting '" + settingName + "' does not appear to be a setting.");
@ -462,13 +469,13 @@ export default class SettingsStore {
return SETTINGS[settingName].default;
}
private static getFinalValue(
setting: ISetting,
private static getFinalValue<S extends SettingKey>(
setting: Settings[S],
level: SettingLevel,
roomId: string | null,
calculatedValue: any,
calculatedValue: Settings[S]["default"],
calculatedAtLevel: SettingLevel | null,
): any {
): Settings[S]["default"] {
let resultingValue = calculatedValue;
if (setting.controller) {
@ -480,25 +487,22 @@ export default class SettingsStore {
return resultingValue;
}
/* eslint-disable valid-jsdoc */ //https://github.com/eslint/eslint/issues/7307
/**
* Sets the value for a setting. The room ID is optional if the setting is not being
* set for a particular room, otherwise it should be supplied. The value may be null
* to indicate that the level should no longer have an override.
* @param {string} settingName The name of the setting to change.
* @param {String} roomId The room ID to change the value in, may be null.
* @param {SettingLevel} level The level
* @param settingName The name of the setting to change.
* @param roomId The room ID to change the value in, may be null.
* @param level The level
* to change the value at.
* @param {*} value The new value of the setting, may be null.
* @return {Promise} Resolves when the setting has been changed.
* @param value The new value of the setting, may be null.
* @return Resolves when the setting has been changed.
*/
/* eslint-enable valid-jsdoc */
public static async setValue(
settingName: SettingKey,
public static async setValue<S extends SettingKey>(
settingName: S,
roomId: string | null,
level: SettingLevel,
value: any,
value: Settings[S]["default"] | null,
): Promise<void> {
// Verify that the setting is actually a setting
const setting = SETTINGS[settingName];

View File

@ -38,8 +38,8 @@ export default class MediaPreviewConfigController extends MatrixClientBackedCont
const validMediaPreviews = Object.values(MediaPreviewValue);
const validInviteAvatars = [MediaPreviewValue.Off, MediaPreviewValue.On];
return {
invite_avatars: validInviteAvatars.includes(inviteAvatars) ? inviteAvatars : undefined,
media_previews: validMediaPreviews.includes(mediaPreviews) ? mediaPreviews : undefined,
invite_avatars: validInviteAvatars.includes(inviteAvatars!) ? inviteAvatars : undefined,
media_previews: validMediaPreviews.includes(mediaPreviews!) ? mediaPreviews : undefined,
};
}

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