diff --git a/playwright/e2e/crypto/dehydration.spec.ts b/playwright/e2e/crypto/dehydration.spec.ts
index a0509434a40..ca6485d88e7 100644
--- a/playwright/e2e/crypto/dehydration.spec.ts
+++ b/playwright/e2e/crypto/dehydration.spec.ts
@@ -6,21 +6,13 @@ 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 Locator, type Page } from "@playwright/test";
-
import { test, expect } from "../../element-web-test";
-import { viewRoomSummaryByName } from "../right-panel/utils";
import { isDendrite } from "../../plugins/homeserver/dendrite";
import { completeCreateSecretStorageDialog, createBot, logIntoElement } from "./utils.ts";
import { type Client } from "../../pages/client.ts";
-const ROOM_NAME = "Test room";
const NAME = "Alice";
-function getMemberTileByName(page: Page, name: string): Locator {
- return page.locator(`.mx_MemberTileView, [title="${name}"]`);
-}
-
test.use({
displayName: NAME,
synapseConfig: {
@@ -70,23 +62,6 @@ test.describe("Dehydration", () => {
// device.
const sessionsTab = await app.settings.openUserSettings("Sessions");
await expect(sessionsTab.getByText("Dehydrated device")).not.toBeVisible();
-
- await app.settings.closeDialog();
-
- // now check that the user info right-panel shows the dehydrated device
- // as a feature rather than as a normal device
- await app.client.createRoom({ name: ROOM_NAME });
-
- await viewRoomSummaryByName(page, app, ROOM_NAME);
-
- await page.locator(".mx_RightPanel").getByRole("menuitem", { name: "People" }).click();
- await expect(page.locator(".mx_MemberListView")).toBeVisible();
-
- await getMemberTileByName(page, NAME).click();
- await page.locator(".mx_UserInfo_devices .mx_UserInfo_expand").click();
-
- await expect(page.locator(".mx_UserInfo_devices").getByText("Offline device enabled")).toBeVisible();
- await expect(page.locator(".mx_UserInfo_devices").getByText("Dehydrated device")).not.toBeVisible();
});
test("Reset recovery key during login re-creates dehydrated device", async ({
diff --git a/playwright/e2e/crypto/event-shields.spec.ts b/playwright/e2e/crypto/event-shields.spec.ts
index 6c33554a947..7ac0df28ecb 100644
--- a/playwright/e2e/crypto/event-shields.spec.ts
+++ b/playwright/e2e/crypto/event-shields.spec.ts
@@ -17,6 +17,7 @@ import {
logIntoElement,
logOutOfElement,
verify,
+ waitForDevices,
} from "./utils";
import { bootstrapCrossSigningForClient } from "../../pages/client.ts";
import { type ElementAppPage } from "../../pages/ElementAppPage.ts";
@@ -144,25 +145,8 @@ test.describe("Cryptography", function () {
// bob deletes his second device
await bobSecondDevice.evaluate((cli) => cli.logout(true));
- // wait for the logout to propagate. Workaround for https://github.com/vector-im/element-web/issues/26263 by repeatedly closing and reopening Bob's user info.
- async function awaitOneDevice(iterations = 1) {
- const rightPanel = page.locator(".mx_RightPanel");
- await rightPanel.getByTestId("base-card-back-button").click();
- await rightPanel.getByText("Bob").click();
- const sessionCountText = await rightPanel
- .locator(".mx_UserInfo_devices")
- .getByText(" session", { exact: false })
- .textContent();
- // cf https://github.com/vector-im/element-web/issues/26279: Element-R uses the wrong text here
- if (sessionCountText != "1 session" && sessionCountText != "1 verified session") {
- if (iterations >= 10) {
- throw new Error(`Bob still has ${sessionCountText} after 10 iterations`);
- }
- await awaitOneDevice(iterations + 1);
- }
- }
-
- await awaitOneDevice();
+ // wait for the logout to propagate.
+ await waitForDevices(app, bob.credentials.userId, 1);
// close and reopen the room, to get the shield to update.
await app.viewRoomByName("Bob");
@@ -285,11 +269,7 @@ test.describe("Cryptography", function () {
// Workaround for https://github.com/element-hq/element-web/issues/28640:
// make sure that Alice has seen Bob's identity before she goes offline. We do this by opening
// his user info.
- await app.toggleRoomInfoPanel();
- const rightPanel = page.locator(".mx_RightPanel");
- await rightPanel.getByRole("menuitem", { name: "People" }).click();
- await rightPanel.getByRole("button", { name: bob.credentials!.userId }).click();
- await expect(rightPanel.locator(".mx_UserInfo_devices")).toContainText("1 session");
+ await waitForDevices(app, bob.credentials.userId, 1);
// Our app is blocked from syncing while Bob sends his messages.
await app.client.network.goOffline();
diff --git a/playwright/e2e/crypto/user-verification.spec.ts b/playwright/e2e/crypto/user-verification.spec.ts
index 46bdefb8fb5..ebe86c0a6e0 100644
--- a/playwright/e2e/crypto/user-verification.spec.ts
+++ b/playwright/e2e/crypto/user-verification.spec.ts
@@ -8,9 +8,8 @@ Please see LICENSE files in the repository root for full details.
import { type Preset, type Visibility } from "matrix-js-sdk/src/matrix";
-import type { Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";
-import { doTwoWaySasVerification, awaitVerifier } from "./utils";
+import { doTwoWaySasVerification, awaitVerifier, waitForDevices } from "./utils";
import { type Client } from "../../pages/client";
test.describe("User verification", () => {
@@ -33,13 +32,17 @@ test.describe("User verification", () => {
});
test("can receive a verification request when there is no existing DM", async ({
+ app,
page,
bot: bob,
user: aliceCredentials,
toasts,
room: { roomId: dmRoomId },
}) => {
- await waitForDeviceKeys(page);
+ await waitForDevices(app, bob.credentials.userId, 1);
+ await expect(page.getByRole("button", { name: "Avatar" })).toBeVisible();
+ const avatar = page.getByRole("button", { name: "Avatar" });
+ await avatar.click();
// once Alice has joined, Bob starts the verification
const bobVerificationRequest = await bob.evaluateHandle(
@@ -84,13 +87,17 @@ test.describe("User verification", () => {
});
test("can abort emoji verification when emoji mismatch", async ({
+ app,
page,
bot: bob,
user: aliceCredentials,
toasts,
room: { roomId: dmRoomId },
}) => {
- await waitForDeviceKeys(page);
+ await waitForDevices(app, bob.credentials.userId, 1);
+ await expect(page.getByRole("button", { name: "Avatar" })).toBeVisible();
+ const avatar = page.getByRole("button", { name: "Avatar" });
+ await avatar.click();
// once Alice has joined, Bob starts the verification
const bobVerificationRequest = await bob.evaluateHandle(
@@ -154,15 +161,3 @@ async function createDMRoom(client: Client, userId: string): Promise {
],
});
}
-
-/**
- * Wait until we get the other user's device keys.
- * In newer rust-crypto versions, the verification request will be ignored if we
- * don't have the sender's device keys.
- */
-async function waitForDeviceKeys(page: Page): Promise {
- await expect(page.getByRole("button", { name: "Avatar" })).toBeVisible();
- const avatar = await page.getByRole("button", { name: "Avatar" });
- await avatar.click();
- await expect(page.getByText("1 session")).toBeVisible();
-}
diff --git a/playwright/e2e/crypto/utils.ts b/playwright/e2e/crypto/utils.ts
index ccdb320b94f..da2d00e9060 100644
--- a/playwright/e2e/crypto/utils.ts
+++ b/playwright/e2e/crypto/utils.ts
@@ -499,3 +499,31 @@ export async function deleteCachedSecrets(page: Page) {
});
await page.reload();
}
+
+/**
+ * Wait until the given user has a given number of devices.
+ * This function will check the device keys ten times and if
+ * the expected number of devices were not found by then, an
+ * error is thrown.
+ */
+export async function waitForDevices(
+ app: ElementAppPage,
+ userId: string,
+ expectedNumberOfDevices: number,
+): Promise {
+ const result = await app.client.evaluate(
+ async (cli, { userId, expectedNumberOfDevices }) => {
+ for (let i = 0; i < 10; ++i) {
+ const userDeviceMap = await cli.getCrypto()?.getUserDeviceInfo([userId], true);
+ const deviceMap = userDeviceMap?.get(userId);
+ if (deviceMap.size === expectedNumberOfDevices) return true;
+ await new Promise((r) => setTimeout(r, 500));
+ }
+ return false;
+ },
+ { userId, expectedNumberOfDevices },
+ );
+ if (!result) {
+ throw new Error(`User ${userId} did not have ${expectedNumberOfDevices} devices within ten iterations!`);
+ }
+}
diff --git a/playwright/e2e/user-view/user-view.spec.ts b/playwright/e2e/user-view/user-view.spec.ts
index f3745e78595..de97133e6a0 100644
--- a/playwright/e2e/user-view/user-view.spec.ts
+++ b/playwright/e2e/user-view/user-view.spec.ts
@@ -19,7 +19,6 @@ test.describe("UserView", () => {
const rightPanel = page.locator("#mx_RightPanel");
await expect(rightPanel.getByRole("heading", { name: bot.credentials.displayName, exact: true })).toBeVisible();
- await expect(rightPanel.getByText("1 session")).toBeVisible();
await expect(rightPanel).toMatchScreenshot("user-info.png", {
mask: [page.locator(".mx_UserInfo_profile_mxid")],
css: `
diff --git a/playwright/snapshots/user-view/user-view.spec.ts/user-info-linux.png b/playwright/snapshots/user-view/user-view.spec.ts/user-info-linux.png
index 4e305879350..bd474845962 100644
Binary files a/playwright/snapshots/user-view/user-view.spec.ts/user-info-linux.png and b/playwright/snapshots/user-view/user-view.spec.ts/user-info-linux.png differ
diff --git a/res/css/views/right_panel/_UserInfo.pcss b/res/css/views/right_panel/_UserInfo.pcss
index 7a67986ae83..7fccd6e2d10 100644
--- a/res/css/views/right_panel/_UserInfo.pcss
+++ b/res/css/views/right_panel/_UserInfo.pcss
@@ -37,10 +37,6 @@ Please see LICENSE files in the repository root for full details.
padding: var(--cpd-space-2x) 0 var(--cpd-space-4x);
margin: 0 var(--cpd-space-4x);
- .mx_UserInfo_container_verifyButton {
- margin-top: $spacing-8;
- }
-
& + .mx_UserInfo_container {
border-top: 1px solid $separator;
}
@@ -180,6 +176,28 @@ Please see LICENSE files in the repository root for full details.
opacity: 1;
}
+ .mx_UserInfo_verification {
+ margin-top: var(--cpd-space-4x);
+ height: 36px;
+
+ .mx_UserInfo_verified_badge {
+ min-width: 68px;
+ height: 20px;
+
+ .mx_UserInfo_verified_icon {
+ flex-shrink: 0;
+ }
+
+ .mx_UserInfo_verified_label {
+ margin: 0;
+ }
+ }
+
+ .mx_UserInfo_verification_unavailable {
+ color: var(--cpd-color-text-secondary);
+ }
+ }
+
.mx_UserInfo_memberDetails {
.mx_UserInfo_profileField {
display: flex;
@@ -226,45 +244,6 @@ Please see LICENSE files in the repository root for full details.
flex: 1 1 0;
}
- .mx_UserInfo_devices {
- .mx_UserInfo_device {
- display: flex;
- margin: $spacing-8 0;
-
- &.mx_UserInfo_device_verified {
- .mx_UserInfo_device_trusted {
- color: $accent;
- }
- }
- &.mx_UserInfo_device_unverified {
- .mx_UserInfo_device_trusted {
- color: $alert;
- }
- }
-
- .mx_UserInfo_device_name {
- flex: 1;
- margin: 0 5px;
- word-break: break-word;
- }
- }
-
- /* both for icon in expand button and device item */
- .mx_E2EIcon {
- /* don't squeeze */
- flex: 0 0 auto;
- margin: 0;
- width: 12px;
- height: 12px;
- }
-
- .mx_UserInfo_expand {
- column-gap: 5px; /* cf: mx_UserInfo_device_name */
- margin-bottom: 11px;
- align-items: initial; /* Cancel the default property */
- }
- }
-
&.mx_UserInfo_smallAvatar {
.mx_UserInfo_avatar {
.mx_UserInfo_avatar_transition {
diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx
index e7ae5761502..276c5b3bbd5 100644
--- a/src/components/views/right_panel/UserInfo.tsx
+++ b/src/components/views/right_panel/UserInfo.tsx
@@ -25,7 +25,8 @@ import {
import { KnownMembership } from "matrix-js-sdk/src/types";
import { type UserVerificationStatus, type VerificationRequest, CryptoEvent } from "matrix-js-sdk/src/crypto-api";
import { logger } from "matrix-js-sdk/src/logger";
-import { Heading, MenuItem, Text, Tooltip } from "@vector-im/compound-web";
+import { Badge, Button, Heading, InlineSpinner, MenuItem, Text, Tooltip } from "@vector-im/compound-web";
+import VerifiedIcon from "@vector-im/compound-design-tokens/assets/web/icons/verified";
import ChatIcon from "@vector-im/compound-design-tokens/assets/web/icons/chat";
import CheckIcon from "@vector-im/compound-design-tokens/assets/web/icons/check";
import ShareIcon from "@vector-im/compound-design-tokens/assets/web/icons/share";
@@ -42,21 +43,19 @@ import dis from "../../../dispatcher/dispatcher";
import Modal from "../../../Modal";
import { _t, UserFriendlyError } from "../../../languageHandler";
import DMRoomMap from "../../../utils/DMRoomMap";
-import AccessibleButton, { type ButtonEvent } from "../elements/AccessibleButton";
+import { type ButtonEvent } from "../elements/AccessibleButton";
import SdkConfig from "../../../SdkConfig";
import MultiInviter from "../../../utils/MultiInviter";
-import E2EIcon from "../rooms/E2EIcon";
import { useTypedEventEmitter } from "../../../hooks/useEventEmitter";
import { textualPowerLevel } from "../../../Roles";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
import EncryptionPanel from "./EncryptionPanel";
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
-import { verifyDevice, verifyUser } from "../../../verification";
+import { verifyUser } from "../../../verification";
import { Action } from "../../../dispatcher/actions";
import { useIsEncrypted } from "../../../hooks/useIsEncrypted";
import BaseCard from "./BaseCard";
-import { E2EStatus } from "../../../utils/ShieldUtils";
import ImageView from "../elements/ImageView";
import Spinner from "../elements/Spinner";
import PowerSelector from "../elements/PowerSelector";
@@ -81,7 +80,6 @@ import PosthogTrackers from "../../../PosthogTrackers";
import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { DirectoryMember, startDmOnFirstMessage } from "../../../utils/direct-messages";
import { SdkContextClass } from "../../../contexts/SDKContext";
-import { asyncSome } from "../../../utils/arrays";
import { Flex } from "../../utils/Flex";
import CopyableText from "../elements/CopyableText";
import { useUserTimezone } from "../../../hooks/useUserTimezone";
@@ -107,32 +105,6 @@ export const disambiguateDevices = (devices: IDevice[]): void => {
}
};
-export const getE2EStatus = async (
- cli: MatrixClient,
- userId: string,
- devices: IDevice[],
-): Promise => {
- const crypto = cli.getCrypto();
- if (!crypto) return undefined;
- const isMe = userId === cli.getUserId();
- const userTrust = await crypto.getUserVerificationStatus(userId);
- if (!userTrust.isCrossSigningVerified()) {
- return userTrust.wasCrossSigningVerified() ? E2EStatus.Warning : E2EStatus.Normal;
- }
-
- const anyDeviceUnverified = await asyncSome(devices, async (device) => {
- const { deviceId } = device;
- // For your own devices, we use the stricter check of cross-signing
- // verification to encourage everyone to trust their own devices via
- // cross-signing so that other users can then safely trust you.
- // For other people's devices, the more general verified check that
- // includes locally verified devices can be used.
- const deviceTrust = await crypto.getDeviceVerificationStatus(userId, deviceId);
- return isMe ? !deviceTrust?.crossSigningVerified : !deviceTrust?.isVerified();
- });
- return anyDeviceUnverified ? E2EStatus.Warning : E2EStatus.Verified;
-};
-
/**
* Converts the member to a DirectoryMember and starts a DM with them.
*/
@@ -146,251 +118,6 @@ async function openDmForUser(matrixClient: MatrixClient, user: Member): Promise<
await startDmOnFirstMessage(matrixClient, [startDmUser]);
}
-type SetUpdating = (updating: boolean) => void;
-
-function useHasCrossSigningKeys(
- cli: MatrixClient,
- member: User,
- canVerify: boolean,
- setUpdating: SetUpdating,
-): boolean | undefined {
- return useAsyncMemo(async () => {
- if (!canVerify) {
- return undefined;
- }
- setUpdating(true);
- try {
- return await cli.getCrypto()?.userHasCrossSigningKeys(member.userId, true);
- } finally {
- setUpdating(false);
- }
- }, [cli, member, canVerify]);
-}
-
-/**
- * Display one device and the related actions
- * @param userId current user id
- * @param device device to display
- * @param isUserVerified false when the user is not verified
- * @constructor
- */
-export function DeviceItem({
- userId,
- device,
- isUserVerified,
-}: {
- userId: string;
- device: IDevice;
- isUserVerified: boolean;
-}): JSX.Element {
- const cli = useContext(MatrixClientContext);
- const isMe = userId === cli.getUserId();
-
- /** is the device verified? */
- const isVerified = useAsyncMemo(async () => {
- const deviceTrust = await cli.getCrypto()?.getDeviceVerificationStatus(userId, device.deviceId);
- if (!deviceTrust) return false;
-
- // For your own devices, we use the stricter check of cross-signing
- // verification to encourage everyone to trust their own devices via
- // cross-signing so that other users can then safely trust you.
- // For other people's devices, the more general verified check that
- // includes locally verified devices can be used.
- return isMe ? deviceTrust.crossSigningVerified : deviceTrust.isVerified();
- }, [cli, userId, device]);
-
- const classes = classNames("mx_UserInfo_device", {
- mx_UserInfo_device_verified: isVerified,
- mx_UserInfo_device_unverified: !isVerified,
- });
- const iconClasses = classNames("mx_E2EIcon", {
- mx_E2EIcon_normal: !isUserVerified,
- mx_E2EIcon_verified: isVerified,
- mx_E2EIcon_warning: isUserVerified && !isVerified,
- });
-
- const onDeviceClick = (): void => {
- const user = cli.getUser(userId);
- if (user) {
- verifyDevice(cli, user, device);
- }
- };
-
- let deviceName;
- if (!device.displayName?.trim()) {
- deviceName = device.deviceId;
- } else {
- deviceName = device.ambiguous ? device.displayName + " (" + device.deviceId + ")" : device.displayName;
- }
-
- let trustedLabel: string | undefined;
- if (isUserVerified) trustedLabel = isVerified ? _t("common|trusted") : _t("common|not_trusted");
-
- if (isVerified === undefined) {
- // we're still deciding if the device is verified
- return ;
- } else if (isVerified) {
- return (
-
-
-
{deviceName}
-
{trustedLabel}
-
- );
- } else {
- return (
-
-
-
{deviceName}
-
{trustedLabel}
-
- );
- }
-}
-
-/**
- * Display a list of devices
- * @param devices devices to display
- * @param userId current user id
- * @param loading displays a spinner instead of the device section
- * @param isUserVerified is false when
- * - the user is not verified, or
- * - `MatrixClient.getCrypto.getUserVerificationStatus` async call is in progress (in which case `loading` will also be `true`)
- * @constructor
- */
-function DevicesSection({
- devices,
- userId,
- loading,
- isUserVerified,
-}: {
- devices: IDevice[];
- userId: string;
- loading: boolean;
- isUserVerified: boolean;
-}): JSX.Element {
- const cli = useContext(MatrixClientContext);
-
- const [isExpanded, setExpanded] = useState(false);
-
- const deviceTrusts = useAsyncMemo(() => {
- const cryptoApi = cli.getCrypto();
- if (!cryptoApi) return Promise.resolve(undefined);
- return Promise.all(devices.map((d) => cryptoApi.getDeviceVerificationStatus(userId, d.deviceId)));
- }, [cli, userId, devices]);
-
- if (loading || deviceTrusts === undefined) {
- // still loading
- return ;
- }
- const isMe = userId === cli.getUserId();
-
- let expandSectionDevices: IDevice[] = [];
- const unverifiedDevices: IDevice[] = [];
-
- let expandCountCaption;
- let expandHideCaption;
- let expandIconClasses = "mx_E2EIcon";
-
- const dehydratedDeviceIds: string[] = [];
- for (const device of devices) {
- if (device.dehydrated) {
- dehydratedDeviceIds.push(device.deviceId);
- }
- }
- // If the user has exactly one device marked as dehydrated, we consider
- // that as the dehydrated device, and hide it as a normal device (but
- // indicate that the user is using a dehydrated device). If the user has
- // more than one, that is anomalous, and we show all the devices so that
- // nothing is hidden.
- const dehydratedDeviceId: string | undefined = dehydratedDeviceIds.length == 1 ? dehydratedDeviceIds[0] : undefined;
- let dehydratedDeviceInExpandSection = false;
-
- if (isUserVerified) {
- for (let i = 0; i < devices.length; ++i) {
- const device = devices[i];
- const deviceTrust = deviceTrusts[i];
- // For your own devices, we use the stricter check of cross-signing
- // verification to encourage everyone to trust their own devices via
- // cross-signing so that other users can then safely trust you.
- // For other people's devices, the more general verified check that
- // includes locally verified devices can be used.
- const isVerified = deviceTrust && (isMe ? deviceTrust.crossSigningVerified : deviceTrust.isVerified());
-
- if (isVerified) {
- // don't show dehydrated device as a normal device, if it's
- // verified
- if (device.deviceId === dehydratedDeviceId) {
- dehydratedDeviceInExpandSection = true;
- } else {
- expandSectionDevices.push(device);
- }
- } else {
- unverifiedDevices.push(device);
- }
- }
- expandCountCaption = _t("user_info|count_of_verified_sessions", { count: expandSectionDevices.length });
- expandHideCaption = _t("user_info|hide_verified_sessions");
- expandIconClasses += " mx_E2EIcon_verified";
- } else {
- if (dehydratedDeviceId) {
- devices = devices.filter((device) => device.deviceId !== dehydratedDeviceId);
- dehydratedDeviceInExpandSection = true;
- }
- expandSectionDevices = devices;
- expandCountCaption = _t("user_info|count_of_sessions", { count: devices.length });
- expandHideCaption = _t("user_info|hide_sessions");
- expandIconClasses += " mx_E2EIcon_normal";
- }
-
- let expandButton;
- if (expandSectionDevices.length) {
- if (isExpanded) {
- expandButton = (
- setExpanded(false)}>
-
+ );
+ } else {
+ content = (
+
+ ({_t("user_info|verification_unavailable")})
+
+ );
+ }
+
+ return (
+
+ {content}
+
+ );
+};
+
const BasicUserInfo: React.FC<{
room: Room;
member: User | RoomMember;
- devices: IDevice[];
- isRoomEncrypted: boolean;
-}> = ({ room, member, devices, isRoomEncrypted }) => {
+}> = ({ room, member }) => {
const cli = useContext(MatrixClientContext);
const powerLevels = useRoomPowerLevels(cli, room);
@@ -1503,111 +1302,10 @@ const BasicUserInfo: React.FC<{
spinner = ;
}
- // only display the devices list if our client supports E2E
- const cryptoEnabled = Boolean(cli.getCrypto());
-
- let text;
- if (!isRoomEncrypted) {
- if (!cryptoEnabled) {
- text = _t("encryption|unsupported");
- } else if (room && !room.isSpaceRoom()) {
- text = _t("user_info|room_unencrypted");
- }
- } else if (!room.isSpaceRoom()) {
- text = _t("user_info|room_encrypted");
- }
-
- let verifyButton;
- const homeserverSupportsCrossSigning = useHomeserverSupportsCrossSigning(cli);
-
- const userTrust = useAsyncMemo(
- async () => cli.getCrypto()?.getUserVerificationStatus(member.userId),
- [member.userId],
- // the user verification status is not initialized
- undefined,
- );
- const hasUserVerificationStatus = Boolean(userTrust);
- const isUserVerified = Boolean(userTrust?.isVerified());
const isMe = member.userId === cli.getUserId();
- const canVerify =
- hasUserVerificationStatus &&
- homeserverSupportsCrossSigning &&
- !isUserVerified &&
- !isMe &&
- devices &&
- devices.length > 0;
-
- const setUpdating: SetUpdating = (updating) => {
- setPendingUpdateCount((count) => count + (updating ? 1 : -1));
- };
- const hasCrossSigningKeys = useHasCrossSigningKeys(cli, member as User, canVerify, setUpdating);
-
- // Display the spinner only when
- // - the devices are not populated yet, or
- // - the crypto is available and we don't have the user verification status yet
- const showDeviceListSpinner = (cryptoEnabled && !hasUserVerificationStatus) || devices === undefined;
- if (canVerify) {
- if (hasCrossSigningKeys !== undefined) {
- // Note: mx_UserInfo_verifyButton is for the end-to-end tests
- verifyButton = (
-
- verifyUser(cli, member as User)}
- >
- {_t("action|verify")}
-
-
- );
- } else if (!showDeviceListSpinner) {
- // HACK: only show a spinner if the device section spinner is not shown,
- // to avoid showing a double spinner
- // We should ask for a design that includes all the different loading states here
- verifyButton = ;
- }
- }
-
- let editDevices;
- if (member.userId == cli.getUserId()) {
- editDevices = (
-