diff --git a/CHANGELOG.md b/CHANGELOG.md
index b3a18c5396..a138735650 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,32 @@
+Changes in [1.11.104](https://github.com/element-hq/element-web/releases/tag/v1.11.104) (2025-06-17)
+====================================================================================================
+## ✨ Features
+
+* Update the mobile\_guide page to the new design. ([#30006](https://github.com/element-hq/element-web/pull/30006)). Contributed by @pixlwave.
+* Provide a devtool for manually verifying other devices ([#30094](https://github.com/element-hq/element-web/pull/30094)). Contributed by @andybalaam.
+* Implement MSC4155: Invite filtering ([#29603](https://github.com/element-hq/element-web/pull/29603)). Contributed by @Half-Shot.
+* Add low priority avatar decoration to room tile ([#30065](https://github.com/element-hq/element-web/pull/30065)). Contributed by @MidhunSureshR.
+* Add ability to prevent window content being captured by other apps (Desktop) ([#30098](https://github.com/element-hq/element-web/pull/30098)). Contributed by @t3chguy.
+* New room list: move message preview in user settings ([#30023](https://github.com/element-hq/element-web/pull/30023)). Contributed by @florianduros.
+* New room list: change room options icon ([#30029](https://github.com/element-hq/element-web/pull/30029)). Contributed by @florianduros.
+* RoomListStore: Sort low priority rooms to the bottom of the list ([#30070](https://github.com/element-hq/element-web/pull/30070)). Contributed by @MidhunSureshR.
+* Add low priority filter pill to the room list UI ([#30060](https://github.com/element-hq/element-web/pull/30060)). Contributed by @MidhunSureshR.
+* New room list: remove color gradient in space panel ([#29721](https://github.com/element-hq/element-web/pull/29721)). Contributed by @florianduros.
+* /share?msg=foo endpoint using forward message dialog ([#29874](https://github.com/element-hq/element-web/pull/29874)). Contributed by @ara4n.
+
+## 🐛 Bug Fixes
+
+* Do not send empty auth when setting up cross-signing keys ([#29914](https://github.com/element-hq/element-web/pull/29914)). Contributed by @gnieto.
+* Settings: flip local video feed by default ([#29501](https://github.com/element-hq/element-web/pull/29501)). Contributed by @jbtrystram.
+* AccessSecretStorageDialog: various fixes ([#30093](https://github.com/element-hq/element-web/pull/30093)). Contributed by @richvdh.
+* AccessSecretStorageDialog: fix inability to enter recovery key ([#30090](https://github.com/element-hq/element-web/pull/30090)). Contributed by @richvdh.
+* Fix failure to upload thumbnail causing image to send as file ([#30086](https://github.com/element-hq/element-web/pull/30086)). Contributed by @t3chguy.
+* Low priority menu item should be a toggle ([#30071](https://github.com/element-hq/element-web/pull/30071)). Contributed by @MidhunSureshR.
+* Add sanity checks to prevent users from ignoring themselves ([#30079](https://github.com/element-hq/element-web/pull/30079)). Contributed by @MidhunSureshR.
+* Fix issue with duplicate images ([#30073](https://github.com/element-hq/element-web/pull/30073)). Contributed by @fatlewis.
+* Handle errors returned from Seshat ([#30083](https://github.com/element-hq/element-web/pull/30083)). Contributed by @richvdh.
+
+
Changes in [1.11.103](https://github.com/element-hq/element-web/releases/tag/v1.11.103) (2025-06-10)
====================================================================================================
## 🐛 Bug Fixes
diff --git a/package.json b/package.json
index 7d1dd907c8..3641c7a03f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "element-web",
- "version": "1.11.103",
+ "version": "1.11.104",
"description": "Element: the future of secure communication",
"author": "New Vector Ltd.",
"repository": {
@@ -93,7 +93,7 @@
"@types/png-chunks-extract": "^1.0.2",
"@types/react-virtualized": "^9.21.30",
"@vector-im/compound-design-tokens": "^4.0.0",
- "@vector-im/compound-web": "^7.11.0",
+ "@vector-im/compound-web": "^8.0.0",
"@vector-im/matrix-wysiwyg": "2.38.3",
"@zxcvbn-ts/core": "^3.0.4",
"@zxcvbn-ts/language-common": "^3.0.4",
diff --git a/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts b/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts
index 7f87f3f43c..26d27cc01c 100644
--- a/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts
+++ b/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts
@@ -326,7 +326,9 @@ test.describe("Room list filters and sort", () => {
async ({ page, app, user }) => {
const emptyRoomList = getEmptyRoomList(page);
await expect(emptyRoomList).toMatchScreenshot("default-empty-room-list.png");
- await expect(page.getByTestId("room-list-panel")).toMatchScreenshot("room-panel-empty-room-list.png");
+ await expect(page.getByRole("navigation", { name: "Room list" })).toMatchScreenshot(
+ "room-panel-empty-room-list.png",
+ );
},
);
diff --git a/playwright/e2e/left-panel/room-list-panel/room-list-panel.spec.ts b/playwright/e2e/left-panel/room-list-panel/room-list-panel.spec.ts
index 8ca138a707..d0503e2caf 100644
--- a/playwright/e2e/left-panel/room-list-panel/room-list-panel.spec.ts
+++ b/playwright/e2e/left-panel/room-list-panel/room-list-panel.spec.ts
@@ -19,7 +19,7 @@ test.describe("Room list panel", () => {
* @param page
*/
function getRoomListView(page: Page) {
- return page.getByTestId("room-list-panel");
+ return page.getByRole("navigation", { name: "Room list" });
}
test.beforeEach(async ({ page, app, user }) => {
@@ -44,7 +44,7 @@ test.describe("Room list panel", () => {
test("should respond to small screen sizes", { tag: "@screenshot" }, async ({ page }) => {
await page.setViewportSize({ width: 575, height: 600 });
- const roomListPanel = page.getByTestId("room-list-panel");
+ const roomListPanel = getRoomListView(page);
await expect(roomListPanel).toMatchScreenshot("room-list-panel-smallscreen.png");
});
});
diff --git a/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts b/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts
index 73eb98512b..0567b8a162 100644
--- a/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts
+++ b/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts
@@ -278,7 +278,7 @@ test.describe("Room list", () => {
});
test("should be a video room", { tag: "@screenshot" }, async ({ page, app, user }) => {
- await page.getByTestId("room-list-panel").getByRole("button", { name: "Add" }).click();
+ await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
await page.getByRole("menuitem", { name: "New video room" }).click();
await page.getByRole("textbox", { name: "Name" }).fill("video room");
await page.getByRole("button", { name: "Create video room" }).click();
diff --git a/playwright/e2e/oidc/index.ts b/playwright/e2e/oidc/index.ts
index 02de6e2f03..602a4368c7 100644
--- a/playwright/e2e/oidc/index.ts
+++ b/playwright/e2e/oidc/index.ts
@@ -11,6 +11,9 @@ import { type Page } from "@playwright/test";
import { expect } from "../../element-web-test";
+/**
+ * Click through registering a new user in the MAS UI.
+ */
export async function registerAccountMas(
page: Page,
mailpit: MailpitClient,
@@ -42,3 +45,17 @@ export async function registerAccountMas(
await expect(page.getByText("Allow access to your account?")).toBeVisible();
await page.getByRole("button", { name: "Continue" }).click();
}
+
+/**
+ * Click through entering username and password into the MAS login prompt.
+ */
+export async function logInAccountMas(page: Page, username: string, password: string): Promise {
+ await expect(page.getByText("Please sign in to continue:")).toBeVisible();
+
+ await page.getByRole("textbox", { name: "Username" }).fill(username);
+ await page.getByRole("textbox", { name: "Password", exact: true }).fill(password);
+ await page.getByRole("button", { name: "Continue" }).click();
+
+ await expect(page.getByText("Allow access to your account?")).toBeVisible();
+ await page.getByRole("button", { name: "Continue" }).click();
+}
diff --git a/playwright/e2e/oidc/oidc-native.spec.ts b/playwright/e2e/oidc/oidc-native.spec.ts
index a6fbf231ce..3268c2b65a 100644
--- a/playwright/e2e/oidc/oidc-native.spec.ts
+++ b/playwright/e2e/oidc/oidc-native.spec.ts
@@ -6,8 +6,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
+import { type Config, CONFIG_JSON } from "@element-hq/element-web-playwright-common";
+import { type Browser, type Page } from "@playwright/test";
+import { type StartedHomeserverContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers/HomeserverContainer";
+
import { test, expect } from "../../element-web-test.ts";
-import { registerAccountMas } from ".";
+import { logInAccountMas, registerAccountMas } from ".";
import { ElementAppPage } from "../../pages/ElementAppPage.ts";
import { masHomeserver } from "../../plugins/homeserver/synapse/masHomeserver.ts";
@@ -101,4 +105,154 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
expect(localStorageKeys).toHaveLength(0);
},
);
+
+ test("can log in to an existing MAS account", { tag: "@screenshot" }, async ({ page, mailpitClient }, testInfo) => {
+ // Register an account with MAS
+ await page.goto("/#/login");
+ await page.getByRole("button", { name: "Continue" }).click();
+
+ const userId = `alice_${testInfo.testId}`;
+ await registerAccountMas(page, mailpitClient, userId, `${userId}@email.com`, "Pa$sW0rD!");
+ await expect(page.getByText("Welcome")).toBeVisible();
+
+ // Log out
+ await page.getByRole("button", { name: "User menu" }).click();
+ await expect(page.getByText(userId, { exact: true })).toBeVisible();
+
+ // Allow the outstanding requests queue to settle before logging out
+ await page.waitForTimeout(2000);
+ await page.locator(".mx_UserMenu_contextMenu").getByRole("menuitem", { name: "Sign out" }).click();
+ await expect(page).toHaveURL(/\/#\/login$/);
+
+ // Log in again
+ await page.goto("/#/login");
+ await page.getByRole("button", { name: "Continue" }).click();
+ await page.getByRole("button", { name: "Continue" }).click();
+
+ // We should be in (we see an error because we have no recovery key).
+ await expect(page.getByText("Unable to verify this device")).toBeVisible();
+ });
+
+ test.describe("with force_verification on", () => {
+ test.use({
+ config: {
+ force_verification: true,
+ },
+ });
+
+ test("verify dialog cannot be dismissed", { tag: "@screenshot" }, async ({ page, mailpitClient }, testInfo) => {
+ // Register an account with MAS
+ await page.goto("/#/login");
+ await page.getByRole("button", { name: "Continue" }).click();
+
+ const userId = `alice_${testInfo.testId}`;
+ await registerAccountMas(page, mailpitClient, userId, `${userId}@email.com`, "Pa$sW0rD!");
+ await expect(page.getByText("Welcome")).toBeVisible();
+
+ // Log out
+ await page.getByRole("button", { name: "User menu" }).click();
+ await expect(page.getByText(userId, { exact: true })).toBeVisible();
+ await page.waitForTimeout(2000);
+ await page.locator(".mx_UserMenu_contextMenu").getByRole("menuitem", { name: "Sign out" }).click();
+ await expect(page).toHaveURL(/\/#\/login$/);
+
+ // Log in again
+ await page.goto("/#/login");
+ await page.getByRole("button", { name: "Continue" }).click();
+ await page.getByRole("button", { name: "Continue" }).click();
+
+ // We should be being warned that we need to verify (but we can't)
+ await expect(page.getByText("Unable to verify this device")).toBeVisible();
+
+ // And there should be no way to close this prompt
+ await expect(page.getByRole("button", { name: "Skip verification for now" })).not.toBeVisible();
+ });
+
+ test(
+ "continues to show verification prompt after cancelling device verification",
+ { tag: "@screenshot" },
+ async ({ browser, config, homeserver, page, mailpitClient }, testInfo) => {
+ // Register an account with MAS
+ await page.goto("/#/login");
+ await page.getByRole("button", { name: "Continue" }).click();
+
+ const userId = `alice_${testInfo.testId}`;
+ const password = "Pa$sW0rD!";
+ await registerAccountMas(page, mailpitClient, userId, `${userId}@email.com`, password);
+ await expect(page.getByText("Welcome")).toBeVisible();
+
+ // Log in an additional account, and verify it.
+ //
+ // This means that when we log out and in again, we are offered
+ // to verify using another device.
+ const otherContext = await newContext(browser, config, homeserver);
+ const otherDevicePage = await otherContext.newPage();
+ await otherDevicePage.goto("/#/login");
+ await otherDevicePage.getByRole("button", { name: "Continue" }).click();
+ await logInAccountMas(otherDevicePage, userId, password);
+ await verifyUsingOtherDevice(otherDevicePage, page);
+ await otherDevicePage.close();
+
+ // Log out
+ await page.getByRole("button", { name: "User menu" }).click();
+ await expect(page.getByText(userId, { exact: true })).toBeVisible();
+ await page.waitForTimeout(2000);
+ await page.locator(".mx_UserMenu_contextMenu").getByRole("menuitem", { name: "Sign out" }).click();
+ await expect(page).toHaveURL(/\/#\/login$/);
+
+ // Log in again
+ await page.goto("/#/login");
+ await page.getByRole("button", { name: "Continue" }).click();
+ await page.getByRole("button", { name: "Continue" }).click();
+
+ // We should be in, and not able to dismiss the verify dialog
+ await expect(page.getByText("Verify this device")).toBeVisible();
+ await expect(page.getByRole("button", { name: "Skip verification for now" })).not.toBeVisible();
+
+ // When we start verifying with another device
+ await page.getByRole("button", { name: "Verify with another device" }).click();
+
+ // And then cancel it
+ await page.getByRole("button", { name: "Close dialog" }).click();
+
+ // Then we should still be at the unskippable verify prompt
+ await expect(page.getByText("Verify this device")).toBeVisible();
+ await expect(page.getByRole("button", { name: "Skip verification for now" })).not.toBeVisible();
+ },
+ );
+ });
});
+
+/**
+ * Perform interactive emoji verification for a new device.
+ */
+async function verifyUsingOtherDevice(deviceToVerifyPage: Page, alreadyVerifiedDevicePage: Page) {
+ await deviceToVerifyPage.getByRole("button", { name: "Verify with another device" }).click();
+ await alreadyVerifiedDevicePage.getByRole("button", { name: "Verify session" }).click();
+ await alreadyVerifiedDevicePage.getByRole("button", { name: "Start" }).click();
+ await alreadyVerifiedDevicePage.getByRole("button", { name: "They match" }).click();
+ await deviceToVerifyPage.getByRole("button", { name: "They match" }).click();
+ await alreadyVerifiedDevicePage.getByRole("button", { name: "Got it" }).click();
+ await deviceToVerifyPage.getByRole("button", { name: "Got it" }).click();
+}
+
+/**
+ * Create a new browser context which serves up the default config plus what you supplied, and sets m.homeserver to the
+ * supplied homeserver's URL.
+ */
+async function newContext(browser: Browser, config: Partial>, homeserver: StartedHomeserverContainer) {
+ const otherContext = await browser.newContext();
+ await otherContext.route(`http://localhost:8080/config.json*`, async (route) => {
+ const json = {
+ ...CONFIG_JSON,
+ ...config,
+ default_server_config: {
+ "m.homeserver": {
+ base_url: homeserver.baseUrl,
+ },
+ },
+ };
+ await route.fulfill({ json });
+ });
+ return otherContext;
+}
diff --git a/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-All-new-pinned-messages-linux.png b/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-All-new-pinned-messages-linux.png
index f466c17d64..78ba6e5cfd 100644
Binary files a/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-All-new-pinned-messages-linux.png and b/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-All-new-pinned-messages-linux.png differ
diff --git a/res/css/structures/_ThreadsActivityCentre.pcss b/res/css/structures/_ThreadsActivityCentre.pcss
index a1472108ac..f26139da64 100644
--- a/res/css/structures/_ThreadsActivityCentre.pcss
+++ b/res/css/structures/_ThreadsActivityCentre.pcss
@@ -49,12 +49,12 @@
&:hover,
&:hover .mx_ThreadsActivityCentreButton_Icon {
background-color: $quaternary-content;
- color: $primary-content;
+ fill: $primary-content;
}
}
& .mx_ThreadsActivityCentreButton_Icon {
- color: $secondary-content;
+ fill: $secondary-content;
}
}
diff --git a/res/css/views/rooms/_RoomHeader.pcss b/res/css/views/rooms/_RoomHeader.pcss
index afde8f3464..0e2b40e0bd 100644
--- a/res/css/views/rooms/_RoomHeader.pcss
+++ b/res/css/views/rooms/_RoomHeader.pcss
@@ -103,5 +103,5 @@ Please see LICENSE files in the repository root for full details.
}
.mx_RoomHeader .mx_RoomHeader_toggled {
- color: var(--cpd-color-icon-accent-primary);
+ fill: var(--cpd-color-icon-accent-primary);
}
diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts
index 5641f936ae..20e7435599 100644
--- a/src/Lifecycle.ts
+++ b/src/Lifecycle.ts
@@ -657,7 +657,7 @@ export async function restoreSessionFromStorage(opts?: { ignoreGuest?: boolean }
freshLogin: freshLogin,
},
false,
- false,
+ freshLogin,
);
return true;
} else {
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index d64f4b7a36..b6f8bc06b3 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -315,7 +315,7 @@ function LocalRoomView(props: LocalRoomViewProps): ReactElement {
-
+
diff --git a/src/components/viewmodels/right_panel/user_info/admin/UserInfoAdminToolsContainerViewModel.tsx b/src/components/viewmodels/right_panel/user_info/admin/UserInfoAdminToolsContainerViewModel.tsx
new file mode 100644
index 0000000000..54ed32ceb5
--- /dev/null
+++ b/src/components/viewmodels/right_panel/user_info/admin/UserInfoAdminToolsContainerViewModel.tsx
@@ -0,0 +1,82 @@
+/*
+Copyright 2025 New Vector Ltd.
+SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
+Please see LICENSE files in the repository root for full details.
+*/
+
+import { type Room, type RoomMember, type IPowerLevelsContent } from "matrix-js-sdk/src/matrix";
+
+import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext";
+
+/**
+ * Interface used by admin tools container subcomponents props
+ */
+export interface RoomAdminToolsProps {
+ room: Room;
+ member: RoomMember;
+ isUpdating: boolean;
+ startUpdating: () => void;
+ stopUpdating: () => void;
+}
+
+/**
+ * Interface used by admin tools container props
+ */
+export interface RoomAdminToolsContainerProps {
+ room: Room;
+ member: RoomMember;
+ powerLevels: IPowerLevelsContent;
+}
+
+interface UserInfoAdminToolsContainerState {
+ shouldShowKickButton: boolean;
+ shouldShowBanButton: boolean;
+ shouldShowMuteButton: boolean;
+ shouldShowRedactButton: boolean;
+ isCurrentUserInTheRoom: boolean;
+}
+
+/**
+ * The view model for the user info admin tools container
+ * @param {RoomAdminToolsContainerProps} props - the object containing the necceray props for the view model
+ * @param {Room} props.room - the room that display the admin tools
+ * @param {RoomMember} props.member - the selected member
+ * @param {IPowerLevelsContent} props.powerLevels - current room power levels
+ * @returns {UserInfoAdminToolsContainerState} the user info admin tools container state
+ */
+export const useUserInfoAdminToolsContainerViewModel = (
+ props: RoomAdminToolsContainerProps,
+): UserInfoAdminToolsContainerState => {
+ const cli = useMatrixClientContext();
+ const { room, member, powerLevels } = props;
+
+ const editPowerLevel =
+ (powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) || powerLevels.state_default;
+
+ // if these do not exist in the event then they should default to 50 as per the spec
+ const { ban: banPowerLevel = 50, kick: kickPowerLevel = 50, redact: redactPowerLevel = 50 } = powerLevels;
+
+ const me = room.getMember(cli.getUserId() || "");
+ const isCurrentUserInTheRoom = me !== null;
+
+ if (!isCurrentUserInTheRoom) {
+ return {
+ shouldShowKickButton: false,
+ shouldShowBanButton: false,
+ shouldShowMuteButton: false,
+ shouldShowRedactButton: false,
+ isCurrentUserInTheRoom: false,
+ };
+ }
+
+ const isMe = me.userId === member.userId;
+ const canAffectUser = member.powerLevel < me.powerLevel || isMe;
+
+ return {
+ shouldShowKickButton: !isMe && canAffectUser && me.powerLevel >= kickPowerLevel,
+ shouldShowRedactButton: me.powerLevel >= redactPowerLevel && !room.isSpaceRoom(),
+ shouldShowBanButton: !isMe && canAffectUser && me.powerLevel >= banPowerLevel,
+ shouldShowMuteButton: !isMe && canAffectUser && me.powerLevel >= Number(editPowerLevel) && !room.isSpaceRoom(),
+ isCurrentUserInTheRoom,
+ };
+};
diff --git a/src/components/viewmodels/right_panel/user_info/admin/UserInfoBanButtonViewModel.tsx b/src/components/viewmodels/right_panel/user_info/admin/UserInfoBanButtonViewModel.tsx
new file mode 100644
index 0000000000..525b10e093
--- /dev/null
+++ b/src/components/viewmodels/right_panel/user_info/admin/UserInfoBanButtonViewModel.tsx
@@ -0,0 +1,153 @@
+/*
+Copyright 2025 New Vector Ltd.
+SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
+Please see LICENSE files in the repository root for full details.
+*/
+
+import { logger } from "@sentry/browser";
+import { type Room } from "matrix-js-sdk/src/matrix";
+import { KnownMembership } from "matrix-js-sdk/src/types";
+
+import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext";
+import { _t } from "../../../../../languageHandler";
+import Modal from "../../../../../Modal";
+import { bulkSpaceBehaviour } from "../../../../../utils/space";
+import ConfirmSpaceUserActionDialog from "../../../../views/dialogs/ConfirmSpaceUserActionDialog";
+import ConfirmUserActionDialog from "../../../../views/dialogs/ConfirmUserActionDialog";
+import ErrorDialog from "../../../../views/dialogs/ErrorDialog";
+import { type RoomAdminToolsProps } from "./UserInfoAdminToolsContainerViewModel";
+
+export interface BanButtonState {
+ /**
+ * The function to call when the button is clicked
+ */
+ onBanOrUnbanClick: () => Promise;
+ /**
+ * The label of the ban button can be ban or unban
+ */
+ banLabel: string;
+}
+/**
+ * The view model for the room ban button used in the UserInfoAdminToolsContainer
+ * @param {RoomAdminToolsProps} props - the object containing the necceray props for banButton the view model
+ * @param {Room} props.room - the room to ban/unban the user in
+ * @param {RoomMember} props.member - the member to ban/unban
+ * @param {boolean} props.isUpdating - whether the operation is currently in progress
+ * @param {function} props.startUpdating - callback function to start the operation
+ * @param {function} props.stopUpdating - callback function to stop the operation
+ * @returns {BanButtonState} the room ban/unban button state
+ */
+export const useBanButtonViewModel = (props: RoomAdminToolsProps): BanButtonState => {
+ const { isUpdating, startUpdating, stopUpdating, room, member } = props;
+
+ const cli = useMatrixClientContext();
+
+ const isBanned = member.membership === KnownMembership.Ban;
+
+ let banLabel = room.isSpaceRoom() ? _t("user_info|ban_button_space") : _t("user_info|ban_button_room");
+ if (isBanned) {
+ banLabel = room.isSpaceRoom() ? _t("user_info|unban_button_space") : _t("user_info|unban_button_room");
+ }
+
+ const onBanOrUnbanClick = async (): Promise => {
+ if (isUpdating) return; // only allow one operation at a time
+ startUpdating();
+
+ const commonProps = {
+ member,
+ action: room.isSpaceRoom()
+ ? isBanned
+ ? _t("user_info|unban_button_space")
+ : _t("user_info|ban_button_space")
+ : isBanned
+ ? _t("user_info|unban_button_room")
+ : _t("user_info|ban_button_room"),
+ title: isBanned
+ ? _t("user_info|unban_room_confirm_title", { roomName: room.name })
+ : _t("user_info|ban_room_confirm_title", { roomName: room.name }),
+ askReason: !isBanned,
+ danger: !isBanned,
+ };
+
+ let finished: Promise<[success?: boolean, reason?: string, rooms?: Room[]]>;
+
+ if (room.isSpaceRoom()) {
+ ({ finished } = Modal.createDialog(
+ ConfirmSpaceUserActionDialog,
+ {
+ ...commonProps,
+ space: room,
+ spaceChildFilter: isBanned
+ ? (child: Room) => {
+ // Return true if the target member is banned and we have sufficient PL to unban
+ const myMember = child.getMember(cli.credentials.userId || "");
+ const theirMember = child.getMember(member.userId);
+ return (
+ !!myMember &&
+ !!theirMember &&
+ theirMember.membership === KnownMembership.Ban &&
+ myMember.powerLevel > theirMember.powerLevel &&
+ child.currentState.hasSufficientPowerLevelFor("ban", myMember.powerLevel)
+ );
+ }
+ : (child: Room) => {
+ // Return true if the target member isn't banned and we have sufficient PL to ban
+ const myMember = child.getMember(cli.credentials.userId || "");
+ const theirMember = child.getMember(member.userId);
+ return (
+ !!myMember &&
+ !!theirMember &&
+ theirMember.membership !== KnownMembership.Ban &&
+ myMember.powerLevel > theirMember.powerLevel &&
+ child.currentState.hasSufficientPowerLevelFor("ban", myMember.powerLevel)
+ );
+ },
+ allLabel: isBanned ? _t("user_info|unban_space_everything") : _t("user_info|ban_space_everything"),
+ specificLabel: isBanned ? _t("user_info|unban_space_specific") : _t("user_info|ban_space_specific"),
+ warningMessage: isBanned ? _t("user_info|unban_space_warning") : _t("user_info|kick_space_warning"),
+ },
+ "mx_ConfirmSpaceUserActionDialog_wrapper",
+ ));
+ } else {
+ ({ finished } = Modal.createDialog(ConfirmUserActionDialog, commonProps));
+ }
+
+ const [proceed, reason, rooms = []] = await finished;
+ if (!proceed) {
+ stopUpdating();
+ return;
+ }
+
+ const fn = (roomId: string): Promise => {
+ if (isBanned) {
+ return cli.unban(roomId, member.userId);
+ } else {
+ return cli.ban(roomId, member.userId, reason || undefined);
+ }
+ };
+
+ bulkSpaceBehaviour(room, rooms, (room) => fn(room.roomId))
+ .then(
+ () => {
+ // NO-OP; rely on the m.room.member event coming down else we could
+ // get out of sync if we force setState here!
+ logger.info("Ban success");
+ },
+ function (err) {
+ logger.error("Ban error: " + err);
+ Modal.createDialog(ErrorDialog, {
+ title: _t("common|error"),
+ description: _t("user_info|error_ban_user"),
+ });
+ },
+ )
+ .finally(() => {
+ stopUpdating();
+ });
+ };
+
+ return {
+ onBanOrUnbanClick,
+ banLabel,
+ };
+};
diff --git a/src/components/viewmodels/right_panel/user_info/admin/UserInfoKickButtonViewModel.tsx b/src/components/viewmodels/right_panel/user_info/admin/UserInfoKickButtonViewModel.tsx
new file mode 100644
index 0000000000..8ae179ac07
--- /dev/null
+++ b/src/components/viewmodels/right_panel/user_info/admin/UserInfoKickButtonViewModel.tsx
@@ -0,0 +1,142 @@
+/*
+Copyright 2025 New Vector Ltd.
+SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
+Please see LICENSE files in the repository root for full details.
+*/
+
+import { logger } from "@sentry/browser";
+import { type Room } from "matrix-js-sdk/src/matrix";
+import { KnownMembership } from "matrix-js-sdk/src/types";
+
+import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext";
+import { _t } from "../../../../../languageHandler";
+import Modal from "../../../../../Modal";
+import { bulkSpaceBehaviour } from "../../../../../utils/space";
+import ConfirmSpaceUserActionDialog from "../../../../views/dialogs/ConfirmSpaceUserActionDialog";
+import ConfirmUserActionDialog from "../../../../views/dialogs/ConfirmUserActionDialog";
+import ErrorDialog from "../../../../views/dialogs/ErrorDialog";
+import { type RoomAdminToolsProps } from "./UserInfoAdminToolsContainerViewModel";
+
+interface RoomKickButtonState {
+ /**
+ * The function to call when the button is clicked
+ */
+ onKickClick: () => Promise;
+ /**
+ * Whether the user can be kicked based on membership value. If the user already join or was invited, it can be kicked
+ */
+ canUserBeKicked: boolean;
+ /**
+ * The label of the kick button can be kick or disinvite
+ */
+ kickLabel: string;
+}
+
+/**
+ * The view model for the room kick button used in the UserInfoAdminToolsContainer
+ * @param {RoomAdminToolsProps} props - the object containing the necceray props for kickButton the view model
+ * @param {Room} props.room - the room to kick/disinvite the user from
+ * @param {RoomMember} props.member - the member to kick/disinvite
+ * @param {boolean} props.isUpdating - whether the operation is currently in progress
+ * @param {function} props.startUpdating - callback function to start the operation
+ * @param {function} props.stopUpdating - callback function to stop the operation
+ * @returns {KickButtonState} the room kick/disinvite button state
+ */
+export function useRoomKickButtonViewModel(props: RoomAdminToolsProps): RoomKickButtonState {
+ const { isUpdating, startUpdating, stopUpdating, room, member } = props;
+
+ const cli = useMatrixClientContext();
+
+ const onKickClick = async (): Promise => {
+ if (isUpdating) return; // only allow one operation at a time
+ startUpdating();
+
+ const commonProps = {
+ member,
+ action: room.isSpaceRoom()
+ ? member.membership === KnownMembership.Invite
+ ? _t("user_info|disinvite_button_space")
+ : _t("user_info|kick_button_space")
+ : member.membership === KnownMembership.Invite
+ ? _t("user_info|disinvite_button_room")
+ : _t("user_info|kick_button_room"),
+ title:
+ member.membership === KnownMembership.Invite
+ ? _t("user_info|disinvite_button_room_name", { roomName: room.name })
+ : _t("user_info|kick_button_room_name", { roomName: room.name }),
+ askReason: member.membership === KnownMembership.Join,
+ danger: true,
+ };
+
+ let finished: Promise<[success?: boolean, reason?: string, rooms?: Room[]]>;
+
+ if (room.isSpaceRoom()) {
+ ({ finished } = Modal.createDialog(
+ ConfirmSpaceUserActionDialog,
+ {
+ ...commonProps,
+ space: room,
+ spaceChildFilter: (child: Room) => {
+ // Return true if the target member is not banned and we have sufficient PL to ban them
+ const myMember = child.getMember(cli.credentials.userId || "");
+ const theirMember = child.getMember(member.userId);
+ return (
+ !!myMember &&
+ !!theirMember &&
+ theirMember.membership === member.membership &&
+ myMember.powerLevel > theirMember.powerLevel &&
+ child.currentState.hasSufficientPowerLevelFor("kick", myMember.powerLevel)
+ );
+ },
+ allLabel: _t("user_info|kick_button_space_everything"),
+ specificLabel: _t("user_info|kick_space_specific"),
+ warningMessage: _t("user_info|kick_space_warning"),
+ },
+ "mx_ConfirmSpaceUserActionDialog_wrapper",
+ ));
+ } else {
+ ({ finished } = Modal.createDialog(ConfirmUserActionDialog, commonProps));
+ }
+
+ const [proceed, reason, rooms = []] = await finished;
+ if (!proceed) {
+ stopUpdating();
+ return;
+ }
+
+ bulkSpaceBehaviour(room, rooms, (room) => cli.kick(room.roomId, member.userId, reason || undefined))
+ .then(
+ () => {
+ // NO-OP; rely on the m.room.member event coming down else we could
+ // get out of sync if we force setState here!
+ logger.info("Kick success");
+ },
+ function (err) {
+ logger.error("Kick error: " + err);
+ Modal.createDialog(ErrorDialog, {
+ title: _t("user_info|error_kicking_user"),
+ description: err?.message ?? "Operation failed",
+ });
+ },
+ )
+ .finally(() => {
+ stopUpdating();
+ });
+ };
+
+ const canUserBeKicked = member.membership === KnownMembership.Invite || member.membership === KnownMembership.Join;
+
+ const kickLabel = room.isSpaceRoom()
+ ? member.membership === KnownMembership.Invite
+ ? _t("user_info|disinvite_button_space")
+ : _t("user_info|kick_button_space")
+ : member.membership === KnownMembership.Invite
+ ? _t("user_info|disinvite_button_room")
+ : _t("user_info|kick_button_room");
+
+ return {
+ onKickClick,
+ canUserBeKicked,
+ kickLabel,
+ };
+}
diff --git a/src/components/viewmodels/right_panel/user_info/admin/UserInfoMuteButtonViewModel.tsx b/src/components/viewmodels/right_panel/user_info/admin/UserInfoMuteButtonViewModel.tsx
new file mode 100644
index 0000000000..1608628198
--- /dev/null
+++ b/src/components/viewmodels/right_panel/user_info/admin/UserInfoMuteButtonViewModel.tsx
@@ -0,0 +1,120 @@
+/*
+Copyright 2025 New Vector Ltd.
+SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
+Please see LICENSE files in the repository root for full details.
+*/
+
+import { logger } from "@sentry/browser";
+import { type RoomMember, type IPowerLevelsContent } from "matrix-js-sdk/src/matrix";
+import { KnownMembership } from "matrix-js-sdk/src/types";
+
+import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext";
+import { _t } from "../../../../../languageHandler";
+import Modal from "../../../../../Modal";
+import ErrorDialog from "../../../../views/dialogs/ErrorDialog";
+import { type RoomAdminToolsProps } from "./UserInfoAdminToolsContainerViewModel";
+
+interface MuteButtonState {
+ /**
+ * Whether the member is in the roomn based on the membership value
+ */
+ isMemberInTheRoom: boolean;
+ /**
+ * The label of the mute button can be mute or unmute
+ */
+ muteLabel: string;
+ /**
+ * The function to call when the mute button is clicked
+ */
+ onMuteButtonClick: () => Promise;
+}
+
+/**
+ * The view model for the room mute button used in the UserInfoAdminToolsContainer
+ * @param {RoomAdminToolsProps} props - the object containing the necceray props for muteButton the view model
+ * @param {Room} props.room - the room to mute/unmute the user in
+ * @param {RoomMember} props.member - the member to mute/unmute
+ * @param {boolean} props.isUpdating - whether the operation is currently in progress
+ * @param {function} props.startUpdating - callback function to start the operation
+ * @param {function} props.stopUpdating - callback function to stop the operation
+ * @returns {MuteButtonState} the room mute/unmute button state
+ */
+export const useMuteButtonViewModel = (props: RoomAdminToolsProps): MuteButtonState => {
+ const { isUpdating, startUpdating, stopUpdating, room, member } = props;
+
+ const cli = useMatrixClientContext();
+
+ const isMuted = (member: RoomMember, powerLevelContent: IPowerLevelsContent): boolean => {
+ if (!powerLevelContent || !member) return false;
+
+ const levelToSend =
+ (powerLevelContent.events ? powerLevelContent.events["m.room.message"] : null) ||
+ powerLevelContent.events_default;
+
+ // levelToSend could be undefined as .events_default is optional. Coercing in this case using
+ // Number() would always return false, so this preserves behaviour
+ // FIXME: per the spec, if `events_default` is unset, it defaults to zero. If
+ // the member has a negative powerlevel, this will give an incorrect result.
+ if (levelToSend === undefined) return false;
+
+ return member.powerLevel < levelToSend;
+ };
+
+ const muted = isMuted(member, room.currentState.getStateEvents("m.room.power_levels", "")?.getContent() || {});
+ const muteLabel = muted ? _t("common|unmute") : _t("common|mute");
+
+ const isMemberInTheRoom = member.membership == KnownMembership.Join;
+
+ const onMuteButtonClick = async (): Promise => {
+ if (isUpdating) return; // only allow one operation at a time
+ startUpdating();
+
+ const roomId = member.roomId;
+ const target = member.userId;
+
+ const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
+ const powerLevels = powerLevelEvent?.getContent();
+ const levelToSend = powerLevels?.events?.["m.room.message"] ?? powerLevels?.events_default;
+
+ let level;
+ if (muted) {
+ // unmute
+ level = levelToSend;
+ } else {
+ // mute
+ level = levelToSend - 1;
+ }
+ level = parseInt(level);
+
+ console.log("level", level);
+ if (isNaN(level)) {
+ stopUpdating();
+ return;
+ }
+
+ cli.setPowerLevel(roomId, target, level)
+ .then(
+ () => {
+ // NO-OP; rely on the m.room.member event coming down else we could
+ // get out of sync if we force setState here!
+ logger.info("Mute toggle success");
+ },
+ function (err) {
+ logger.error("Mute error: " + err);
+ Modal.createDialog(ErrorDialog, {
+ title: _t("common|error"),
+ description: _t("user_info|error_mute_user"),
+ });
+ },
+ )
+ .finally(() => {
+ stopUpdating();
+ });
+ };
+
+ return {
+ isMemberInTheRoom,
+ onMuteButtonClick,
+ muteLabel,
+ };
+};
diff --git a/src/components/viewmodels/right_panel/user_info/admin/UserInfoRedactButtonViewModel.tsx b/src/components/viewmodels/right_panel/user_info/admin/UserInfoRedactButtonViewModel.tsx
new file mode 100644
index 0000000000..73b8ea70f0
--- /dev/null
+++ b/src/components/viewmodels/right_panel/user_info/admin/UserInfoRedactButtonViewModel.tsx
@@ -0,0 +1,39 @@
+/*
+Copyright 2025 New Vector Ltd.
+SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
+Please see LICENSE files in the repository root for full details.
+*/
+
+import { type RoomMember } from "matrix-js-sdk/src/matrix";
+
+import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext";
+import Modal from "../../../../../Modal";
+import BulkRedactDialog from "../../../../views/dialogs/BulkRedactDialog";
+
+export interface RedactMessagesButtonState {
+ onRedactAllMessagesClick: () => void;
+}
+
+/**
+ * The view model for the redact messages button used in the UserInfoAdminToolsContainer
+ * @param {RoomMember} member - the selected member to redact messages for
+ * @returns {RedactMessagesButtonState} the redact messages button state
+ */
+export const useRedactMessagesButtonViewModel = (member: RoomMember): RedactMessagesButtonState => {
+ const cli = useMatrixClientContext();
+
+ const onRedactAllMessagesClick = (): void => {
+ const room = cli.getRoom(member.roomId);
+ if (!room) return;
+
+ Modal.createDialog(BulkRedactDialog, {
+ matrixClient: cli,
+ room,
+ member,
+ });
+ };
+
+ return {
+ onRedactAllMessagesClick,
+ };
+};
diff --git a/src/components/views/right_panel/BaseCard.tsx b/src/components/views/right_panel/BaseCard.tsx
index a564a60695..b308e1d973 100644
--- a/src/components/views/right_panel/BaseCard.tsx
+++ b/src/components/views/right_panel/BaseCard.tsx
@@ -76,7 +76,7 @@ const BaseCard: React.FC = ({
data-testid="base-card-back-button"
onClick={onBackClick}
tooltip={label}
- subtleBackground
+ kind="secondary"
>
@@ -92,7 +92,7 @@ const BaseCard: React.FC = ({
onClick={onClose ?? closeRightPanel}
ref={closeButtonRef}
tooltip={closeLabel ?? _t("action|close")}
- subtleBackground
+ kind="secondary"
>
diff --git a/src/components/views/right_panel/ExtensionsCard.tsx b/src/components/views/right_panel/ExtensionsCard.tsx
index 58c2435e72..448917290a 100644
--- a/src/components/views/right_panel/ExtensionsCard.tsx
+++ b/src/components/views/right_panel/ExtensionsCard.tsx
@@ -27,6 +27,8 @@ import AccessibleButton from "../elements/AccessibleButton";
import WidgetAvatar from "../avatars/WidgetAvatar";
import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
import EmptyState from "./EmptyState";
+import { shouldShowComponent } from "../../../customisations/helpers/UIComponents.ts";
+import { UIComponent } from "../../../settings/UIFeature.ts";
interface Props {
room: Room;
@@ -191,9 +193,11 @@ const ExtensionsCard: React.FC = ({ room, onClose }) => {
return (
-
- {_t("right_panel|add_integrations")}
-
+ {shouldShowComponent(UIComponent.AddIntegrations) && (
+
+ {_t("right_panel|add_integrations")}
+
+ )}
{body}
);
diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx
index 7bba4f0950..542e6421de 100644
--- a/src/components/views/right_panel/UserInfo.tsx
+++ b/src/components/views/right_panel/UserInfo.tsx
@@ -34,10 +34,6 @@ import MentionIcon from "@vector-im/compound-design-tokens/assets/web/icons/ment
import InviteIcon from "@vector-im/compound-design-tokens/assets/web/icons/user-add";
import BlockIcon from "@vector-im/compound-design-tokens/assets/web/icons/block";
import DeleteIcon from "@vector-im/compound-design-tokens/assets/web/icons/delete";
-import CloseIcon from "@vector-im/compound-design-tokens/assets/web/icons/close";
-import ChatProblemIcon from "@vector-im/compound-design-tokens/assets/web/icons/chat-problem";
-import VisibilityOffIcon from "@vector-im/compound-design-tokens/assets/web/icons/visibility-off";
-import LeaveIcon from "@vector-im/compound-design-tokens/assets/web/icons/leave";
import dis from "../../../dispatcher/dispatcher";
import Modal from "../../../Modal";
@@ -61,15 +57,11 @@ import Spinner from "../elements/Spinner";
import PowerSelector from "../elements/PowerSelector";
import MemberAvatar from "../avatars/MemberAvatar";
import PresenceLabel from "../rooms/PresenceLabel";
-import BulkRedactDialog from "../dialogs/BulkRedactDialog";
import { ShareDialog } from "../dialogs/ShareDialog";
import ErrorDialog from "../dialogs/ErrorDialog";
import QuestionDialog from "../dialogs/QuestionDialog";
-import ConfirmUserActionDialog from "../dialogs/ConfirmUserActionDialog";
import { mediaFromMxc } from "../../../customisations/Media";
import { type ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
-import ConfirmSpaceUserActionDialog from "../dialogs/ConfirmSpaceUserActionDialog";
-import { bulkSpaceBehaviour } from "../../../utils/space";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
import { UIComponent } from "../../../settings/UIFeature";
import { TimelineRenderingType } from "../../../contexts/RoomContext";
@@ -83,6 +75,7 @@ import { SdkContextClass } from "../../../contexts/SDKContext";
import { Flex } from "../../utils/Flex";
import CopyableText from "../elements/CopyableText";
import { useUserTimezone } from "../../../hooks/useUserTimezone";
+import { UserInfoAdminToolsContainer } from "./user_info/UserInfoAdminToolsContainer";
export interface IDevice extends Device {
ambiguous?: boolean;
@@ -314,7 +307,7 @@ const Container: React.FC<{
return {children}
;
};
-interface IPowerLevelsContent {
+export interface IPowerLevelsContent {
events?: Record;
// eslint-disable-next-line camelcase
users_default?: number;
@@ -368,362 +361,6 @@ export const useRoomPowerLevels = (cli: MatrixClient, room: Room): IPowerLevelsC
return powerLevels;
};
-interface IBaseProps {
- member: RoomMember;
- isUpdating: boolean;
- startUpdating(): void;
- stopUpdating(): void;
-}
-
-export const RoomKickButton = ({
- room,
- member,
- isUpdating,
- startUpdating,
- stopUpdating,
-}: Omit): JSX.Element | null => {
- const cli = useContext(MatrixClientContext);
-
- // check if user can be kicked/disinvited
- if (member.membership !== KnownMembership.Invite && member.membership !== KnownMembership.Join) return <>>;
-
- const onKick = async (): Promise => {
- if (isUpdating) return; // only allow one operation at a time
- startUpdating();
-
- const commonProps = {
- member,
- action: room.isSpaceRoom()
- ? member.membership === KnownMembership.Invite
- ? _t("user_info|disinvite_button_space")
- : _t("user_info|kick_button_space")
- : member.membership === KnownMembership.Invite
- ? _t("user_info|disinvite_button_room")
- : _t("user_info|kick_button_room"),
- title:
- member.membership === KnownMembership.Invite
- ? _t("user_info|disinvite_button_room_name", { roomName: room.name })
- : _t("user_info|kick_button_room_name", { roomName: room.name }),
- askReason: member.membership === KnownMembership.Join,
- danger: true,
- };
-
- let finished: Promise<[success?: boolean, reason?: string, rooms?: Room[]]>;
-
- if (room.isSpaceRoom()) {
- ({ finished } = Modal.createDialog(
- ConfirmSpaceUserActionDialog,
- {
- ...commonProps,
- space: room,
- spaceChildFilter: (child: Room) => {
- // Return true if the target member is not banned and we have sufficient PL to ban them
- const myMember = child.getMember(cli.credentials.userId || "");
- const theirMember = child.getMember(member.userId);
- return (
- !!myMember &&
- !!theirMember &&
- theirMember.membership === member.membership &&
- myMember.powerLevel > theirMember.powerLevel &&
- child.currentState.hasSufficientPowerLevelFor("kick", myMember.powerLevel)
- );
- },
- allLabel: _t("user_info|kick_button_space_everything"),
- specificLabel: _t("user_info|kick_space_specific"),
- warningMessage: _t("user_info|kick_space_warning"),
- },
- "mx_ConfirmSpaceUserActionDialog_wrapper",
- ));
- } else {
- ({ finished } = Modal.createDialog(ConfirmUserActionDialog, commonProps));
- }
-
- const [proceed, reason, rooms = []] = await finished;
- if (!proceed) {
- stopUpdating();
- return;
- }
-
- bulkSpaceBehaviour(room, rooms, (room) => cli.kick(room.roomId, member.userId, reason || undefined))
- .then(
- () => {
- // NO-OP; rely on the m.room.member event coming down else we could
- // get out of sync if we force setState here!
- logger.log("Kick success");
- },
- function (err) {
- logger.error("Kick error: " + err);
- Modal.createDialog(ErrorDialog, {
- title: _t("user_info|error_kicking_user"),
- description: err?.message ?? "Operation failed",
- });
- },
- )
- .finally(() => {
- stopUpdating();
- });
- };
-
- const kickLabel = room.isSpaceRoom()
- ? member.membership === KnownMembership.Invite
- ? _t("user_info|disinvite_button_space")
- : _t("user_info|kick_button_space")
- : member.membership === KnownMembership.Invite
- ? _t("user_info|disinvite_button_room")
- : _t("user_info|kick_button_room");
-
- return (
- {
- ev.preventDefault();
- onKick();
- }}
- disabled={isUpdating}
- label={kickLabel}
- kind="critical"
- Icon={LeaveIcon}
- />
- );
-};
-
-const RedactMessagesButton: React.FC = ({ member }) => {
- const cli = useContext(MatrixClientContext);
-
- const onRedactAllMessages = (): void => {
- const room = cli.getRoom(member.roomId);
- if (!room) return;
-
- Modal.createDialog(BulkRedactDialog, {
- matrixClient: cli,
- room,
- member,
- });
- };
-
- return (
- {
- ev.preventDefault();
- onRedactAllMessages();
- }}
- label={_t("user_info|redact_button")}
- kind="critical"
- Icon={CloseIcon}
- />
- );
-};
-
-export const BanToggleButton = ({
- room,
- member,
- isUpdating,
- startUpdating,
- stopUpdating,
-}: Omit): JSX.Element => {
- const cli = useContext(MatrixClientContext);
-
- const isBanned = member.membership === KnownMembership.Ban;
- const onBanOrUnban = async (): Promise => {
- if (isUpdating) return; // only allow one operation at a time
- startUpdating();
-
- const commonProps = {
- member,
- action: room.isSpaceRoom()
- ? isBanned
- ? _t("user_info|unban_button_space")
- : _t("user_info|ban_button_space")
- : isBanned
- ? _t("user_info|unban_button_room")
- : _t("user_info|ban_button_room"),
- title: isBanned
- ? _t("user_info|unban_room_confirm_title", { roomName: room.name })
- : _t("user_info|ban_room_confirm_title", { roomName: room.name }),
- askReason: !isBanned,
- danger: !isBanned,
- };
-
- let finished: Promise<[success?: boolean, reason?: string, rooms?: Room[]]>;
-
- if (room.isSpaceRoom()) {
- ({ finished } = Modal.createDialog(
- ConfirmSpaceUserActionDialog,
- {
- ...commonProps,
- space: room,
- spaceChildFilter: isBanned
- ? (child: Room) => {
- // Return true if the target member is banned and we have sufficient PL to unban
- const myMember = child.getMember(cli.credentials.userId || "");
- const theirMember = child.getMember(member.userId);
- return (
- !!myMember &&
- !!theirMember &&
- theirMember.membership === KnownMembership.Ban &&
- myMember.powerLevel > theirMember.powerLevel &&
- child.currentState.hasSufficientPowerLevelFor("ban", myMember.powerLevel)
- );
- }
- : (child: Room) => {
- // Return true if the target member isn't banned and we have sufficient PL to ban
- const myMember = child.getMember(cli.credentials.userId || "");
- const theirMember = child.getMember(member.userId);
- return (
- !!myMember &&
- !!theirMember &&
- theirMember.membership !== KnownMembership.Ban &&
- myMember.powerLevel > theirMember.powerLevel &&
- child.currentState.hasSufficientPowerLevelFor("ban", myMember.powerLevel)
- );
- },
- allLabel: isBanned ? _t("user_info|unban_space_everything") : _t("user_info|ban_space_everything"),
- specificLabel: isBanned ? _t("user_info|unban_space_specific") : _t("user_info|ban_space_specific"),
- warningMessage: isBanned ? _t("user_info|unban_space_warning") : _t("user_info|kick_space_warning"),
- },
- "mx_ConfirmSpaceUserActionDialog_wrapper",
- ));
- } else {
- ({ finished } = Modal.createDialog(ConfirmUserActionDialog, commonProps));
- }
-
- const [proceed, reason, rooms = []] = await finished;
- if (!proceed) {
- stopUpdating();
- return;
- }
-
- const fn = (roomId: string): Promise => {
- if (isBanned) {
- return cli.unban(roomId, member.userId);
- } else {
- return cli.ban(roomId, member.userId, reason || undefined);
- }
- };
-
- bulkSpaceBehaviour(room, rooms, (room) => fn(room.roomId))
- .then(
- () => {
- // NO-OP; rely on the m.room.member event coming down else we could
- // get out of sync if we force setState here!
- logger.log("Ban success");
- },
- function (err) {
- logger.error("Ban error: " + err);
- Modal.createDialog(ErrorDialog, {
- title: _t("common|error"),
- description: _t("user_info|error_ban_user"),
- });
- },
- )
- .finally(() => {
- stopUpdating();
- });
- };
-
- let label = room.isSpaceRoom() ? _t("user_info|ban_button_space") : _t("user_info|ban_button_room");
- if (isBanned) {
- label = room.isSpaceRoom() ? _t("user_info|unban_button_space") : _t("user_info|unban_button_room");
- }
-
- return (
- {
- ev.preventDefault();
- onBanOrUnban();
- }}
- disabled={isUpdating}
- label={label}
- kind="critical"
- Icon={ChatProblemIcon}
- />
- );
-};
-
-interface IBaseRoomProps extends IBaseProps {
- room: Room;
- powerLevels: IPowerLevelsContent;
- children?: ReactNode;
-}
-
-// We do not show a Mute button for ourselves so it doesn't need to handle warning self demotion
-const MuteToggleButton: React.FC = ({
- member,
- room,
- powerLevels,
- isUpdating,
- startUpdating,
- stopUpdating,
-}) => {
- const cli = useContext(MatrixClientContext);
-
- // Don't show the mute/unmute option if the user is not in the room
- if (member.membership !== KnownMembership.Join) return null;
-
- const muted = isMuted(member, powerLevels);
- const onMuteToggle = async (): Promise => {
- if (isUpdating) return; // only allow one operation at a time
- startUpdating();
-
- const roomId = member.roomId;
- const target = member.userId;
-
- const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
- const powerLevels = powerLevelEvent?.getContent();
- const levelToSend = powerLevels?.events?.["m.room.message"] ?? powerLevels?.events_default;
- let level;
- if (muted) {
- // unmute
- level = levelToSend;
- } else {
- // mute
- level = levelToSend - 1;
- }
- level = parseInt(level);
-
- if (isNaN(level)) {
- stopUpdating();
- return;
- }
-
- cli.setPowerLevel(roomId, target, level)
- .then(
- () => {
- // NO-OP; rely on the m.room.member event coming down else we could
- // get out of sync if we force setState here!
- logger.log("Mute toggle success");
- },
- function (err) {
- logger.error("Mute error: " + err);
- Modal.createDialog(ErrorDialog, {
- title: _t("common|error"),
- description: _t("user_info|error_mute_user"),
- });
- },
- )
- .finally(() => {
- stopUpdating();
- });
- };
-
- const muteLabel = muted ? _t("common|unmute") : _t("common|mute");
- return (
- {
- ev.preventDefault();
- onMuteToggle();
- }}
- disabled={isUpdating}
- label={muteLabel}
- kind="critical"
- Icon={VisibilityOffIcon}
- />
- );
-};
-
const IgnoreToggleButton: React.FC<{
member: User | RoomMember;
}> = ({ member }) => {
@@ -786,96 +423,6 @@ const IgnoreToggleButton: React.FC<{
);
};
-export const RoomAdminToolsContainer: React.FC = ({
- room,
- children,
- member,
- isUpdating,
- startUpdating,
- stopUpdating,
- powerLevels,
-}) => {
- const cli = useContext(MatrixClientContext);
- let kickButton;
- let banButton;
- let muteButton;
- let redactButton;
-
- const editPowerLevel =
- (powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) || powerLevels.state_default;
-
- // if these do not exist in the event then they should default to 50 as per the spec
- const { ban: banPowerLevel = 50, kick: kickPowerLevel = 50, redact: redactPowerLevel = 50 } = powerLevels;
-
- const me = room.getMember(cli.getUserId() || "");
- if (!me) {
- // we aren't in the room, so return no admin tooling
- return
;
- }
-
- const isMe = me.userId === member.userId;
- const canAffectUser = member.powerLevel < me.powerLevel || isMe;
-
- if (!isMe && canAffectUser && me.powerLevel >= kickPowerLevel) {
- kickButton = (
-
- );
- }
- if (me.powerLevel >= redactPowerLevel && !room.isSpaceRoom()) {
- redactButton = (
-
- );
- }
- if (!isMe && canAffectUser && me.powerLevel >= banPowerLevel) {
- banButton = (
-
- );
- }
- if (!isMe && canAffectUser && me.powerLevel >= Number(editPowerLevel) && !room.isSpaceRoom()) {
- muteButton = (
-
- );
- }
-
- if (kickButton || banButton || muteButton || redactButton || children) {
- return (
-
- {muteButton}
- {redactButton}
- {kickButton}
- {banButton}
- {children}
-
- );
- }
-
- return
;
-};
-
const useIsSynapseAdmin = (cli?: MatrixClient): boolean => {
return useAsyncMemo(async () => (cli ? cli.isSynapseAdministrator().catch(() => false) : false), [cli], false);
};
@@ -1283,7 +830,7 @@ const BasicUserInfo: React.FC<{
}
adminToolsContainer = (
-
{synapseDeactivateButton}
-
+
);
} else if (synapseDeactivateButton) {
adminToolsContainer = {synapseDeactivateButton} ;
diff --git a/src/components/views/right_panel/user_info/UserInfoAdminToolsContainer.tsx b/src/components/views/right_panel/user_info/UserInfoAdminToolsContainer.tsx
new file mode 100644
index 0000000000..1f3eeb706d
--- /dev/null
+++ b/src/components/views/right_panel/user_info/UserInfoAdminToolsContainer.tsx
@@ -0,0 +1,220 @@
+/*
+Copyright 2025 New Vector Ltd.
+
+SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+Please see LICENSE files in the repository root for full details.
+*/
+
+import React, { type JSX, type ReactNode } from "react";
+import classNames from "classnames";
+import { type RoomMember, type Room } from "matrix-js-sdk/src/matrix";
+import { MenuItem } from "@vector-im/compound-web";
+import CloseIcon from "@vector-im/compound-design-tokens/assets/web/icons/close";
+import ChatProblemIcon from "@vector-im/compound-design-tokens/assets/web/icons/chat-problem";
+import VisibilityOffIcon from "@vector-im/compound-design-tokens/assets/web/icons/visibility-off";
+import LeaveIcon from "@vector-im/compound-design-tokens/assets/web/icons/leave";
+
+import { _t } from "../../../../languageHandler";
+import { type IPowerLevelsContent } from "../UserInfo";
+import { useUserInfoAdminToolsContainerViewModel } from "../../../viewmodels/right_panel/user_info/admin/UserInfoAdminToolsContainerViewModel";
+import { useMuteButtonViewModel } from "../../../viewmodels/right_panel/user_info/admin/UserInfoMuteButtonViewModel";
+import { useBanButtonViewModel } from "../../../viewmodels/right_panel/user_info/admin/UserInfoBanButtonViewModel";
+import { useRoomKickButtonViewModel } from "../../../viewmodels/right_panel/user_info/admin/UserInfoKickButtonViewModel";
+import { useRedactMessagesButtonViewModel } from "../../../viewmodels/right_panel/user_info/admin/UserInfoRedactButtonViewModel";
+
+const Container: React.FC<{
+ children: ReactNode;
+ className?: string;
+}> = ({ children, className }) => {
+ const classes = classNames("mx_UserInfo_container", className);
+ return {children}
;
+};
+
+interface IBaseProps {
+ member: RoomMember;
+ isUpdating: boolean;
+ startUpdating(): void;
+ stopUpdating(): void;
+}
+
+export const RoomKickButton = ({
+ room,
+ member,
+ isUpdating,
+ startUpdating,
+ stopUpdating,
+}: Omit): JSX.Element | null => {
+ const vm = useRoomKickButtonViewModel({ room, member, isUpdating, startUpdating, stopUpdating });
+ // check if user can be kicked/disinvited
+ if (!vm.canUserBeKicked) return <>>;
+
+ return (
+ {
+ ev.preventDefault();
+ vm.onKickClick();
+ }}
+ disabled={isUpdating}
+ label={vm.kickLabel}
+ kind="critical"
+ Icon={LeaveIcon}
+ />
+ );
+};
+
+const RedactMessagesButton: React.FC = ({ member }) => {
+ const vm = useRedactMessagesButtonViewModel(member);
+
+ return (
+ {
+ ev.preventDefault();
+ vm.onRedactAllMessagesClick();
+ }}
+ label={_t("user_info|redact_button")}
+ kind="critical"
+ Icon={CloseIcon}
+ />
+ );
+};
+
+export const BanToggleButton = ({
+ room,
+ member,
+ isUpdating,
+ startUpdating,
+ stopUpdating,
+}: Omit): JSX.Element => {
+ const vm = useBanButtonViewModel({ room, member, isUpdating, startUpdating, stopUpdating });
+
+ return (
+ {
+ ev.preventDefault();
+ vm.onBanOrUnbanClick();
+ }}
+ disabled={isUpdating}
+ label={vm.banLabel}
+ kind="critical"
+ Icon={ChatProblemIcon}
+ />
+ );
+};
+
+interface IBaseRoomProps extends IBaseProps {
+ room: Room;
+ powerLevels: IPowerLevelsContent;
+ children?: ReactNode;
+}
+
+// We do not show a Mute button for ourselves so it doesn't need to handle warning self demotion
+const MuteToggleButton: React.FC = ({
+ member,
+ room,
+ powerLevels,
+ isUpdating,
+ startUpdating,
+ stopUpdating,
+}) => {
+ const vm = useMuteButtonViewModel({ room, member, isUpdating, startUpdating, stopUpdating });
+ // Don't show the mute/unmute option if the user is not in the room
+ if (!vm.isMemberInTheRoom) return null;
+
+ return (
+ {
+ ev.preventDefault();
+ vm.onMuteButtonClick();
+ }}
+ disabled={isUpdating}
+ label={vm.muteLabel}
+ kind="critical"
+ Icon={VisibilityOffIcon}
+ />
+ );
+};
+
+export const UserInfoAdminToolsContainer: React.FC = ({
+ room,
+ children,
+ member,
+ isUpdating,
+ startUpdating,
+ stopUpdating,
+ powerLevels,
+}) => {
+ let kickButton;
+ let banButton;
+ let muteButton;
+ let redactButton;
+
+ const vm = useUserInfoAdminToolsContainerViewModel({ room, member, powerLevels });
+
+ if (!vm.isCurrentUserInTheRoom) {
+ // we aren't in the room, so return no admin tooling
+ return
;
+ }
+
+ if (vm.shouldShowKickButton) {
+ kickButton = (
+
+ );
+ }
+ if (vm.shouldShowRedactButton) {
+ redactButton = (
+
+ );
+ }
+ if (vm.shouldShowBanButton) {
+ banButton = (
+
+ );
+ }
+ if (vm.shouldShowMuteButton) {
+ muteButton = (
+
+ );
+ }
+
+ if (kickButton || banButton || muteButton || redactButton || children) {
+ return (
+
+ {muteButton}
+ {redactButton}
+ {kickButton}
+ {banButton}
+ {children}
+
+ );
+ }
+
+ return
;
+};
diff --git a/src/components/views/rooms/RoomListPanel/RoomListPanel.tsx b/src/components/views/rooms/RoomListPanel/RoomListPanel.tsx
index 291794399f..f5c0620a66 100644
--- a/src/components/views/rooms/RoomListPanel/RoomListPanel.tsx
+++ b/src/components/views/rooms/RoomListPanel/RoomListPanel.tsx
@@ -13,6 +13,7 @@ import { RoomListSearch } from "./RoomListSearch";
import { RoomListHeaderView } from "./RoomListHeaderView";
import { RoomListView } from "./RoomListView";
import { Flex } from "../../../utils/Flex";
+import { _t } from "../../../../languageHandler";
type RoomListPanelProps = {
/**
@@ -30,11 +31,11 @@ export const RoomListPanel: React.FC = ({ activeSpace }) =>
return (
{displayRoomSearch && }
diff --git a/src/components/views/rooms/RoomListPanel/RoomListPrimaryFilters.tsx b/src/components/views/rooms/RoomListPanel/RoomListPrimaryFilters.tsx
index 892f2b56b7..676501a3ea 100644
--- a/src/components/views/rooms/RoomListPanel/RoomListPrimaryFilters.tsx
+++ b/src/components/views/rooms/RoomListPanel/RoomListPrimaryFilters.tsx
@@ -39,7 +39,7 @@ export function RoomListPrimaryFilters({ vm }: RoomListPrimaryFiltersProps): JSX
>
{displayChevron && (
setIsExpanded((_expanded) => !_expanded)}
>
-
+
)}
%(brand)s Επιφάνεια εργασίας για να εμφανίζονται κρυπτογραφημένα μηνύματα στα αποτελέσματα αναζήτησης.",
"record_session_details": "Κατέγραψε το όνομα του πελάτη, την έκδοση και τη διεύθυνση URL για να αναγνωρίζεις τις συνεδρίες πιο εύκολα στον διαχειριστή συνεδρίας",
"send_analytics": "Αποστολή δεδομένων αναλυτικών στοιχείων",
- "strict_encryption": "Μη στέλνετε ποτέ κρυπτογραφημένα μηνύματα σε μη επαληθευμένες συνεδρίες από αυτήν τη συνεδρία"
+ "strict_encryption": "Αποστολή μηνυμάτων μόνο σε επαληθευμένους χρήστες"
},
"send_read_receipts": "Αποστολή αποδείξεων ανάγνωσης",
"send_read_receipts_unsupported": "Ο διακομιστής σου δεν υποστηρίζει την απενεργοποίηση αποστολής αποδείξεων ανάγνωσης.",
"send_typing_notifications": "Αποστολή ειδοποιήσεων πληκτρολόγησης",
"sessions": {
+ "best_security_note": "Για τη βέλτιστη ασφάλεια, επαληθεύστε τις συνεδρίες σας και αποσυνδεθείτε από οποιαδήποτε συνεδρία που δεν αναγνωρίζετε ή χρησιμοποιείτε πλέον.",
+ "browser": "Πρόγραμμα περιήγησης",
"confirm_sign_out": {
"one": "Επιβεβαιώστε την αποσύνδεση αυτής της συσκευής",
"other": "Επιβεβαιώστε την αποσύνδεση αυτών των συσκευών"
@@ -2197,8 +2201,83 @@
"one": "Επιβεβαιώστε ότι αποσυνδέεστε από αυτήν τη συσκευή χρησιμοποιώντας Single Sign On για να αποδείξετε την ταυτότητά σας.",
"other": "Επιβεβαιώστε την αποσύνδεση από αυτές τις συσκευές χρησιμοποιώντας Single Sign On για να αποδείξετε την ταυτότητά σας."
},
+ "current_session": "Τρέχουσα συνεδρία",
+ "desktop_session": "Συνεδρία εφαρμογής υπολογιστή",
+ "details_heading": "Λεπτομέρειες συνεδρίας",
+ "device_unverified_description": "Επαληθεύστε ή αποσυνδεθείτε από αυτήν τη συνεδρία για βέλτιστη ασφάλεια και αξιοπιστία.",
+ "device_unverified_description_current": "Επαληθεύστε την τρέχουσα συνεδρία σας για βελτιωμένα ασφαλή μηνύματα.",
+ "device_verified_description": "Αυτή η συνεδρία είναι έτοιμη για ασφαλή ανταλλαγή μηνυμάτων.",
+ "device_verified_description_current": "Η τρέχουσα συνεδρία σας είναι έτοιμη για ασφαλή ανταλλαγή μηνυμάτων.",
+ "error_pusher_state": "Αποτυχία ορισμού κατάστασης pusher",
+ "error_set_name": "Αποτυχία ορισμού ονόματος συνεδρίας",
+ "filter_all": "Όλα",
+ "filter_inactive": "Ανενεργό",
+ "filter_inactive_description": "Ανενεργό για%(inactiveAgeDays)s ημέρες ή και περισσότερο",
+ "filter_label": "Φιλτράρισμα συσκευών",
+ "filter_unverified_description": "Δεν είναι έτοιμο για ασφαλή ανταλλαγή μηνυμάτων",
+ "filter_verified_description": "Έτοιμο για ασφαλή ανταλλαγή μηνυμάτων",
+ "hide_details": "Απόκρυψη λεπτομερειών",
+ "inactive_days": "Ανενεργό για %(inactiveAgeDays)s+ ημέρες",
+ "inactive_sessions": "Ανενεργές συνεδρίες",
+ "inactive_sessions_explainer_1": "Οι ανενεργές συνεδρίες είναι συνεδρίες που δεν έχετε χρησιμοποιήσει για κάποιο χρονικό διάστημα, αλλά συνεχίζουν να λαμβάνουν κλειδιά κρυπτογράφησης.",
+ "inactive_sessions_explainer_2": "Η αφαίρεση ανενεργών συνεδριών βελτιώνει την ασφάλεια και την απόδοση και σας διευκολύνει να εντοπίσετε αν μια νέα συνεδρία είναι ύποπτη.",
+ "inactive_sessions_list_description": "Εξετάστε το ενδεχόμενο να αποσυνδεθείτε από παλιές συνεδρίες (%(inactiveAgeDays)s ημέρες ή παλαιότερες) που δεν χρησιμοποιείτε πλέον.",
+ "ip": "Διεύθυνση IP",
+ "last_activity": "Τελευταία δραστηριότητα",
+ "mobile_session": "Συνεδρία κινητού",
+ "n_sessions_selected": {
+ "one": "%(count)s επιλεγμένη συνεδρία",
+ "other": "%(count)s επιλεγμένες συνεδρίες"
+ },
+ "no_inactive_sessions": "Δεν βρέθηκαν ανενεργές συνεδρίες.",
+ "no_sessions": "Δεν βρέθηκαν συνεδρίες.",
+ "no_unverified_sessions": "Δεν βρέθηκαν μη επαληθευμένες συνεδρίες.",
+ "no_verified_sessions": "Δεν βρέθηκαν επαληθευμένες συνεδρίες.",
+ "os": "Λειτουργικό σύστημα",
+ "other_sessions_heading": "Άλλες συνεδρίες",
+ "push_heading": "Ειδοποιήσεις push",
+ "push_subheading": "Λάβετε ειδοποιήσεις push σε αυτήν τη συνεδρία.",
+ "push_toggle": "Ενεργοποίηση/απενεργοποίηση ειδοποιήσεων push σε αυτήν τη συνεδρία.",
+ "rename_form_caption": "Λάβετε υπόψη ότι τα ονόματα των συνεδριών είναι επίσης ορατά στα άτομα με τα οποία επικοινωνείτε.",
+ "rename_form_heading": "Μετονομασία συνεδρίας",
+ "rename_form_learn_more": "Μετονομασία συνεδριών",
+ "rename_form_learn_more_description_1": "Άλλοι χρήστες σε απευθείας μηνύματα και αίθουσες στις οποίες συμμετέχετε μπορούν να δουν μια πλήρη λίστα των συνεδριών σας.",
+ "rename_form_learn_more_description_2": "Αυτό τους παρέχει την εμπιστοσύνη ότι πραγματικά μιλούν με εσάς, αλλά σημαίνει επίσης ότι μπορούν να δουν το όνομα της συνεδρίας που εισάγετε εδώ.",
+ "security_recommendations": "Συστάσεις ασφαλείας",
+ "security_recommendations_description": "Βελτιώστε την ασφάλεια του λογαριασμού σας ακολουθώντας αυτές τις συστάσεις.",
"session_id": "Αναγνωριστικό συνεδρίας",
- "verify_session": "Επαλήθευση συνεδρίας"
+ "show_details": "Εμφάνιση λεπτομερειών",
+ "sign_in_with_qr": "Σύνδεση νέας συσκευής",
+ "sign_in_with_qr_button": "Εμφάνιση κωδικού QR",
+ "sign_in_with_qr_description": "Χρησιμοποιήστε έναν κωδικό QR για να συνδεθείτε σε άλλη συσκευή και να ρυθμίσετε την ασφαλή ανταλλαγή μηνυμάτων.",
+ "sign_out": "Αποσυνδεθείτε από αυτήν τη συνεδρία",
+ "sign_out_all_other_sessions": "Αποσύνδεση από όλες τις άλλες συνεδρίες (%(otherSessionsCount)s)",
+ "sign_out_confirm_description": {
+ "one": "Είστε βέβαιοι ότι θέλετε να αποσυνδεθείτε από τη %(count)s συνεδρία;",
+ "other": "Είστε βέβαιοι ότι θέλετε να αποσυνδεθείτε από τις %(count)s συνεδρίες;"
+ },
+ "sign_out_n_sessions": {
+ "one": "Αποσύνδεση από %(count)s συνεδρία",
+ "other": "Αποσύνδεση από %(count)s συνεδρίες"
+ },
+ "title": "Συνεδρίες",
+ "unknown_session": "Άγνωστος τύπος συνεδρίας",
+ "unverified_session": "Μη επαληθευμένη συνεδρία",
+ "unverified_session_explainer_1": "Αυτή η συνεδρία δεν υποστηρίζει κρυπτογράφηση και συνεπώς δεν μπορεί να επαληθευτεί.",
+ "unverified_session_explainer_2": "Δεν θα μπορείτε να συμμετάσχετε σε αίθουσες όπου είναι ενεργοποιημένη η κρυπτογράφηση κατά τη χρήση αυτής της συνεδρίας.",
+ "unverified_session_explainer_3": "Για καλύτερη ασφάλεια και ιδιωτικότητα, συνιστάται να χρησιμοποιείτε εφαρμογές Matrix που υποστηρίζουν κρυπτογράφηση.",
+ "unverified_sessions": "Μη επαληθευμένες συνεδρίες",
+ "unverified_sessions_explainer_1": "Οι μη επαληθευμένες συνεδρίες είναι συνεδρίες στις οποίες έχετε συνδεθεί με τα διαπιστευτήριά σας, αλλά δεν έχουν επαληθευτεί.",
+ "unverified_sessions_explainer_2": "Θα πρέπει να βεβαιωθείτε ιδιαίτερα ότι αναγνωρίζετε αυτές τις συνεδρίες, καθώς ενδέχεται να αποτελούν μη εξουσιοδοτημένη χρήση του λογαριασμού σας.",
+ "unverified_sessions_list_description": "Επαληθεύστε τις συνεδρίες σας για βελτιωμένα ασφαλή μηνύματα ή αποσυνδεθείτε από αυτές που δεν αναγνωρίζετε ή χρησιμοποιείτε πλέον.",
+ "url": "URL",
+ "verified_session": "Επαληθευμένη συνεδρία",
+ "verified_sessions": "Επαληθευμένες συνεδρίες",
+ "verified_sessions_explainer_1": "Οι επαληθευμένες συνεδρίες είναι οπουδήποτε χρησιμοποιείτε αυτόν τον λογαριασμό αφού εισαγάγετε τη φράση πρόσβασής σας ή επιβεβαιώσετε την ταυτότητά σας με άλλη επαληθευμένη συνεδρία.",
+ "verified_sessions_explainer_2": "Αυτό σημαίνει ότι έχετε όλα τα κλειδιά που απαιτούνται για να ξεκλειδώσετε τα κρυπτογραφημένα μηνύματά σας και να επιβεβαιώσετε σε άλλους χρήστες ότι εμπιστεύεστε αυτήν τη συνεδρία.",
+ "verified_sessions_list_description": "Για βέλτιστη ασφάλεια, αποσυνδεθείτε από οποιαδήποτε συνεδρία που δεν αναγνωρίζετε ή χρησιμοποιείτε πλέον.",
+ "verify_session": "Επαλήθευση συνεδρίας",
+ "web_session": "Συνεδρία web"
},
"show_avatar_changes": "Εμφάνιση αλλαγών εικόνας προφίλ",
"show_breadcrumbs": "Εμφάνιση συντομεύσεων σε δωμάτια που προβλήθηκαν πρόσφατα πάνω από τη λίστα δωματίων",
@@ -2219,6 +2298,7 @@
"metaspaces_orphans_description": "Ομαδοποιήστε σε ένα μέρος όλα τα δωμάτιά σας που δεν αποτελούν μέρος ενός χώρου.",
"metaspaces_people_description": "Ομαδοποιήστε όλα τα άτομα σας σε ένα μέρος.",
"metaspaces_subsection": "Χώροι για εμφάνιση",
+ "spaces_explainer": "Οι χώροι είναι τρόποι ομαδοποίησης αιθουσών και ανθρώπων. Παράλληλα με τους χώρους στους οποίους βρίσκεστε, μπορείτε να χρησιμοποιήσετε και κάποιους προκατασκευασμένους χώρους.",
"title": "Πλαϊνή μπάρα"
},
"start_automatically": "Αυτόματη έναρξη μετά τη σύνδεση",
@@ -2249,7 +2329,8 @@
"voice_processing": "Επεξεργασία φωνής",
"voice_section": "Ρυθμίσεις φωνής"
},
- "warn_quit": "Προειδοποιήστε πριν την παραίτηση"
+ "warn_quit": "Προειδοποιήστε πριν την παραίτηση",
+ "warning": "ΠΡΟΕΙΔΟΠΟΙΗΣΗ: "
},
"share": {
"permalink_message": "Σύνδεσμος στο επιλεγμένο μήνυμα",
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 5763e0a781..56f22dc6d7 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2066,6 +2066,7 @@
"read_topic": "Click to read topic",
"rejecting": "Rejecting invite…",
"rejoin_button": "Re-join",
+ "room_content": "Room content",
"room_is_low_priority": "This is a low priority room",
"search": {
"all_rooms_button": "Search all rooms",
diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json
index 9a27318316..24f5661733 100644
--- a/src/i18n/strings/et.json
+++ b/src/i18n/strings/et.json
@@ -2115,6 +2115,7 @@
"add_space_label": "Lisa kogukonnakeskus",
"breadcrumbs_empty": "Hiljuti külastatud jututubasid ei leidu",
"breadcrumbs_label": "Hiljuti külastatud jututoad",
+ "collapse_filters": "Ahenda filtriloendit",
"empty": {
"no_chats": "Vestlusi veel ei leidu",
"no_chats_description": "Alusta sellest, et leia mõni vestluspartner või loo oma jututuba",
@@ -2122,6 +2123,7 @@
"no_favourites": "Sa pole veel ühtegi vestlust märkinud lemmikuks",
"no_favourites_description": "Vestluse saad märkida lemmikuks tema seadistustest",
"no_invites": "Sul pole lugemata kutseid",
+ "no_lowpriority": "Sul pole ühtegi vähetähtsat jututuba",
"no_mentions": "Sul pole lugemata mainimisi",
"no_people": "Sul pole veel ühtegi otsevestlust kellegagi",
"no_people_description": "Kõikide muude vestluste nägemiseks eemalda otsingufiltrid",
@@ -2131,6 +2133,7 @@
"show_activity": "Vaata kõiki tegevusi",
"show_chats": "Näita kõiki vestlusi"
},
+ "expand_filters": "Laienda filtriloendit",
"failed_add_tag": "Sildi %(tagName)s lisamine jututoale ebaõnnestus",
"failed_remove_tag": "Sildi %(tagName)s eemaldamine jututoast ebaõnnestus",
"failed_set_dm_tag": "Otsevestluse sildi seadmine ei õnnestunud",
diff --git a/src/i18n/strings/id.json b/src/i18n/strings/id.json
index 9f7c8b10f8..331a1a597e 100644
--- a/src/i18n/strings/id.json
+++ b/src/i18n/strings/id.json
@@ -784,6 +784,7 @@
"cross_signing_status": "Status penandatanganan silang:",
"cross_signing_untrusted": "Akun Anda memiliki identitas penandatanganan silang dalam penyimpanan rahasia, tetapi belum dipercaya oleh sesi ini.",
"crypto_not_available": "Modul kriptografi tidak tersedia",
+ "device_id": "ID Perangkat",
"key_backup_active_version": "Versi cadangan aktif:",
"key_backup_active_version_none": "Tidak ada",
"key_backup_inactive_warning": "Kunci Anda tidak dicadangkan dari sesi ini.",
@@ -796,6 +797,8 @@
"secret_storage_ready": "siap",
"secret_storage_status": "Penyimpanan rahasia:",
"self_signing_private_key_cached_status": "Kunci pribadi penandatanganan sendiri:",
+ "session": "Sesi",
+ "session_fingerprint": "Sidik jari (kunci sesi)",
"title": "Enkripsi ujung ke ujung",
"user_signing_private_key_cached_status": "Kunci pribadi penandatanganan pengguna:"
},
@@ -821,6 +824,7 @@
"low_bandwidth_mode": "Mode bandwidth rendah",
"low_bandwidth_mode_description": "Membutuhkan homeserver yang kompatibel.",
"main_timeline": "Lini masa utama",
+ "manual_device_verification": "Verifikasi perangkat manual",
"no_receipt_found": "Tidak ada laporan yang ditemukan",
"notification_state": "Keadaan notifikasi adalah %(notificationState)s ",
"notifications_debug": "Pengawakutuan notifikasi",
@@ -1004,6 +1008,21 @@
"incoming_sas_dialog_waiting": "Menunggu pengguna untuk konfirmasi…",
"incoming_sas_user_dialog_text_1": "Verifikasi pengguna ini untuk menandainya sebagai terpercaya. Mempercayai pengguna memberikan Anda ketenangan saat menggunakan pesan terenkripsi secara ujung ke ujung.",
"incoming_sas_user_dialog_text_2": "Memverifikasi pengguna ini akan menandai sesinya sebagai terpercaya, dan juga menandai sesi Anda sebagai terpercaya kepadanya.",
+ "manual": {
+ "already_verified": "Perangkat ini sudah diverifikasi",
+ "already_verified_and_wrong_fingerprint": "Sidik jari yang disediakan tidak cocok, tetapi perangkat sudah diverifikasi!",
+ "device_id": "ID Perangkat",
+ "failure_description": "Gagal memverifikasi '%(deviceId)s': %(error)s",
+ "failure_title": "Verifikasi gagal",
+ "fingerprint": "Sidik jari (kunci sesi)",
+ "no_crypto": "Tidak dapat memverifikasi perangkat - kripto tidak diaktifkan",
+ "no_device": "Tidak dapat memverifikasi perangkat - perangkat %(deviceId)s '' tidak ditemukan",
+ "no_userid": "Tidak dapat memverifikasi perangkat - tidak dapat menemukan ID Pengguna kami",
+ "success_description": "Perangkat (%(deviceId)s) sekarang ditandatangani silang",
+ "success_title": "Verifikasi berhasil",
+ "text": "Berikan ID dan sidik jari salah satu perangkat Anda untuk memverifikasinya. PERHATIKAN bahwa ini memungkinkan perangkat lain untuk mengirim dan menerima pesan seperti Anda. JIKA SESEORANG MEMINTA ANDA UNTUK MENEMPELKAN SESUATU DI SINI, KEMUNGKINAN ANDA SEDANG DITIPU!",
+ "wrong_fingerprint": "Tidak dapat memverifikasi perangkat '%(deviceId)s' - sidik jari yang disediakan '%(fingerprint)s' tidak cocok dengan sidik jari perangkat, '%(fprint)s'"
+ },
"no_key_or_device": "Sepertinya Anda tidak memiliki Kunci Pemulihan atau perangkat lain yang dapat Anda verifikasi. Perangkat ini tidak akan dapat mengakses pesan terenkripsi lama. Untuk memverifikasi identitas Anda di perangkat ini, Anda harus mengatur ulang kunci verifikasi Anda.",
"no_support_qr_emoji": "Perangkat yang Anda sedang verifikasi tidak mendukung pemindaian kode QR atau verifikasi emoji, yang didukung oleh %(brand)s. Coba menggunakan klien yang lain.",
"other_party_cancelled": "Pengguna yang lain membatalkan proses verifikasi ini.",
@@ -1949,6 +1968,7 @@
},
"face_pile_tooltip_shortcut": "Termasuk %(commaSeparatedMembers)s",
"face_pile_tooltip_shortcut_joined": "Termasuk Anda, %(commaSeparatedMembers)s",
+ "failed_determine_user": "Tidak dapat menentukan pengguna mana yang akan diabaikan karena peristiwa anggota telah berubah.",
"failed_reject_invite": "Gagal untuk menolak undangan",
"forget_room": "Lupakan ruangan ini",
"forget_space": "Lupakan space ini",
@@ -2039,6 +2059,7 @@
"read_topic": "Klik untuk membaca topik",
"rejecting": "Menolak undangan…",
"rejoin_button": "Bergabung Ulang",
+ "room_is_low_priority": "Ini adalah ruangan dengan prioritas rendah",
"search": {
"all_rooms_button": "Cari semua ruangan",
"placeholder": "Cari pesan...",
@@ -2086,6 +2107,7 @@
"add_space_label": "Tambahkan space",
"breadcrumbs_empty": "Tidak ada ruangan yang baru saja dilihat",
"breadcrumbs_label": "Ruangan yang baru saja dilihat",
+ "collapse_filters": "Tutup daftar filter",
"empty": {
"no_chats": "Belum ada obrolan",
"no_chats_description": "Mulailah dengan mengirim pesan kepada seseorang atau dengan membuat ruangan",
@@ -2093,6 +2115,7 @@
"no_favourites": "Anda belum memiliki obrolan favorit",
"no_favourites_description": "Anda dapat menambahkan obrolan ke favorit Anda di pengaturan obrolan",
"no_invites": "Anda tidak memiliki undangan yang belum dibaca",
+ "no_lowpriority": "Anda tidak memiliki ruangan dengan prioritas rendah",
"no_mentions": "Anda tidak memiliki sebutan yang belum dibaca",
"no_people": "Anda belum memiliki obrolan langsung dengan siapa pun",
"no_people_description": "Anda dapat membatalkan pilihan saringan untuk melihat percakapan Anda yang lain",
@@ -2102,12 +2125,14 @@
"show_activity": "Lihat semua aktivitas",
"show_chats": "Tampilkan semua obrolan"
},
+ "expand_filters": "Buka daftar filter",
"failed_add_tag": "Gagal menambahkan tag %(tagName)s ke ruangan",
"failed_remove_tag": "Gagal menghapus tanda %(tagName)s dari ruangan",
"failed_set_dm_tag": "Gagal menetapkan tanda pesan langsung",
"filters": {
"favourite": "Favorit",
"invites": "Undangan",
+ "low_priority": "Prioritas rendah",
"mentions": "Sebutan",
"people": "Orang",
"rooms": "Ruangan",
@@ -2675,6 +2700,9 @@
"inline_url_previews_room": "Aktifkan tampilan URL secara bawaan untuk anggota di ruangan ini",
"inline_url_previews_room_account": "Aktifkan tampilan URL secara bawaan (hanya memengaruhi Anda)",
"insert_trailing_colon_mentions": "Tambahkan sebuah karakter titik dua sesudah sebutan pengguna dari awal pesan",
+ "invite_controls": {
+ "default_label": "Izinkan pengguna mengundang Anda ke ruangan"
+ },
"jump_to_bottom_on_send": "Pergi ke bawah lini masa ketika Anda mengirim pesan",
"key_backup": {
"backup_in_progress": "Kunci Anda sedang dicadangkan (cadangan pertama mungkin membutuhkan beberapa menit).",
@@ -2741,6 +2769,7 @@
"show_in_private": "Di ruangan privat",
"show_media": "Selalu tampilkan"
},
+ "not_supported": "Server Anda tidak menerapkan fitur ini.",
"notifications": {
"default_setting_description": "Pengaturan ini akan diterapkan secara bawaan ke semua ruangan Anda.",
"default_setting_section": "Saya ingin diberi tahu (Pengaturan Bawaan)",
@@ -2798,6 +2827,7 @@
"voip": "Panggilan Audio dan Video"
},
"preferences": {
+ "Electron.enableContentProtection": "Mencegah konten jendela agar tidak ditangkap oleh aplikasi lain",
"Electron.enableHardwareAcceleration": "Aktifkan akselerasi perangkat keras (mulai ulang %(appName)s untuk menerapkan)",
"always_show_menu_bar": "Selalu tampilkan bilah menu window",
"autocomplete_delay": "Delay penyelesaian otomatis (md)",
@@ -2970,6 +3000,7 @@
"show_chat_effects": "Tampilkan efek (animasi ketika menerima konfeti, misalnya)",
"show_displayname_changes": "Tampilkan perubahan nama tampilan",
"show_join_leave": "Tampilkan pesan-pesan gabung/keluar (undangan/pengeluaran/cekalan tidak terpengaruh)",
+ "show_message_previews": "Tampilkan pratinjau pesan",
"show_nsfw_content": "Tampilkan konten NSFW",
"show_read_receipts": "Tampilkan laporan dibaca terkirim oleh pengguna lain",
"show_redaction_placeholder": "Tampilkan sebuah penampung untuk pesan terhapus",
@@ -3076,6 +3107,8 @@
"jumptodate": "Pergi ke tanggal yang diberikan di lini masa",
"jumptodate_invalid_input": "Kami tidak dapat mengerti tanggal yang dicantumkan (%(inputDate)s). Coba menggunakan format TTTT-BB-HH.",
"lenny": "Menambahkan ( ͡° ͜ʖ ͡°) ke pesan teks biasa",
+ "manual_device_verification_confirm_description": "Ini akan memungkinkan perangkat lain untuk mengirim dan menerima pesan seperti Anda. JIKA SESEORANG MENYURUH ANDA MENEMPELKAN SESUATU DI SINI, KEMUNGKINAN ANDA SEDANG DITIPU! Apakah Anda yakin ingin memverifikasi perangkat lain ini?",
+ "manual_device_verification_confirm_title": "Perhatian: verifikasi perangkat manual",
"me": "Menampilkan aksi",
"msg": "Mengirim sebuah pesan ke pengguna yang dicantumkan",
"myavatar": "Ubah foto profil Anda dalam semua ruangan",
@@ -3116,7 +3149,7 @@
"upgraderoom": "Meningkatkan ruangan ke versi yang baru",
"upgraderoom_permission_error": "Anda tidak memiliki izin yang dibutuhkan untuk menggunakan perintah ini.",
"usage": "Penggunaan",
- "verify": "Memverifikasi sebuah pengguna, sesi, dan tupel pubkey",
+ "verify": "Verifikasi salah satu perangkat Anda secara manual",
"view": "Menampilkan ruangan dengan alamat yang ditentukan",
"whois": "Menampilkan informasi tentang sebuah pengguna"
},
diff --git a/src/i18n/strings/nb_NO.json b/src/i18n/strings/nb_NO.json
index 799e67b4a6..dd8b57a1b8 100644
--- a/src/i18n/strings/nb_NO.json
+++ b/src/i18n/strings/nb_NO.json
@@ -1020,7 +1020,11 @@
"fingerprint": "Fingeravtrykk (sesjonsnøkkel)",
"no_crypto": "Kan ikke verifisere enheten - krypto er ikke aktivert",
"no_device": "Kunne ikke verifisere enheten - enheten '%(deviceId)s' ble ikke funnet",
- "no_userid": "Kan ikke verifisere enheten - finner ikke bruker-ID"
+ "no_userid": "Kan ikke verifisere enheten - finner ikke bruker-ID",
+ "success_description": "Enheten (%(deviceId)s) er nå krysssignert",
+ "success_title": "Verifiseringen var vellykket",
+ "text": "Oppgi ID-en og fingeravtrykket til en av dine egne enheter for å verifisere det. MERK at dette lar den andre enheten sende og motta meldinger som deg. HVIS NOEN HAR FORTALT DEG BARE Å LIME INN NOE HER, ER DET SANNSYNLIGVIS AT DU BLIR SVINDLET!",
+ "wrong_fingerprint": "Kan ikke verifisere enheten %(deviceId)s '- det medfølgende fingeravtrykket'%(fingerprint)s «samsvarer ikke med enhetens fingeravtrykk»%(fprint)s '"
},
"no_key_or_device": "Det ser ut til at du ikke har en gjenopprettingsnøkkel eller andre enheter du kan verifisere mot. Denne enheten vil ikke kunne få tilgang til gamle krypterte meldinger. For å bekrefte identiteten din på denne enheten, må du tilbakestille verifiseringsnøklene dine.",
"no_support_qr_emoji": "Enheten du prøver å bekrefte støtter ikke skanning av en QR-kode eller emoji-verifikasjon, som er det som %(brand)s støtter. Prøv med en annen klient.",
@@ -2062,6 +2066,7 @@
"read_topic": "Klikk for å lese emnet",
"rejecting": "Avviser invitasjon...",
"rejoin_button": "Bli med igjen",
+ "room_content": "Rominnhold",
"room_is_low_priority": "Dette er et lavt prioritert rom",
"search": {
"all_rooms_button": "Søk i alle rom",
@@ -2111,6 +2116,7 @@
"add_space_label": "Legg til område",
"breadcrumbs_empty": "Ingen nylig besøkte rom",
"breadcrumbs_label": "Nylig besøkte rom",
+ "collapse_filters": "Skjul filterlisten",
"empty": {
"no_chats": "Ingen chatter ennå",
"no_chats_description": "Kom i gang ved å sende meldinger til noen eller ved å opprette et rom",
@@ -2118,6 +2124,7 @@
"no_favourites": "Du har ikke favorittchat ennå",
"no_favourites_description": "Du kan legge til en chat til dine favoritter i chat-innstillingene",
"no_invites": "Du har ingen uleste invitasjoner",
+ "no_lowpriority": "Du har ingen rom med lav prioritet",
"no_mentions": "Du har ingen uleste omtaler",
"no_people": "Du har ikke direkte chatter med noen ennå",
"no_people_description": "Du kan fjerne merket for filtre for å se de andre chattene dine",
@@ -2127,6 +2134,7 @@
"show_activity": "Se alle aktiviteter",
"show_chats": "Vis alle chatter"
},
+ "expand_filters": "Utvid filterlisten",
"failed_add_tag": "Kunne ikke legge til tagg %(tagName)s til rom",
"failed_remove_tag": "Kunne ikke fjerne tagg %(tagName)s fra rommet",
"failed_set_dm_tag": "Kan ikke sette kode på direktemeldingen",
@@ -3107,6 +3115,8 @@
"jumptodate": "Gå til den gitte datoen i tidslinjen",
"jumptodate_invalid_input": "Vi klarte ikke å forstå den gitte datoen (%(inputDate)s). Prøv å bruke formatet ÅÅÅÅ-MM-DD.",
"lenny": "Legger til ( ͡° ͜ʖ ͡°) foran en ren tekstmelding",
+ "manual_device_verification_confirm_description": "Dette vil tillate en annen enhet å sende og motta meldinger som deg. HVIS NOEN BA DEG LIME INN NOE HER, ER DET SANNSYNLIG AT DU BLIR LURT! Er du sikker på at du vil bekrefte denne andre enheten?",
+ "manual_device_verification_confirm_title": "Forsiktig: manuell enhetsverifisering",
"me": "Viser handling",
"msg": "Sender en melding til den angitte brukeren",
"myavatar": "Endrer profilbildet ditt i alle rom",
@@ -3147,7 +3157,7 @@
"upgraderoom": "Oppgraderer et rom til en ny versjon",
"upgraderoom_permission_error": "Du har ikke de rette tilgangene til å bruke denne kommandoen.",
"usage": "Bruk",
- "verify": "Verifiserer en bruker-, økt- og pubkey-tuple",
+ "verify": "Verifiser en av dine egne enheter manuelt",
"view": "Viser rom med oppgitt adresse",
"whois": "Viser informasjon om en bruker"
},
diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json
index 44eeefdf92..126a915ca1 100644
--- a/src/i18n/strings/ru.json
+++ b/src/i18n/strings/ru.json
@@ -762,6 +762,7 @@
"backup_key_not_stored": "не сохранено",
"backup_key_stored": "в секретном хранилище",
"backup_key_stored_status": "Сохраненный резервный ключ:",
+ "backup_key_unexpected_type": "непредвиденный тип",
"backup_key_well_formed": "корректный",
"cross_signing": "Кросс-подпись",
"cross_signing_cached": "сохранено локально",
@@ -775,6 +776,7 @@
"cross_signing_status": "Статус кросс-подписи:",
"cross_signing_untrusted": "У вашей учётной записи есть кросс-подпись в секретное хранилище, но она пока не является доверенной в этом сеансе.",
"crypto_not_available": "Криптографический модуль недоступен",
+ "device_id": "Идентификатор устройства",
"key_backup_active_version": "Активная резервная версия:",
"key_backup_active_version_none": "Нет",
"key_backup_inactive_warning": "Резервное копирование ваших ключей из этого сеанса не выполняется.",
@@ -787,6 +789,8 @@
"secret_storage_ready": "готово",
"secret_storage_status": "Секретное хранилище:",
"self_signing_private_key_cached_status": "Самоподписанный закрытый ключ:",
+ "session": "Сессия",
+ "session_fingerprint": "Отпечаток пальца (ключ сессии)",
"title": "Сквозное шифрование",
"user_signing_private_key_cached_status": "Закрытый ключ подписи пользователей:"
},
@@ -812,6 +816,7 @@
"low_bandwidth_mode": "Режим низкой пропускной способности",
"low_bandwidth_mode_description": "Требуется совместимый сервер.",
"main_timeline": "Основная хронология",
+ "manual_device_verification": "Ручная проверка устройства",
"no_receipt_found": "Квитанция не найдена",
"notification_state": "Состояние уведомления %(notificationState)s ",
"notifications_debug": "Отладка уведомлений",
@@ -996,6 +1001,15 @@
"incoming_sas_dialog_waiting": "Ожидаем подтверждения от партнера…",
"incoming_sas_user_dialog_text_1": "Проверить этого пользователя, чтобы отметить его, как доверенного. Доверенные пользователи дают вам больше уверенности при использовании шифрованных сообщений.",
"incoming_sas_user_dialog_text_2": "Подтверждение этого пользователя сделает его сеанс доверенным у вас, а также сделает ваш сеанс доверенным у него.",
+ "manual": {
+ "already_verified": "Это устройство уже проверено",
+ "already_verified_and_wrong_fingerprint": "Предоставленный отпечаток пальца не совпадает, но устройство уже проверено!",
+ "device_id": "Идентификатор устройства",
+ "failure_description": "Не удалось проверить '%(deviceId)s': %(error)s",
+ "failure_title": "Сбой проверки",
+ "fingerprint": "Отпечаток пальца (ключ сессии)",
+ "success_title": "Проверка прошла успешно"
+ },
"no_key_or_device": "Похоже, у вас нет Ключа Восстановления, или других сеансов, с которыми вы могли бы свериться. В этом сеансе вы не сможете получить доступ к старым зашифрованным сообщениям. Чтобы подтвердить свою личность в этом сеансе, вам нужно будет сбросить свои ключи шифрования.",
"no_support_qr_emoji": "Устройство, которое вы пытаетесь проверить, не поддерживает сканирование QR-кода или проверку смайликов, которые поддерживает %(brand)s. Попробуйте использовать другой клиент.",
"other_party_cancelled": "Другая сторона отменила проверку.",
@@ -1035,7 +1049,7 @@
"unverified_sessions_toast_description": "Проверьте, чтобы убедиться, что ваша учётная запись в безопасности",
"unverified_sessions_toast_reject": "Позже",
"unverified_sessions_toast_title": "У вас есть незаверенные сеансы",
- "verification_description": "Подтвердите свою личность, чтобы получить доступ к зашифрованным сообщениям и доказать свою личность другим.",
+ "verification_description": "Подтвердите свою личность, чтобы получить доступ к зашифрованным сообщениям и подтвердить свою личность другим. Если вы также используете мобильное устройство, откройте приложение там, прежде чем продолжить.",
"verification_dialog_title_device": "Проверить другое устройство",
"verification_dialog_title_user": "Запрос на сверку",
"verification_skip_warning": "Без проверки вы не сможете получить доступ ко всем своим сообщениям и можете показаться другим людям недоверенным.",
@@ -1054,8 +1068,10 @@
"waiting_other_user": "Ожидание %(displayName)s для проверки…"
},
"verification_requested_toast_title": "Запрошено подтверждение",
+ "verified_identity_changed": "Подтвержденная личность %(displayName)s (%(userId)s ) изменилась. Узнайте больше ",
"verify_toast_description": "Другие пользователи могут не доверять этому сеансу",
- "verify_toast_title": "Заверьте этот сеанс"
+ "verify_toast_title": "Заверьте этот сеанс",
+ "withdraw_verification_action": "Подтверждение верификации"
},
"error": {
"admin_contact": "Пожалуйста, обратитесь к вашему администратору , чтобы продолжить использовать этот сервис.",
@@ -1096,13 +1112,14 @@
"unknown_error_code": "неизвестный код ошибки",
"update_power_level": "Не удалось изменить уровень прав"
},
- "error_app_open_in_another_tab": "%(brand)s был открыт в другой вкладке.",
+ "error_app_open_in_another_tab": "Переключитесь на другую вкладку, чтобы подключиться к %(brand)s . Теперь эту вкладку можно закрыть.",
+ "error_app_open_in_another_tab_title": "%(brand)s подключен в другой вкладке",
"error_app_opened_in_another_window": "%(brand)s открыт в другом окне. Нажмите \"%(label)s\" чтобы использовать %(brand)s в данном окне и отключить другое.",
"error_database_closed_description": {
"for_desktop": "Возможно, ваш диск переполнен. Освободите место и перезагрузите компьютер.",
"for_web": "Если вы очистили данные браузера, то это сообщение ожидаемо. %(brand)s также может быть открыт в другой вкладке, или ваш диск заполнен. Пожалуйста, освободите место и перезагрузите"
},
- "error_database_closed_title": "База данных неожиданно закрылась",
+ "error_database_closed_title": "%(brand)s перестал работать",
"error_dialog": {
"copy_room_link_failed": {
"description": "Не удалось скопировать ссылку на комнату в буфер обмена.",
@@ -1141,7 +1158,8 @@
"image": "Изображение",
"poll": "Опрос",
"video": "Видео"
- }
+ },
+ "preview": "%(prefix)s: %(preview)s"
},
"export_chat": {
"cancelled": "Экспорт отменён",
@@ -1266,12 +1284,16 @@
},
"incompatible_browser": {
"continue": "Продолжить в любом случае",
+ "description": "%(brand)s использует некоторые функции браузера, которые недоступны в вашем текущем браузере. %(detail)s",
+ "detail_can_continue": "Если вы продолжите, некоторые функции могут перестать работать, и существует риск потери данных в будущем.",
"detail_no_continue": "Попробуйте обновить этот браузер, если вы используете не последнюю версию, и повторите попытку.",
"learn_more": "Подробнее",
"linux": "Linux",
"macos": "Mac",
+ "supported_browsers": "Для наилучшего впечатления используйте Chrome , Firefox , Edge или Safari .",
"title": "Неподдерживаемый браузер",
"use_desktop_heading": "Вместо этого используйте %(brand)s Desktop",
+ "use_mobile_heading": "Для этого используйте %(brand)s на мобильном телефоне",
"use_mobile_heading_after_desktop": "Или воспользуйтесь нашим мобильным приложением",
"windows_64bit": "Windows (64-бит)",
"windows_arm_64bit": "Windows (ARM 64-бит)"
@@ -1284,8 +1306,8 @@
"explainer": "Менеджеры по интеграции получают данные конфигурации и могут изменять виджеты, отправлять приглашения в комнаты и устанавливать уровни доступа от вашего имени.",
"manage_title": "Управление интеграциями",
"toggle_label": "Включить менеджер интеграции",
- "use_im": "Используйте менеджер интеграций для управления ботами, виджетами и наклейками.",
- "use_im_default": "Используйте менеджер интеграций %(serverName)s для управления ботами, виджетами и наклейками."
+ "use_im": "Используйте менеджер интеграций для управления ботами, виджетами и наборами стикеров.",
+ "use_im_default": "Используйте менеджер интеграций %(serverName)s для управления ботами, виджетами и наборами стикеров."
},
"integrations": {
"disabled_dialog_description": "Включите '%(manageIntegrations)s' в Настройках.",
@@ -1327,7 +1349,7 @@
"name_email_mxid_share_room": "Пригласите кого-нибудь, используя его имя, адрес электронной почты, имя пользователя (например, ) или поделитесь этой комнатой .",
"name_email_mxid_share_space": "Пригласите кого-нибудь, используя их имя, адрес электронной почты, имя пользователя (например, ) или поделитесь этим пространством .",
"name_mxid_share_room": "Пригласите кого-нибудь, используя его имя, имя пользователя (например, ) или поделитесь этой комнатой .",
- "name_mxid_share_space": "Пригласите кого-нибудь, используя их имя, учётную запись (как ) или поделитесь этим пространством .",
+ "name_mxid_share_space": "Пригласите кого-нибудь, используя их отображаемое имя или имя учётной записи (например, ) или поделитесь этим пространством .",
"recents_section": "Недавние Диалоги",
"room_failed_partial": "Мы отправили остальных, но нижеперечисленные люди не могут быть приглашены в ",
"room_failed_partial_title": "Некоторые приглашения не могут быть отправлены",
@@ -2018,7 +2040,10 @@
"pinned_message_badge": "Закреплённое сообщение",
"pinned_message_banner": {
"button_close_list": "Закрыть список",
- "button_view_all": "Посмотреть все"
+ "button_view_all": "Посмотреть все",
+ "description": "В этой комнате есть закрепленные сообщения. Нажмите, чтобы просмотреть их.",
+ "go_to_message": "Показать прикрепленное сообщение на временной шкале.",
+ "title": "%(index)s из %(length)s Закрепленные сообщения"
},
"read_topic": "Нажмите, чтобы увидеть тему",
"rejecting": "Отклонение приглашения…",
@@ -2115,7 +2140,8 @@
"other": "Удаляются сообщения в %(count)s комнатах"
},
"room": {
- "more_options": "Дополнительные параметры"
+ "more_options": "Дополнительные параметры",
+ "open_room": "Открыть комнату %(roomName)s"
},
"show_less": "Показать меньше",
"show_n_more": {
@@ -2198,6 +2224,8 @@
"error_deleting_alias_description": "Произошла ошибка при удалении этого адреса. Возможно, он больше не существует или произошла временная ошибка.",
"error_deleting_alias_description_forbidden": "У вас нет прав для удаления этого адреса.",
"error_deleting_alias_title": "Ошибка при удалении адреса",
+ "error_publishing": "Невозможно опубликовать комнату",
+ "error_publishing_detail": "Произошла ошибка при публикации этой комнаты",
"error_save_space_settings": "Не удалось сохранить настройки пространства.",
"error_updating_alias_description": "Произошла ошибка при обновлении альтернативных адресов комнаты. Это может быть запрещено сервером или произошел временный сбой.",
"error_updating_canonical_alias_description": "При обновлении основного адреса комнаты произошла ошибка. Возможно, это не разрешено сервером или произошел временный сбой.",
@@ -2438,20 +2466,26 @@
},
"settings": {
"account": {
+ "dialog_title": "Настройки: Учетная запись",
"title": "Учетная запись"
},
"all_rooms_home": "Показывать все комнаты на Главной",
"all_rooms_home_description": "Все комнаты, в которых вы находитесь, будут отображаться на Главной.",
"always_show_message_timestamps": "Всегда показывать время отправки сообщений",
"appearance": {
+ "bundled_emoji_font": "Использовать встроенный шрифт эмодзи",
+ "compact_layout": "Показывать компактный текст и сообщения",
+ "compact_layout_description": "Для использования этой функции необходимо выбрать современный макет.",
"custom_font": "Использовать системный шрифт",
"custom_font_description": "Установите имя шрифта, установленного в вашей системе, и %(brand)s попытается его использовать.",
"custom_font_name": "Название системного шрифта",
"custom_font_size": "Использовать другой размер",
"custom_theme_add": "Добавить пользовательскую тему",
"custom_theme_downloading": "Загрузка пользовательской темы…",
- "custom_theme_error_downloading": "Ошибка при загрузке информации темы.",
+ "custom_theme_error_downloading": "Ошибка при загрузке темы",
+ "custom_theme_help": "Введите URL-адрес пользовательской темы, которую вы хотите применить.",
"custom_theme_invalid": "Неверная схема темы.",
+ "dialog_title": "Настройки: Внешний вид",
"font_size": "Размер шрифта",
"font_size_default": "%(fontSize)s (по умолчанию)",
"high_contrast": "Высокая контрастность",
@@ -2506,6 +2540,7 @@
"title": "Вы уверены, что хотите отключить хранение ключей и удалить их?"
},
"device_not_verified_button": "Проверить это устройство",
+ "device_not_verified_description": "Для просмотра настроек шифрования необходимо подтвердить это устройство.",
"device_not_verified_title": "Устройство не проверено",
"dialog_title": "Настройки: Шифрование",
"key_storage": {
@@ -2515,19 +2550,26 @@
},
"recovery": {
"change_recovery_confirm_button": "Подтвердите новый ключ восстановления",
+ "change_recovery_confirm_description": "Чтобы завершить, введите новый Ключ Восстановления ниже. Ваш старый ключ больше работать не будет.",
"change_recovery_confirm_title": "Введите новый ключ восстановления",
"change_recovery_key": "Изменить ключ восстановления",
+ "change_recovery_key_description": "Запишите новый ключ восстановления в безопасном месте. Затем нажмите «Продолжить», чтобы подтвердить изменение.",
"change_recovery_key_title": "Изменить ключ восстановления?",
+ "description": "Восстановите свою идентификацию и историю сообщений с помощью ключа восстановления, если вы потеряли все существующие устройства.",
"enter_key_error": "Ключ восстановления, который вы ввел, неверный.",
"enter_recovery_key": "Введите ключ восстановления",
"forgot_recovery_key": "Забыли ключ восстановления?",
+ "key_storage_warning": "Хранилище ключей не синхронизировано. Нажмите кнопку ниже, чтобы устранить проблему.",
"save_key_description": "Не сообщайте эту информацию никому!",
"save_key_title": "Ключ восстановления",
"set_up_recovery": "Настройка восстановления",
"set_up_recovery_confirm_button": "Завершить настройку",
+ "set_up_recovery_confirm_description": "Введите ключ восстановления, показанный на предыдущем экране, чтобы завершить настройку восстановления.",
"set_up_recovery_confirm_title": "Для подтверждения введите ключ восстановления",
+ "set_up_recovery_description": "Хранилище ключей защищено ключом восстановления. Если после установки вам понадобится новый ключ восстановления, вы можете создать его заново, выбрав '%(changeRecoveryKeyButton)s'.",
"set_up_recovery_save_key_description": "Запишите ключ восстановления в безопасном месте, например в диспетчере паролей, зашифрованной заметке или физическом сейфе.",
"set_up_recovery_save_key_title": "Сохраните ключ восстановления в безопасном месте",
+ "set_up_recovery_secondary_description": "После нажатия кнопки «Продолжить» мы сгенерируем для вас ключ восстановления.",
"title": "Восстановление"
},
"title": "Шифрование"
@@ -2609,6 +2651,7 @@
"password_change_success": "Ваш пароль успешно изменён.",
"personal_info": "Личная информация",
"profile_subtitle": "Так вас видят другие пользователи приложения.",
+ "profile_subtitle_oidc": "Ваша учетная запись управляется отдельным поставщиком идентификационных данных, поэтому некоторые ваши личные данные изменить нельзя.",
"remove_email_prompt": "Удалить %(email)s?",
"remove_msisdn_prompt": "Удалить %(phone)s?",
"spell_check_locale_placeholder": "Выберите регион",
@@ -2638,7 +2681,7 @@
"enter_phrase_title": "Введите секретную фразу",
"enter_phrase_to_confirm": "Введите секретную фразу второй раз, чтобы подтвердить ее.",
"generate_security_key_description": "Мы создадим ключ восстановления, который вы сможете хранить в безопасном месте, например в менеджере паролей или сейфе.",
- "generate_security_key_title": "Создание ключа безопасности",
+ "generate_security_key_title": "Создание Ключа Восстановления",
"pass_phrase_match_failed": "Они не совпадают.",
"pass_phrase_match_success": "Они совпадают!",
"phrase_strong_enough": "Отлично! Эта контрольная фраза выглядит достаточно сильной.",
@@ -2647,11 +2690,11 @@
"set_phrase_again": "Задать другой пароль.",
"settings_reminder": "Вы также можете настроить безопасное резервное копирование и управлять своими ключами в настройках.",
"title_confirm_phrase": "Подтвердите секретную фразу",
- "title_save_key": "Сохраните свой ключ безопасности",
+ "title_save_key": "Сохраните ключ восстановления",
"title_set_phrase": "Задайте секретную фразу",
"unable_to_setup": "Невозможно настроить секретное хранилище",
"use_different_passphrase": "Использовать другую кодовую фразу?",
- "use_phrase_only_you_know": "Используйте секретную фразу, известную только вам, и при необходимости сохраните ключ безопасности для резервного копирования."
+ "use_phrase_only_you_know": "Используйте секретную фразу, известную только вам, и при необходимости сохраните Ключ Восстановления от резервной копии."
}
},
"key_export_import": {
@@ -2741,6 +2784,7 @@
"code_blocks_heading": "Блоки кода",
"compact_modern": "Использовать более компактный \"Современный\" макет",
"composer_heading": "Редактор",
+ "default_timezone": "Браузер по умолчанию (%(timezone)s)",
"dialog_title": "Настройки: Параметры",
"enable_hardware_acceleration": "Включить аппаратное ускорение",
"enable_tray_icon": "Показывать значок в трее и сворачивать в него окно при закрытии",
@@ -2766,6 +2810,7 @@
"bulk_options_accept_all_invites": "Принять все приглашения (%(invitedRooms)s)",
"bulk_options_reject_all_invites": "Отклонить все %(invitedRooms)s приглашения",
"bulk_options_section": "Основные опции",
+ "dehydrated_device_description": "Функция автономного устройства позволяет вам получать зашифрованные сообщения, даже если вы не вошли в систему ни на одном устройстве.",
"dehydrated_device_enabled": "Устройство в автономном режиме",
"dialog_title": "Настройки: Безопасность и конфиденциальность",
"e2ee_default_disabled_warning": "Администратор вашего сервера отключил сквозное шифрование по умолчанию в приватных комнатах и диалогах.",
@@ -3281,6 +3326,7 @@
"download_action_decrypting": "Расшифровка",
"download_action_downloading": "Загрузка",
"download_failed": "Загрузка не удалась",
+ "download_failed_description": "Произошла ошибка при загрузке этого файла",
"e2e_state": "Состояние сквозного шифрования",
"edits": {
"tooltip_label": "Изменено %(date)s. Нажмите для посмотра истории изменений.",
@@ -3436,6 +3482,7 @@
"left_reason": "%(targetName)s покинул(а) комнату: %(reason)s",
"no_change": "%(senderName)s не сделал(а) изменений",
"reject_invite": "%(targetName)s отклонил(а) приглашение",
+ "reject_invite_reason": "%(targetName)s отклонил приглашение: %(reason)s",
"remove_avatar": "%(senderName)s удалил(а) аватар",
"remove_name": "%(senderName)s удалил(а) отображаемое имя (%(oldDisplayName)s)",
"set_avatar": "%(senderName)s установил(а) аватар",
@@ -3472,9 +3519,10 @@
},
"m.room.tombstone": "%(senderDisplayName)s обновил(а) эту комнату.",
"m.room.topic": {
- "changed": "%(senderDisplayName)s изменил(а) тему комнаты на \"%(topic)s\"."
+ "changed": "%(senderDisplayName)s изменил(а) тему комнаты на \"%(topic)s\".",
+ "removed": "%(senderDisplayName)s удалил тему."
},
- "m.sticker": "%(senderDisplayName)s отправил(а) наклейку.",
+ "m.sticker": "%(senderDisplayName)s отправил(а) стикер.",
"m.video": {
"error_decrypting": "Ошибка расшифровки видео"
},
@@ -3524,7 +3572,8 @@
"reactions": {
"add_reaction_prompt": "Отреагировать",
"custom_reaction_fallback_label": "Пользовательская реакция",
- "label": "%(reactors)s отреагировали %(content)s"
+ "label": "%(reactors)s отреагировали %(content)s",
+ "tooltip_caption": "отреагировал с %(shortName)s"
},
"read_receipt_title": {
"one": "Просмотрел %(count)s человек",
@@ -3713,6 +3762,10 @@
"few": "И еще %(count)s...",
"many": "И еще %(count)s..."
},
+ "unsupported_browser": {
+ "description": "Если вы продолжите, некоторые функции могут перестать работать, и существует риск потери данных в будущем. Обновите браузер, чтобы продолжить использование %(brand)s.",
+ "title": "%(brand)s не поддерживает этот браузер"
+ },
"unsupported_server_description": "На этом сервере используется старая версия Matrix. Перейдите на Matrix%(version)s, чтобы использовать %(brand)s ее без ошибок.",
"unsupported_server_title": "Ваш сервер не поддерживается",
"update": {
@@ -3730,6 +3783,12 @@
"toast_title": "Обновление %(brand)s",
"unavailable": "Недоступен"
},
+ "update_room_access_modal": {
+ "description": "Чтобы создать ссылку для совместного доступа, сделайте эту комнату общедоступной и разрешите пользователям запрашивать присоединение . Это позволит гостям присоединиться без приглашения.",
+ "dont_change_description": "Кроме того, вы можете провести звонок в отдельной комнате.",
+ "no_change": "Я не хочу менять уровень доступа.",
+ "title": "Изменить уровень доступа в комнату"
+ },
"upload_failed_generic": "Файл '%(fileName)s' не был загружен.",
"upload_failed_size": "Размер файла '%(fileName)s' превышает допустимый предел загрузки, установленный на этом сервере",
"upload_failed_title": "Сбой отправки файла",
@@ -3739,6 +3798,7 @@
"error_files_too_large": "Эти файлы слишком большие для загрузки. Лимит размера файла составляет %(limit)s.",
"error_some_files_too_large": "Некоторые файлы имеют слишком большой размер , чтобы их можно было загрузить. Лимит размера файла составляет %(limit)s.",
"error_title": "Ошибка загрузки",
+ "not_image": "Выбранный вами файл не является изображением.",
"title": "Загрузка файлов",
"title_progress": "Загрузка файлов (%(current)s из %(total)s)",
"upload_all_button": "Загрузить всё",
@@ -3758,7 +3818,7 @@
"deactivate_confirm_description": "Деактивация этого пользователя приведет к его выходу из системы и запрету повторного входа. Кроме того, они оставит все комнаты, в которых он участник. Это действие безповоротно. Вы уверены, что хотите деактивировать этого пользователя?",
"deactivate_confirm_title": "Деактивировать пользователя?",
"demote_button": "Понижение",
- "demote_self_confirm_description_space": "Вы не сможете отменить это изменение, поскольку вы понижаете свои права, если вы являетесь последним привилегированным пользователем в пространстве, будет невозможно восстановить привилегии вбудущем.",
+ "demote_self_confirm_description_space": "Вы не сможете отменить это изменение, поскольку вы понижаете свои права, если вы являетесь последним привилегированным пользователем в пространстве, будет невозможно восстановить привилегии в будущем.",
"demote_self_confirm_room": "После понижения своих привилегий вы не сможете это отменить. Если вы являетесь последним привилегированным пользователем в этой комнате, выдать права кому-либо заново будет невозможно.",
"demote_self_confirm_title": "Понизить самого себя?",
"disinvite_button_room": "Отозвать приглашение в комнату",
@@ -3770,10 +3830,11 @@
"error_mute_user": "Не удалось заглушить пользователя",
"error_revoke_3pid_invite_description": "Не удалось отозвать приглашение. Возможно, на сервере возникла вре́менная проблема или у вас недостаточно прав для отзыва приглашения.",
"error_revoke_3pid_invite_title": "Не удалось отменить приглашение",
+ "ignore_button": "Игнорировать",
"ignore_confirm_description": "Все сообщения и приглашения от этого пользователя будут скрыты. Вы действительно хотите их игнорировать?",
"ignore_confirm_title": "Игнорировать %(user)s",
"invited_by": "Приглашен %(sender)s",
- "jump_to_rr_button": "Перейти к последнему прочтённому",
+ "jump_to_rr_button": "Перейти к последнему прочитанному сообщению",
"kick_button_room": "Удалить из комнаты",
"kick_button_room_name": "Удалить из %(roomName)s",
"kick_button_space": "Исключить из пространства",
@@ -3797,19 +3858,22 @@
"no_recent_messages_description": "Попробуйте пролистать ленту сообщений вверх, чтобы увидеть, есть ли более ранние.",
"no_recent_messages_title": "Последние сообщения от %(user)s не найдены"
},
- "redact_button": "Удалить последние сообщения",
+ "redact_button": "Удалить сообщения",
"revoke_invite": "Отозвать приглашение",
"room_encrypted": "Сообщения в этой комнате защищены сквозным шифрованием.",
"room_encrypted_detail": "Ваши сообщения в безопасности, ключи для расшифровки есть только у вас и получателя.",
"room_unencrypted": "Сообщения в этой комнате не защищены сквозным шифрованием.",
"room_unencrypted_detail": "В зашифрованных комнатах ваши сообщения в безопасности: только у вас и у получателя есть ключи для расшифровки.",
- "share_button": "Поделиться ссылкой на пользователя",
+ "send_message": "Отправить сообщение",
+ "share_button": "Поделиться профилем",
"unban_button_room": "Разблокировать в комнате",
"unban_button_space": "Разблокировать в пространстве",
"unban_room_confirm_title": "Разблокировать в %(roomName)s",
"unban_space_everything": "Разблокировать их везде, где я могу это сделать",
"unban_space_specific": "Разблокировать их из определённых мест, где я могу это сделать",
"unban_space_warning": "Они не смогут получить доступ к тем местам, где вы не являетесь администратором.",
+ "unignore_button": "Не игнорировать",
+ "verification_unavailable": "Проверка пользователя недоступна",
"verify_button": "Подтвердить пользователя",
"verify_explainer": "Для дополнительной безопасности подтвердите этого пользователя, сравнив одноразовый код на ваших устройствах."
},
@@ -3838,6 +3902,7 @@
"camera_disabled": "Ваша камера выключена",
"camera_enabled": "Ваша камера всё ещё включена",
"cannot_call_yourself_description": "Вы не можете позвонить самому себе.",
+ "close_lobby": "Закрыть лобби",
"connecting": "Подключение",
"connection_lost": "Соединение с сервером потеряно",
"connection_lost_description": "Вы не можете совершать вызовы без подключения к серверу.",
@@ -3851,14 +3916,23 @@
"disabled_no_perms_start_video_call": "У вас нет разрешения для запуска видеозвонка",
"disabled_no_perms_start_voice_call": "У вас нет разрешения для запуска звонка",
"disabled_ongoing_call": "Текущий звонок",
+ "element_call": "Element Call",
"enable_camera": "Включить камеру",
"enable_microphone": "Включить микрофон",
"expand": "Вернуться к звонку",
+ "get_call_link": "Поделиться ссылкой на звонок",
"hangup": "Повесить трубку",
"hide_sidebar_button": "Скрыть боковую панель",
"input_devices": "Устройства ввода",
+ "jitsi_call": "Конференция Jitsi",
"join_button_tooltip_call_full": "Извините — этот вызов в настоящее время заполнен",
+ "legacy_call": "Звонок (устаревший)",
"maximise": "Заполнить экран",
+ "maximise_call": "Развернуть звонок",
+ "metaspace_video_rooms": {
+ "conference_room_section": "Конференции"
+ },
+ "minimise_call": "Свернуть звонок",
"misconfigured_server": "Вызов не состоялся из-за неправильно настроенного сервера",
"misconfigured_server_description": "Попросите администратора вашего домашнего сервера (%(homeserverDomain)s) настроить сервер TURN для надежной работы звонков.",
"misconfigured_server_fallback": "В качестве альтернативы вы можете попробовать использовать общедоступный сервер по адресу , но он не будет таким надежным, и ваш IP-адрес будет передаваться на сервер. Вы также можете управлять этим в настройках.",
@@ -3906,6 +3980,7 @@
"user_is_presenting": "%(sharerName)s показывает",
"video_call": "Видеовызов",
"video_call_started": "Начался видеозвонок",
+ "video_call_using": "Видеозвонок с использованием:",
"voice_call": "Голосовой вызов",
"you_are_presenting": "Вы представляете"
},
@@ -4005,7 +4080,7 @@
"error_need_to_be_logged_in": "Вы должны войти в систему.",
"error_unable_start_audio_stream_description": "Невозможно запустить аудио трансляцию.",
"error_unable_start_audio_stream_title": "Не удалось запустить прямую трансляцию",
- "modal_data_warning": "Данные на этом экране используются %(widgetDomain)s",
+ "modal_data_warning": "Приведенные ниже данные передаются %(widgetDomain)s",
"modal_title_default": "Модальный виджет",
"no_name": "Неизвестное приложение",
"open_id_permissions_dialog": {
@@ -4014,7 +4089,7 @@
"title": "Разрешите этому виджету проверить ваш идентификатор"
},
"popout": "Всплывающий виджет",
- "set_room_layout": "Установить мой макет комнаты для всех",
+ "set_room_layout": "Установить макет для всех",
"shared_data_avatar": "URL-адрес изображения вашего профиля",
"shared_data_device_id": "Идентификатор вашего устройства",
"shared_data_lang": "Ваш язык",
diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json
index 6dedd8e10a..84deb17d98 100644
--- a/src/i18n/strings/sv.json
+++ b/src/i18n/strings/sv.json
@@ -3098,7 +3098,7 @@
"upgraderoom": "Uppgraderar ett rum till en ny version",
"upgraderoom_permission_error": "Du har inte de behörigheter som krävs för att använda det här kommandot.",
"usage": "Användande",
- "verify": "Verifierar en användar-, sessions- och pubkey-tupel",
+ "verify": "Verifiera en av dina egna enheter manuellt",
"view": "Visar rum med den angivna adressen",
"whois": "Visar information om en användare"
},
@@ -3207,6 +3207,7 @@
"heading_without_query": "Sök efter",
"join_button_text": "Gå med i %(roomAddress)s",
"keyboard_scroll_hint": "Använd för att skrolla",
+ "messages_label": "Meddelanden",
"other_rooms_in_space": "Andra rum i %(spaceName)s",
"public_rooms_label": "Offentliga rum",
"public_spaces_label": "Offentliga utrymmen",
diff --git a/test/unit-tests/components/structures/MatrixChat-test.tsx b/test/unit-tests/components/structures/MatrixChat-test.tsx
index 94fc9a925a..1fb705d98c 100644
--- a/test/unit-tests/components/structures/MatrixChat-test.tsx
+++ b/test/unit-tests/components/structures/MatrixChat-test.tsx
@@ -1040,7 +1040,7 @@ describe(" ", () => {
localStorage.removeItem("must_verify_device");
});
- it("should show the complete security screen if unskippable verification is enabled", async () => {
+ it("should show the Complete Security screen if unskippable verification is enabled", async () => {
// Given we have force verification on, and an existing logged-in session
// that is not verified (see beforeEach())
@@ -1053,7 +1053,6 @@ describe(" ", () => {
// Sanity: we are not racing with another screen update, so this heading stays visible
await screen.findByRole("heading", { name: "Verify this device", level: 1 });
});
-
it("should not open app after cancelling device verify if unskippable verification is on", async () => {
// See https://github.com/element-hq/element-web/issues/29230
// We used to allow bypassing force verification by choosing "Verify with
@@ -1081,6 +1080,50 @@ describe(" ", () => {
await screen.findByRole("heading", { name: "Verify this device", level: 1 });
});
+ describe("when query params have a loginToken", () => {
+ const loginToken = "test-login-token";
+ const realQueryParams = {
+ loginToken,
+ };
+
+ let loginClient!: ReturnType;
+ const deviceId = "test-device-id";
+ const accessToken = "test-access-token";
+ const clientLoginResponse = {
+ user_id: userId,
+ device_id: deviceId,
+ access_token: accessToken,
+ };
+
+ beforeEach(() => {
+ localStorage.setItem("mx_sso_hs_url", serverConfig.hsUrl);
+ localStorage.setItem("mx_sso_is_url", serverConfig.isUrl);
+ loginClient = getMockClientWithEventEmitter(getMockClientMethods());
+ // this is used to create a temporary client during login
+ jest.spyOn(MatrixJs, "createClient").mockReturnValue(loginClient);
+
+ loginClient.login.mockClear().mockResolvedValue(clientLoginResponse);
+ });
+
+ it("should show the Complete Security screen after OIDC login if unskippable ver. is on", async () => {
+ // Given force_verification is on (outer describe)
+ // And we just logged in via OIDC (inner describe)
+
+ // When we load the page
+ getComponent({ realQueryParams });
+
+ defaultDispatcher.dispatch({
+ action: "will_start_client",
+ });
+ await waitFor(() =>
+ expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ action: "client_started" }),
+ );
+
+ // Then we are not allowed in - we are being asked to verify
+ await screen.findByRole("heading", { name: "Verify this device", level: 1 });
+ });
+ });
+
function createMockCrypto(): CryptoApi {
return {
getVersion: jest.fn().mockReturnValue("Version 0"),
diff --git a/test/unit-tests/components/structures/__snapshots__/FilePanel-test.tsx.snap b/test/unit-tests/components/structures/__snapshots__/FilePanel-test.tsx.snap
index f311521960..18fafbe3e2 100644
--- a/test/unit-tests/components/structures/__snapshots__/FilePanel-test.tsx.snap
+++ b/test/unit-tests/components/structures/__snapshots__/FilePanel-test.tsx.snap
@@ -20,7 +20,8 @@ exports[`FilePanel renders empty state 1`] = `
should render widgets 1`] = `
should show the empty state when there are no pi
should show two pinned messages 1`] = `
should show two pinned messages 1`] = `
aria-expanded="false"
aria-haspopup="menu"
aria-label="Open menu"
- class="_icon-button_m2erp_8"
+ class="_icon-button_1pz9o_8"
+ data-kind="primary"
data-state="closed"
id="radix-«rv»"
role="button"
@@ -245,7 +248,8 @@ exports[` should show two pinned messages 1`] = `
aria-expanded="false"
aria-haspopup="menu"
aria-label="Open menu"
- class="_icon-button_m2erp_8"
+ class="_icon-button_1pz9o_8"
+ data-kind="primary"
data-state="closed"
id="radix-«r16»"
role="button"
@@ -322,7 +326,8 @@ exports[` unpin all should not allow to unpinall 1`] = `
unpin all should not allow to unpinall 1`] = `
aria-expanded="false"
aria-haspopup="menu"
aria-label="Open menu"
- class="_icon-button_m2erp_8"
+ class="_icon-button_1pz9o_8"
+ data-kind="primary"
data-state="closed"
id="radix-«rtd»"
role="button"
@@ -466,7 +472,8 @@ exports[` unpin all should not allow to unpinall 1`] = `
aria-expanded="false"
aria-haspopup="menu"
aria-label="Open menu"
- class="_icon-button_m2erp_8"
+ class="_icon-button_1pz9o_8"
+ data-kind="primary"
data-state="closed"
id="radix-«rtk»"
role="button"
diff --git a/test/unit-tests/components/views/right_panel/__snapshots__/RoomSummaryCardView-test.tsx.snap b/test/unit-tests/components/views/right_panel/__snapshots__/RoomSummaryCardView-test.tsx.snap
index 10e9ae776d..be6fdd5fcf 100644
--- a/test/unit-tests/components/views/right_panel/__snapshots__/RoomSummaryCardView-test.tsx.snap
+++ b/test/unit-tests/components/views/right_panel/__snapshots__/RoomSummaryCardView-test.tsx.snap
@@ -17,7 +17,8 @@ exports[` has button to edit topic 1`] = `
/>
has button to edit topic 1`] = `
renders the room summary 1`] = `
/>
renders the room topic in the summary 1`] = `
/>
renders the room topic in the summary 1`] = `
with crypto enabled renders 1`] = `
with crypto enabled should render a deactivate button for
renders button with an unread marker when room
compose menu should display the compose menu 1`]
aria-expanded="false"
aria-haspopup="menu"
aria-label="Open space menu"
- class="_icon-button_m2erp_8 mx_SpaceMenu_button"
+ class="_icon-button_1pz9o_8 mx_SpaceMenu_button"
+ data-kind="primary"
data-state="closed"
id="radix-«rc»"
role="button"
@@ -59,7 +60,8 @@ exports[` compose menu should display the compose menu 1`]
aria-haspopup="menu"
aria-label="Room Options"
aria-labelledby="«rg»"
- class="_icon-button_m2erp_8"
+ class="_icon-button_1pz9o_8"
+ data-kind="primary"
data-state="closed"
id="radix-«re»"
role="button"
@@ -90,7 +92,8 @@ exports[` compose menu should display the compose menu 1`]
aria-expanded="false"
aria-haspopup="menu"
aria-label="Add"
- class="_icon-button_m2erp_8"
+ class="_icon-button_1pz9o_8"
+ data-kind="primary"
data-state="closed"
id="radix-«rl»"
role="button"
@@ -148,7 +151,8 @@ exports[` compose menu should not display the compose menu
aria-expanded="false"
aria-haspopup="menu"
aria-label="Open space menu"
- class="_icon-button_m2erp_8 mx_SpaceMenu_button"
+ class="_icon-button_1pz9o_8 mx_SpaceMenu_button"
+ data-kind="primary"
data-state="closed"
id="radix-«ro»"
role="button"
@@ -185,7 +189,8 @@ exports[` compose menu should not display the compose menu
aria-haspopup="menu"
aria-label="Room Options"
aria-labelledby="«rs»"
- class="_icon-button_m2erp_8"
+ class="_icon-button_1pz9o_8"
+ data-kind="primary"
data-state="closed"
id="radix-«rq»"
role="button"
@@ -213,7 +218,8 @@ exports[` compose menu should not display the compose menu
should render 'room options' button 1`] = `
aria-expanded="false"
aria-haspopup="menu"
aria-label="Open space menu"
- class="_icon-button_m2erp_8 mx_SpaceMenu_button"
+ class="_icon-button_1pz9o_8 mx_SpaceMenu_button"
+ data-kind="primary"
data-state="closed"
id="radix-«r0»"
role="button"
@@ -305,7 +312,8 @@ exports[` should render 'room options' button 1`] = `
aria-haspopup="menu"
aria-label="Room Options"
aria-labelledby="«r4»"
- class="_icon-button_m2erp_8"
+ class="_icon-button_1pz9o_8"
+ data-kind="primary"
data-state="closed"
id="radix-«r2»"
role="button"
@@ -336,7 +344,8 @@ exports[` should render 'room options' button 1`] = `
aria-expanded="false"
aria-haspopup="menu"
aria-label="Add"
- class="_icon-button_m2erp_8"
+ class="_icon-button_1pz9o_8"
+ data-kind="primary"
data-state="closed"
id="radix-«r9»"
role="button"
@@ -394,7 +403,8 @@ exports[` space menu should display the space menu 1`] = `
aria-expanded="false"
aria-haspopup="menu"
aria-label="Open space menu"
- class="_icon-button_m2erp_8 mx_SpaceMenu_button"
+ class="_icon-button_1pz9o_8 mx_SpaceMenu_button"
+ data-kind="primary"
data-state="closed"
id="radix-«r28»"
role="button"
@@ -431,7 +441,8 @@ exports[` space menu should display the space menu 1`] = `
aria-haspopup="menu"
aria-label="Room Options"
aria-labelledby="«r2c»"
- class="_icon-button_m2erp_8"
+ class="_icon-button_1pz9o_8"
+ data-kind="primary"
data-state="closed"
id="radix-«r2a»"
role="button"
@@ -462,7 +473,8 @@ exports[` space menu should display the space menu 1`] = `
aria-expanded="false"
aria-haspopup="menu"
aria-label="Add"
- class="_icon-button_m2erp_8"
+ class="_icon-button_1pz9o_8"
+ data-kind="primary"
data-state="closed"
id="radix-«r2h»"
role="button"
@@ -526,7 +538,8 @@ exports[` space menu should not display the space menu 1`]
aria-haspopup="menu"
aria-label="Room Options"
aria-labelledby="«r2m»"
- class="_icon-button_m2erp_8"
+ class="_icon-button_1pz9o_8"
+ data-kind="primary"
data-state="closed"
id="radix-«r2k»"
role="button"
@@ -557,7 +570,8 @@ exports[` space menu should not display the space menu 1`]
aria-expanded="false"
aria-haspopup="menu"
aria-label="Add"
- class="_icon-button_m2erp_8"
+ class="_icon-button_1pz9o_8"
+ data-kind="primary"
data-state="closed"
id="radix-«r2r»"
role="button"
diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListItemMenuView-test.tsx.snap b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListItemMenuView-test.tsx.snap
index 7bb17cbd4f..233819e221 100644
--- a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListItemMenuView-test.tsx.snap
+++ b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListItemMenuView-test.tsx.snap
@@ -12,7 +12,8 @@ exports[` should render the more options menu 1`] = `
aria-haspopup="menu"
aria-label="More Options"
aria-labelledby="«r2»"
- class="_icon-button_m2erp_8"
+ class="_icon-button_1pz9o_8"
+ data-kind="primary"
data-state="closed"
id="radix-«r0»"
role="button"
@@ -43,7 +44,8 @@ exports[` should render the more options menu 1`] = `
aria-haspopup="menu"
aria-label="Notification options"
aria-labelledby="«r9»"
- class="_icon-button_m2erp_8"
+ class="_icon-button_1pz9o_8"
+ data-kind="primary"
data-state="closed"
id="radix-«r7»"
role="button"
@@ -87,7 +89,8 @@ exports[` should render the notification options menu 1`
aria-haspopup="menu"
aria-label="More Options"
aria-labelledby="«ri»"
- class="_icon-button_m2erp_8"
+ class="_icon-button_1pz9o_8"
+ data-kind="primary"
data-state="closed"
id="radix-«rg»"
role="button"
@@ -118,7 +121,8 @@ exports[` should render the notification options menu 1`
aria-haspopup="menu"
aria-label="Notification options"
aria-labelledby="«rp»"
- class="_icon-button_m2erp_8"
+ class="_icon-button_1pz9o_8"
+ data-kind="primary"
data-state="closed"
id="radix-«rn»"
role="button"
diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListOptionsMenu-test.tsx.snap b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListOptionsMenu-test.tsx.snap
index 6b75053753..26f4df265f 100644
--- a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListOptionsMenu-test.tsx.snap
+++ b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListOptionsMenu-test.tsx.snap
@@ -8,7 +8,8 @@ exports[` should match snapshot 1`] = `
aria-haspopup="menu"
aria-label="Room Options"
aria-labelledby="«r2»"
- class="_icon-button_m2erp_8"
+ class="_icon-button_1pz9o_8"
+ data-kind="primary"
data-state="closed"
id="radix-«r0»"
role="button"
diff --git a/test/unit-tests/components/views/rooms/__snapshots__/PinnedEventTile-test.tsx.snap b/test/unit-tests/components/views/rooms/__snapshots__/PinnedEventTile-test.tsx.snap
index 84a4da73ef..26a121a88a 100644
--- a/test/unit-tests/components/views/rooms/__snapshots__/PinnedEventTile-test.tsx.snap
+++ b/test/unit-tests/components/views/rooms/__snapshots__/PinnedEventTile-test.tsx.snap
@@ -35,7 +35,8 @@ exports[` should render pinned event 1`] = `
aria-expanded="false"
aria-haspopup="menu"
aria-label="Open menu"
- class="_icon-button_m2erp_8"
+ class="_icon-button_1pz9o_8"
+ data-kind="primary"
data-state="closed"
id="radix-«r5»"
role="button"
@@ -111,7 +112,8 @@ exports[` should render pinned event with thread info 1`] = `
aria-expanded="false"
aria-haspopup="menu"
aria-label="Open menu"
- class="_icon-button_m2erp_8"
+ class="_icon-button_1pz9o_8"
+ data-kind="primary"
data-state="closed"
id="radix-«rd»"
role="button"
diff --git a/test/unit-tests/components/views/rooms/__snapshots__/ThirdPartyMemberInfo-test.tsx.snap b/test/unit-tests/components/views/rooms/__snapshots__/ThirdPartyMemberInfo-test.tsx.snap
index 75d559f390..3396f75663 100644
--- a/test/unit-tests/components/views/rooms/__snapshots__/ThirdPartyMemberInfo-test.tsx.snap
+++ b/test/unit-tests/components/views/rooms/__snapshots__/ThirdPartyMemberInfo-test.tsx.snap
@@ -20,7 +20,8 @@ exports[` should render invite 1`] = `
should render invite when room in not availabl
custom theme should display custom theme 1`] = `
custom theme should render the custom theme sectio
flow to change the recovery key should display th
>
flow to change the recovery key should display th
flow to set up a recovery key should ask the user
>
flow to set up a recovery key should ask the user
>
flow to set up a recovery key should display info
>
flow to set up a recovery key should display the
>
flow to set up a recovery key should display the
should match snapshot 1`] = `
>
should display the 'forgot recovery key' variant
>
should display the 'sync failed' variant correct
>
should reset the encryption when the continue bu
>
should reset the encryption when the continue bu
>
should display the reset identity panel w
>
should show all activated MetaSpaces in the correct orde
aria-haspopup="menu"
aria-label="Threads"
aria-labelledby="«r12»"
- class="_icon-button_m2erp_8 mx_ThreadsActivityCentreButton"
+ class="_icon-button_1pz9o_8 mx_ThreadsActivityCentreButton"
+ data-kind="primary"
data-state="closed"
id="radix-«r10»"
role="button"
diff --git a/test/unit-tests/components/views/spaces/__snapshots__/ThreadsActivityCentre-test.tsx.snap b/test/unit-tests/components/views/spaces/__snapshots__/ThreadsActivityCentre-test.tsx.snap
index 41eefaf277..2e778b5120 100644
--- a/test/unit-tests/components/views/spaces/__snapshots__/ThreadsActivityCentre-test.tsx.snap
+++ b/test/unit-tests/components/views/spaces/__snapshots__/ThreadsActivityCentre-test.tsx.snap
@@ -151,7 +151,8 @@ exports[`ThreadsActivityCentre should close the release announcement when the TA
aria-haspopup="menu"
aria-label="Threads"
aria-labelledby="«rq»"
- class="_icon-button_m2erp_8 mx_ThreadsActivityCentreButton"
+ class="_icon-button_1pz9o_8 mx_ThreadsActivityCentreButton"
+ data-kind="primary"
data-state="open"
id="radix-«ro»"
role="button"
@@ -487,7 +488,8 @@ exports[`ThreadsActivityCentre should render the release announcement 1`] = `
aria-haspopup="menu"
aria-label="Threads"
aria-labelledby="«ra»"
- class="_icon-button_m2erp_8 mx_ThreadsActivityCentreButton"
+ class="_icon-button_1pz9o_8 mx_ThreadsActivityCentreButton"
+ data-kind="primary"
data-state="closed"
id="radix-«r8»"
role="button"
diff --git a/yarn.lock b/yarn.lock
index cb31dad7fc..8f3173d62d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2255,11 +2255,6 @@
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-14.2.0.tgz#1d6bddb2f777ac1674546467aab6f8584a7f2e71"
integrity sha512-xYbH1Yg8YwfXxGsCVDypiRvSVYPnCybsoRqlBDuAvIOs9tOfmdeeJqN+3VxvLWH28g3CtJs+9Afw8dYSHViTFg==
-"@matrix-org/olm@3.2.15":
- version "3.2.15"
- resolved "https://registry.yarnpkg.com/@matrix-org/olm/-/olm-3.2.15.tgz#55f3c1b70a21bbee3f9195cecd6846b1083451ec"
- integrity sha512-S7lOrndAK9/8qOtaTq/WhttJC/o4GAzdfK0MUPpo8ApzsJEC0QjtwrkC3KBXdFP1cD1MXi/mlKR7aaoVMKgs6Q==
-
"@matrix-org/react-sdk-module-api@^2.4.0":
version "2.5.0"
resolved "https://registry.yarnpkg.com/@matrix-org/react-sdk-module-api/-/react-sdk-module-api-2.5.0.tgz#df774d0ae0c327fbd40f8994bbb13ed35e26c337"
@@ -3836,10 +3831,10 @@
resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-4.0.2.tgz#27363d26446eaa21880ab126fa51fec112e6fd86"
integrity sha512-y13bhPyJ5OzbGRl21F6+Y2adrjyK+mu67yKTx+o8MfmIpJzMSn4KkHZtcujMquWSh0e5ZAufsnk4VYvxbSpr1A==
-"@vector-im/compound-web@^7.11.0":
- version "7.11.0"
- resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-7.11.0.tgz#b7c466e64089320b41f8eaf6f2b30950e9692ca2"
- integrity sha512-lRxXUOQJHdBswhykpNs/J/cBW4fPY1qbwyDexlWxX5zCVAYiuMCWo2tI+Y7/SK4tNbDr7nwoTDRh4H9CO1L5LQ==
+"@vector-im/compound-web@^8.0.0":
+ version "8.0.0"
+ resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-8.0.0.tgz#1eeaf54c253730752393d0e5e7c8cd81030b80af"
+ integrity sha512-VAwCGl0KMjN+qEKflnQOB1LidSsxSDiczEWka1IGKj52EzrNOfY0wlfCs73v+H84zUanYKlgOwnQgWU5at9Q/w==
dependencies:
"@floating-ui/react" "^0.27.0"
"@radix-ui/react-context-menu" "^2.2.1"
@@ -9226,12 +9221,11 @@ matrix-events-sdk@0.0.1:
integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
- version "37.7.0"
- resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/c387f30e5c23c897877dd418545a306e6b8cf0c9"
+ version "37.9.0"
+ resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/4efb27354f466d4e50f09e95f5853780326a2b6c"
dependencies:
"@babel/runtime" "^7.12.5"
"@matrix-org/matrix-sdk-crypto-wasm" "^14.2.0"
- "@matrix-org/olm" "3.2.15"
another-json "^0.2.0"
bs58 "^6.0.0"
content-type "^1.0.4"