Add a devtool for looking at users and their devices (#30983)
* add devtool for viewing users and their devices * show number of devices * apply changes from review * Fix typo Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --------- Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
This commit is contained in:
co-authored by
Richard van der Hoff
parent
5888dfd29d
commit
b7db85146f
Binary file not shown.
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 52 KiB |
@@ -13,6 +13,10 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_E2EIcon.mx_E2EIcon_inline {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_E2EIcon_warning,
|
.mx_E2EIcon_warning,
|
||||||
.mx_E2EIcon_normal,
|
.mx_E2EIcon_normal,
|
||||||
.mx_E2EIcon_verified {
|
.mx_E2EIcon_verified {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import SettingExplorer from "./devtools/SettingExplorer";
|
|||||||
import { RoomStateExplorer } from "./devtools/RoomState";
|
import { RoomStateExplorer } from "./devtools/RoomState";
|
||||||
import BaseTool, { DevtoolsContext, type IDevtoolsProps } from "./devtools/BaseTool";
|
import BaseTool, { DevtoolsContext, type IDevtoolsProps } from "./devtools/BaseTool";
|
||||||
import WidgetExplorer from "./devtools/WidgetExplorer";
|
import WidgetExplorer from "./devtools/WidgetExplorer";
|
||||||
|
import { UserList } from "./devtools/Users";
|
||||||
import { AccountDataExplorer, RoomAccountDataExplorer } from "./devtools/AccountData";
|
import { AccountDataExplorer, RoomAccountDataExplorer } from "./devtools/AccountData";
|
||||||
import SettingsFlag from "../elements/SettingsFlag";
|
import SettingsFlag from "../elements/SettingsFlag";
|
||||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||||
@@ -46,6 +47,7 @@ const Tools: Record<Category, [label: TranslationKey, tool: Tool][]> = {
|
|||||||
[_td("devtools|view_servers_in_room"), ServersInRoom],
|
[_td("devtools|view_servers_in_room"), ServersInRoom],
|
||||||
[_td("devtools|notifications_debug"), RoomNotifications],
|
[_td("devtools|notifications_debug"), RoomNotifications],
|
||||||
[_td("devtools|active_widgets"), WidgetExplorer],
|
[_td("devtools|active_widgets"), WidgetExplorer],
|
||||||
|
[_td("devtools|users"), UserList],
|
||||||
],
|
],
|
||||||
[Category.Other]: [
|
[Category.Other]: [
|
||||||
[_td("devtools|explore_account_data"), AccountDataExplorer],
|
[_td("devtools|explore_account_data"), AccountDataExplorer],
|
||||||
|
|||||||
@@ -0,0 +1,359 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file Devtool for viewing room members and their devices.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { type JSX, useContext, useState } from "react";
|
||||||
|
import { type Device, type RoomMember } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { type CryptoApi } from "matrix-js-sdk/src/crypto-api";
|
||||||
|
|
||||||
|
import { _t } from "../../../../languageHandler";
|
||||||
|
import BaseTool, { DevtoolsContext, type IDevtoolsProps } from "./BaseTool";
|
||||||
|
import FilteredList from "./FilteredList";
|
||||||
|
import LabelledToggleSwitch from "../../elements/LabelledToggleSwitch";
|
||||||
|
import { useAsyncMemo } from "../../../../hooks/useAsyncMemo";
|
||||||
|
import CopyableText from "../../elements/CopyableText";
|
||||||
|
import E2EIcon from "../../rooms/E2EIcon";
|
||||||
|
import { E2EStatus } from "../../../../utils/ShieldUtils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replacement function for `<i>` tags in translation strings.
|
||||||
|
*/
|
||||||
|
function i(sub: string): JSX.Element {
|
||||||
|
return <i>{sub}</i>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a list of users in the room, and allows selecting a user to view.
|
||||||
|
*
|
||||||
|
* Initially, filters to only show joined users, but offers the user an option to show all users.
|
||||||
|
*
|
||||||
|
* Once the user chooses a specific member, delegates to {@link UserView} to view a single user.
|
||||||
|
*/
|
||||||
|
export const UserList: React.FC<Pick<IDevtoolsProps, "onBack">> = ({ onBack }) => {
|
||||||
|
const context = useContext(DevtoolsContext);
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
// Show only joined users or all users with member events?
|
||||||
|
const [showOnlyJoined, setShowOnlyJoined] = useState(true);
|
||||||
|
// The `RoomMember` for the selected user (if any)
|
||||||
|
const [member, setMember] = useState<RoomMember | null>(null);
|
||||||
|
|
||||||
|
if (member) {
|
||||||
|
return <UserView member={member} onBack={() => setMember(null)} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const members = showOnlyJoined ? context.room.getJoinedMembers() : context.room.getMembers();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseTool onBack={onBack}>
|
||||||
|
<FilteredList query={query} onChange={setQuery}>
|
||||||
|
{members.map((member) => (
|
||||||
|
<UserButton key={member.userId} member={member} onClick={() => setMember(member)} />
|
||||||
|
))}
|
||||||
|
</FilteredList>
|
||||||
|
<LabelledToggleSwitch
|
||||||
|
label={_t("devtools|only_joined_members")}
|
||||||
|
onChange={setShowOnlyJoined}
|
||||||
|
value={showOnlyJoined}
|
||||||
|
/>
|
||||||
|
</BaseTool>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface UserButtonProps {
|
||||||
|
member: RoomMember;
|
||||||
|
onClick(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Button to select a user to view.
|
||||||
|
*/
|
||||||
|
const UserButton: React.FC<UserButtonProps> = ({ member, onClick }) => {
|
||||||
|
return (
|
||||||
|
<button className="mx_DevTools_button" onClick={onClick}>
|
||||||
|
{member.userId}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface UserProps extends Pick<IDevtoolsProps, "onBack"> {
|
||||||
|
member: RoomMember;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a single user to view, and allows selecting a device to view.
|
||||||
|
*
|
||||||
|
* Once the user chooses a specific device, delegates to {@link DeviceView} to show a single device.
|
||||||
|
*/
|
||||||
|
const UserView: React.FC<UserProps> = ({ member, onBack }) => {
|
||||||
|
const context = useContext(DevtoolsContext);
|
||||||
|
const crypto = context.room.client.getCrypto();
|
||||||
|
// An element to show the verification status of the device (unknown,
|
||||||
|
// unverified, verified by cross signing, signed by owner). The element
|
||||||
|
// will show text as well as an icon. If crypto is not available, the value
|
||||||
|
// will be `null`.
|
||||||
|
const verificationStatus = useAsyncMemo(
|
||||||
|
async () => {
|
||||||
|
if (!crypto) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const status = await crypto.getUserVerificationStatus(member.userId);
|
||||||
|
if (status.isCrossSigningVerified()) {
|
||||||
|
const e2eIcon = (): JSX.Element => (
|
||||||
|
<E2EIcon
|
||||||
|
isUser={true}
|
||||||
|
hideTooltip={true}
|
||||||
|
status={E2EStatus.Verified}
|
||||||
|
className="mx_E2EIcon_inline"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
return _t("devtools|user_verification_status|verified", {}, { E2EIcon: e2eIcon });
|
||||||
|
} else if (status.wasCrossSigningVerified()) {
|
||||||
|
const e2eIcon = (): JSX.Element => (
|
||||||
|
<E2EIcon
|
||||||
|
isUser={true}
|
||||||
|
hideTooltip={true}
|
||||||
|
status={E2EStatus.Warning}
|
||||||
|
className="mx_E2EIcon_inline"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
return _t("devtools|user_verification_status|was_verified", {}, { E2EIcon: e2eIcon });
|
||||||
|
} else if (status.needsUserApproval) {
|
||||||
|
const e2eIcon = (): JSX.Element => (
|
||||||
|
<E2EIcon
|
||||||
|
isUser={true}
|
||||||
|
hideTooltip={true}
|
||||||
|
status={E2EStatus.Warning}
|
||||||
|
className="mx_E2EIcon_inline"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
return _t("devtools|user_verification_status|identity_changed", {}, { E2EIcon: e2eIcon });
|
||||||
|
} else {
|
||||||
|
const e2eIcon = (): JSX.Element => (
|
||||||
|
<E2EIcon isUser={true} hideTooltip={true} status={E2EStatus.Normal} className="mx_E2EIcon_inline" />
|
||||||
|
);
|
||||||
|
return _t("devtools|user_verification_status|unverified", {}, { E2EIcon: e2eIcon });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[context, member],
|
||||||
|
_t("common|loading"),
|
||||||
|
);
|
||||||
|
// The user's devices, as a Map from device ID to device information (see
|
||||||
|
// the `Device` type in `matrix-js-sdk/src/models/device.ts`).
|
||||||
|
const devices = useAsyncMemo(
|
||||||
|
async () => {
|
||||||
|
const devices = await crypto?.getUserDeviceInfo([member.userId]);
|
||||||
|
return devices?.get(member.userId) ?? new Map();
|
||||||
|
},
|
||||||
|
[context, member],
|
||||||
|
new Map(),
|
||||||
|
);
|
||||||
|
// The device to show, if any.
|
||||||
|
const [device, setDevice] = useState<Device | null>(null);
|
||||||
|
|
||||||
|
if (device) {
|
||||||
|
return <DeviceView crypto={crypto!} device={device} onBack={() => setDevice(null)} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const avatarUrl = member.getMxcAvatarUrl();
|
||||||
|
const memberEventContent = member.events.member?.getContent();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseTool onBack={onBack}>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<CopyableText getTextToCopy={() => member.userId} border={false}>
|
||||||
|
{_t("devtools|user_id", { userId: member.userId })}
|
||||||
|
</CopyableText>
|
||||||
|
</li>
|
||||||
|
<li>{_t("devtools|user_room_membership", { membership: member.membership ?? "leave" })}</li>
|
||||||
|
<li>
|
||||||
|
{memberEventContent && "displayname" in memberEventContent
|
||||||
|
? _t("devtools|user_displayname", { displayname: member.rawDisplayName })
|
||||||
|
: _t("devtools|user_no_displayname", {}, { i })}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{avatarUrl !== undefined ? (
|
||||||
|
<CopyableText getTextToCopy={() => avatarUrl} border={false}>
|
||||||
|
{_t("devtools|user_avatar", { avatar: avatarUrl })}
|
||||||
|
</CopyableText>
|
||||||
|
) : (
|
||||||
|
_t("devtools|user_no_avatar", {}, { i })
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
<li>{verificationStatus}</li>
|
||||||
|
</ul>
|
||||||
|
<section>
|
||||||
|
<h2>{_t("devtools|devices", { count: devices.size })}</h2>
|
||||||
|
<ul>
|
||||||
|
{Array.from(devices.values()).map((device) => (
|
||||||
|
<li key={device.deviceId}>
|
||||||
|
<DeviceButton crypto={crypto!} device={device} onClick={() => setDevice(device)} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</BaseTool>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface DeviceButtonProps {
|
||||||
|
crypto: CryptoApi;
|
||||||
|
device: Device;
|
||||||
|
onClick(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Button to select a user to view.
|
||||||
|
*/
|
||||||
|
const DeviceButton: React.FC<DeviceButtonProps> = ({ crypto, device, onClick }) => {
|
||||||
|
const verificationIcon = useAsyncMemo(
|
||||||
|
async () => {
|
||||||
|
const status = await crypto.getDeviceVerificationStatus(device.userId, device.deviceId);
|
||||||
|
if (!status) {
|
||||||
|
return;
|
||||||
|
} else if (status.crossSigningVerified) {
|
||||||
|
return (
|
||||||
|
<E2EIcon
|
||||||
|
isUser={true}
|
||||||
|
hideTooltip={true}
|
||||||
|
status={E2EStatus.Verified}
|
||||||
|
className="mx_E2EIcon_inline"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (status.signedByOwner) {
|
||||||
|
return (
|
||||||
|
<E2EIcon isUser={true} hideTooltip={true} status={E2EStatus.Normal} className="mx_E2EIcon_inline" />
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<E2EIcon
|
||||||
|
isUser={true}
|
||||||
|
hideTooltip={true}
|
||||||
|
status={E2EStatus.Warning}
|
||||||
|
className="mx_E2EIcon_inline"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[crypto, device],
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<button className="mx_DevTools_button" onClick={onClick}>
|
||||||
|
{verificationIcon}
|
||||||
|
{device.deviceId}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface DeviceProps extends Pick<IDevtoolsProps, "onBack"> {
|
||||||
|
crypto: CryptoApi;
|
||||||
|
device: Device;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a single device to view.
|
||||||
|
*/
|
||||||
|
const DeviceView: React.FC<DeviceProps> = ({ crypto, device, onBack }) => {
|
||||||
|
// An element to show the verification status of the device (unknown,
|
||||||
|
// unverified, verified by cross signing, signed by owner). The element
|
||||||
|
// will show text as well as an icon if applicable.
|
||||||
|
const verificationStatus = useAsyncMemo(
|
||||||
|
async () => {
|
||||||
|
const status = await crypto.getDeviceVerificationStatus(device.userId, device.deviceId);
|
||||||
|
if (!status) {
|
||||||
|
// `status` will be `null` if the device is unknown or if the
|
||||||
|
// device doesn't have device keys. In either case, it's not a
|
||||||
|
// security issue since we won't be sending it decryption keys.
|
||||||
|
return _t("devtools|device_verification_status|unknown");
|
||||||
|
} else if (status.crossSigningVerified) {
|
||||||
|
const e2eIcon = (): JSX.Element => (
|
||||||
|
<E2EIcon
|
||||||
|
isUser={true}
|
||||||
|
hideTooltip={true}
|
||||||
|
status={E2EStatus.Verified}
|
||||||
|
className="mx_E2EIcon_inline"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
return _t("devtools|device_verification_status|verified", {}, { E2EIcon: e2eIcon });
|
||||||
|
} else if (status.signedByOwner) {
|
||||||
|
const e2eIcon = (): JSX.Element => (
|
||||||
|
<E2EIcon isUser={true} hideTooltip={true} status={E2EStatus.Normal} className="mx_E2EIcon_inline" />
|
||||||
|
);
|
||||||
|
return _t("devtools|device_verification_status|signed_by_owner", {}, { E2EIcon: e2eIcon });
|
||||||
|
} else {
|
||||||
|
const e2eIcon = (): JSX.Element => (
|
||||||
|
<E2EIcon
|
||||||
|
isUser={true}
|
||||||
|
hideTooltip={true}
|
||||||
|
status={E2EStatus.Warning}
|
||||||
|
className="mx_E2EIcon_inline"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
return _t("devtools|device_verification_status|unverified", {}, { E2EIcon: e2eIcon });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[crypto, device],
|
||||||
|
_t("common|loading"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const keyIdSuffix = ":" + device.deviceId;
|
||||||
|
const deviceKeys = (
|
||||||
|
<ul>
|
||||||
|
{Array.from(device.keys.entries()).map(([keyId, key]) => {
|
||||||
|
if (keyId.endsWith(keyIdSuffix)) {
|
||||||
|
return (
|
||||||
|
<li key={keyId}>
|
||||||
|
<CopyableText getTextToCopy={() => key} border={false}>
|
||||||
|
{keyId.slice(0, -keyIdSuffix.length)}: {key}
|
||||||
|
</CopyableText>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<li key={keyId}>
|
||||||
|
<i>{_t("devtools|invalid_device_key_id")}</i>: {keyId}: {key}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseTool onBack={onBack}>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<CopyableText getTextToCopy={() => device.userId} border={false}>
|
||||||
|
{_t("devtools|user_id", { userId: device.userId })}
|
||||||
|
</CopyableText>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<CopyableText getTextToCopy={() => device.deviceId} border={false}>
|
||||||
|
{_t("devtools|device_id", { deviceId: device.deviceId })}
|
||||||
|
</CopyableText>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{"displayName" in device
|
||||||
|
? _t("devtools|user_displayname", { displayname: device.displayName })
|
||||||
|
: _t("devtools|user_no_displayname", {}, { i })}
|
||||||
|
</li>
|
||||||
|
<li>{verificationStatus}</li>
|
||||||
|
<li>
|
||||||
|
{device.dehydrated ? _t("devtools|device_dehydrated_yes") : _t("devtools|device_dehydrated_no")}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{_t("devtools|device_keys")}
|
||||||
|
{deviceKeys}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</BaseTool>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -805,6 +805,17 @@
|
|||||||
},
|
},
|
||||||
"developer_mode": "Developer mode",
|
"developer_mode": "Developer mode",
|
||||||
"developer_tools": "Developer Tools",
|
"developer_tools": "Developer Tools",
|
||||||
|
"device_dehydrated_no": "Dehydrated: No",
|
||||||
|
"device_dehydrated_yes": "Dehydrated: Yes",
|
||||||
|
"device_id": "Device ID: %(deviceId)s",
|
||||||
|
"device_keys": "Device keys",
|
||||||
|
"device_verification_status": {
|
||||||
|
"signed_by_owner": "Verification status: <E2EIcon /> Signed by owner",
|
||||||
|
"unknown": "Verification status: Unknown",
|
||||||
|
"unverified": "Verification status: <E2EIcon /> Not signed by owner",
|
||||||
|
"verified": "Verification status: <E2EIcon /> Verified by cross-signing"
|
||||||
|
},
|
||||||
|
"devices": "Cryptographic devices (%(count)s)",
|
||||||
"edit_setting": "Edit setting",
|
"edit_setting": "Edit setting",
|
||||||
"edit_values": "Edit values",
|
"edit_values": "Edit values",
|
||||||
"empty_string": "<empty string>",
|
"empty_string": "<empty string>",
|
||||||
@@ -820,6 +831,7 @@
|
|||||||
"failed_to_save": "Failed to save settings.",
|
"failed_to_save": "Failed to save settings.",
|
||||||
"failed_to_send": "Failed to send event!",
|
"failed_to_send": "Failed to send event!",
|
||||||
"id": "ID: ",
|
"id": "ID: ",
|
||||||
|
"invalid_device_key_id": "Invalid device key ID",
|
||||||
"invalid_json": "Doesn't look like valid JSON.",
|
"invalid_json": "Doesn't look like valid JSON.",
|
||||||
"level": "Level",
|
"level": "Level",
|
||||||
"low_bandwidth_mode": "Low bandwidth mode",
|
"low_bandwidth_mode": "Low bandwidth mode",
|
||||||
@@ -830,6 +842,7 @@
|
|||||||
"notification_state": "Notification state is <strong>%(notificationState)s</strong>",
|
"notification_state": "Notification state is <strong>%(notificationState)s</strong>",
|
||||||
"notifications_debug": "Notifications debug",
|
"notifications_debug": "Notifications debug",
|
||||||
"number_of_users": "Number of users",
|
"number_of_users": "Number of users",
|
||||||
|
"only_joined_members": "Only joined users",
|
||||||
"original_event_source": "Original event source",
|
"original_event_source": "Original event source",
|
||||||
"room_encrypted": "Room is <strong>encrypted ✅</strong>",
|
"room_encrypted": "Room is <strong>encrypted ✅</strong>",
|
||||||
"room_id": "Room ID: %(roomId)s",
|
"room_id": "Room ID: %(roomId)s",
|
||||||
@@ -876,10 +889,23 @@
|
|||||||
"toggle_event": "toggle event",
|
"toggle_event": "toggle event",
|
||||||
"toolbox": "Toolbox",
|
"toolbox": "Toolbox",
|
||||||
"use_at_own_risk": "This UI does NOT check the types of the values. Use at your own risk.",
|
"use_at_own_risk": "This UI does NOT check the types of the values. Use at your own risk.",
|
||||||
|
"user_avatar": "Avatar: %(avatar)s",
|
||||||
|
"user_displayname": "Displayname: %(displayname)s",
|
||||||
|
"user_id": "User ID: %(userId)s",
|
||||||
|
"user_no_avatar": "Avatar: <i>None</i>",
|
||||||
|
"user_no_displayname": "Displayname: <i>None</i>",
|
||||||
"user_read_up_to": "User read up to: ",
|
"user_read_up_to": "User read up to: ",
|
||||||
"user_read_up_to_ignore_synthetic": "User read up to (ignoreSynthetic): ",
|
"user_read_up_to_ignore_synthetic": "User read up to (ignoreSynthetic): ",
|
||||||
"user_read_up_to_private": "User read up to (m.read.private): ",
|
"user_read_up_to_private": "User read up to (m.read.private): ",
|
||||||
"user_read_up_to_private_ignore_synthetic": "User read up to (m.read.private;ignoreSynthetic): ",
|
"user_read_up_to_private_ignore_synthetic": "User read up to (m.read.private;ignoreSynthetic): ",
|
||||||
|
"user_room_membership": "Membership: %(membership)s",
|
||||||
|
"user_verification_status": {
|
||||||
|
"identity_changed": "Verification status: <E2EIcon /> Unverified, and identity changed",
|
||||||
|
"unverified": "Verification status: <E2EIcon /> Unverified",
|
||||||
|
"verified": "Verification status: <E2EIcon /> Verified",
|
||||||
|
"was_verified": "Verification status: <E2EIcon /> Was verified, but identity changed"
|
||||||
|
},
|
||||||
|
"users": "Users",
|
||||||
"value": "Value",
|
"value": "Value",
|
||||||
"value_colon": "Value:",
|
"value_colon": "Value:",
|
||||||
"value_in_this_room": "Value in this room",
|
"value_in_this_room": "Value in this room",
|
||||||
|
|||||||
@@ -83,6 +83,11 @@ exports[`DevtoolsDialog renders the devtools dialog 1`] = `
|
|||||||
>
|
>
|
||||||
Active Widgets
|
Active Widgets
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
class="mx_DevTools_button"
|
||||||
|
>
|
||||||
|
Users
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2
|
<h2
|
||||||
|
|||||||
@@ -0,0 +1,361 @@
|
|||||||
|
/*
|
||||||
|
* 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 from "react";
|
||||||
|
import { mocked } from "jest-mock";
|
||||||
|
import { Device, DeviceVerification, type MatrixClient, MatrixEvent, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { render, screen, waitFor } from "jest-matrix-react";
|
||||||
|
import { Room, PendingEventOrdering } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { type DeviceVerificationStatus, type UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
|
||||||
|
|
||||||
|
import { createTestClient } from "../../../../../test-utils";
|
||||||
|
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
|
||||||
|
import { DevtoolsContext } from "../../../../../../src/components/views/dialogs/devtools/BaseTool";
|
||||||
|
import { UserList } from "../../../../../../src/components/views/dialogs/devtools/Users";
|
||||||
|
|
||||||
|
const userId = "@alice:example.com";
|
||||||
|
|
||||||
|
describe("<Users />", () => {
|
||||||
|
let matrixClient: MatrixClient;
|
||||||
|
beforeEach(() => {
|
||||||
|
matrixClient = createTestClient();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render a user list", () => {
|
||||||
|
const room = new Room("!roomId", matrixClient, userId, {
|
||||||
|
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||||
|
});
|
||||||
|
room.getJoinedMembers = jest.fn().mockReturnValue([]);
|
||||||
|
|
||||||
|
const { asFragment } = render(
|
||||||
|
<MatrixClientContext.Provider value={matrixClient}>
|
||||||
|
<DevtoolsContext.Provider value={{ room }}>
|
||||||
|
<UserList onBack={() => {}} />
|
||||||
|
</DevtoolsContext.Provider>
|
||||||
|
</MatrixClientContext.Provider>,
|
||||||
|
);
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render a single user", async () => {
|
||||||
|
const room = new Room("!roomId", matrixClient, userId, {
|
||||||
|
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||||
|
});
|
||||||
|
|
||||||
|
const alice = new RoomMember("!roomId", userId);
|
||||||
|
alice.setMembershipEvent(
|
||||||
|
new MatrixEvent({
|
||||||
|
content: {
|
||||||
|
membership: "join",
|
||||||
|
},
|
||||||
|
state_key: userId,
|
||||||
|
room_id: "!roomId",
|
||||||
|
type: "m.room.member",
|
||||||
|
sender: userId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
room.getJoinedMembers = jest.fn().mockReturnValue([alice]);
|
||||||
|
|
||||||
|
mocked(matrixClient.getCrypto()!.getUserVerificationStatus).mockResolvedValue({
|
||||||
|
isCrossSigningVerified: jest.fn().mockReturnValue(true),
|
||||||
|
wasCrossSigningVerified: jest.fn().mockReturnValue(true),
|
||||||
|
needsUserApproval: false,
|
||||||
|
} as unknown as UserVerificationStatus);
|
||||||
|
mocked(matrixClient.getCrypto()!.getUserDeviceInfo).mockResolvedValue(
|
||||||
|
new Map([
|
||||||
|
[
|
||||||
|
userId,
|
||||||
|
new Map([
|
||||||
|
[
|
||||||
|
"VERIFIED",
|
||||||
|
new Device({
|
||||||
|
deviceId: "VERIFIED",
|
||||||
|
userId: userId,
|
||||||
|
algorithms: [],
|
||||||
|
keys: new Map([
|
||||||
|
["ed25519:VERIFIED", "an_ed25519_public_key"],
|
||||||
|
["curve25519:VERIFIED", "a_curve25519_public_key"],
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"SIGNED",
|
||||||
|
new Device({
|
||||||
|
deviceId: "SIGNED",
|
||||||
|
userId: userId,
|
||||||
|
algorithms: [],
|
||||||
|
keys: new Map([
|
||||||
|
["ed25519:SIGNED", "an_ed25519_public_key"],
|
||||||
|
["curve25519:SIGNED", "a_curve25519_public_key"],
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"UNSIGNED",
|
||||||
|
new Device({
|
||||||
|
deviceId: "UNSIGNED",
|
||||||
|
userId: userId,
|
||||||
|
algorithms: [],
|
||||||
|
keys: new Map([
|
||||||
|
["ed25519:UNSIGNED", "an_ed25519_public_key"],
|
||||||
|
["curve25519:UNSIGNED", "a_curve25519_public_key"],
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
mocked(matrixClient.getCrypto()!.getDeviceVerificationStatus).mockImplementation(
|
||||||
|
async (userId: string, deviceId: string) => {
|
||||||
|
switch (deviceId) {
|
||||||
|
case "VERIFIED":
|
||||||
|
return {
|
||||||
|
signedByOwner: true,
|
||||||
|
crossSigningVerified: true,
|
||||||
|
} as unknown as DeviceVerificationStatus;
|
||||||
|
case "SIGNED":
|
||||||
|
return {
|
||||||
|
signedByOwner: true,
|
||||||
|
crossSigningVerified: false,
|
||||||
|
} as unknown as DeviceVerificationStatus;
|
||||||
|
case "UNSIGNED":
|
||||||
|
return {
|
||||||
|
signedByOwner: false,
|
||||||
|
crossSigningVerified: false,
|
||||||
|
} as unknown as DeviceVerificationStatus;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { asFragment } = render(
|
||||||
|
<MatrixClientContext.Provider value={matrixClient}>
|
||||||
|
<DevtoolsContext.Provider value={{ room }}>
|
||||||
|
<UserList onBack={() => {}} />
|
||||||
|
</DevtoolsContext.Provider>
|
||||||
|
</MatrixClientContext.Provider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
screen.getByRole("button", { name: userId }).click();
|
||||||
|
|
||||||
|
await waitFor(() => expect(screen.getByText(/Verification status:/)).toHaveTextContent(/Verified/));
|
||||||
|
await waitFor(() => expect(screen.getByRole("button", { name: "VERIFIED" })).toBeInTheDocument());
|
||||||
|
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render a single device - verified by cross-signing", async () => {
|
||||||
|
const room = new Room("!roomId", matrixClient, userId, {
|
||||||
|
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||||
|
});
|
||||||
|
|
||||||
|
const alice = new RoomMember("!roomId", userId);
|
||||||
|
alice.setMembershipEvent(
|
||||||
|
new MatrixEvent({
|
||||||
|
content: {
|
||||||
|
membership: "join",
|
||||||
|
},
|
||||||
|
state_key: userId,
|
||||||
|
room_id: "!roomId",
|
||||||
|
type: "m.room.member",
|
||||||
|
sender: userId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
room.getJoinedMembers = jest.fn().mockReturnValue([alice]);
|
||||||
|
|
||||||
|
mocked(matrixClient.getCrypto()!.getUserVerificationStatus).mockResolvedValue({
|
||||||
|
isCrossSigningVerified: jest.fn().mockReturnValue(true),
|
||||||
|
wasCrossSigningVerified: jest.fn().mockReturnValue(true),
|
||||||
|
needsUserApproval: false,
|
||||||
|
} as unknown as UserVerificationStatus);
|
||||||
|
mocked(matrixClient.getCrypto()!.getUserDeviceInfo).mockResolvedValue(
|
||||||
|
new Map([
|
||||||
|
[
|
||||||
|
userId,
|
||||||
|
new Map([
|
||||||
|
[
|
||||||
|
"VERIFIED",
|
||||||
|
new Device({
|
||||||
|
deviceId: "VERIFIED",
|
||||||
|
userId: userId,
|
||||||
|
algorithms: [],
|
||||||
|
keys: new Map([
|
||||||
|
["ed25519:VERIFIED", "an_ed25519_public_key"],
|
||||||
|
["curve25519:VERIFIED", "a_curve25519_public_key"],
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
mocked(matrixClient.getCrypto()!.getDeviceVerificationStatus).mockResolvedValue({
|
||||||
|
signedByOwner: true,
|
||||||
|
crossSigningVerified: true,
|
||||||
|
} as unknown as DeviceVerificationStatus);
|
||||||
|
|
||||||
|
const { asFragment } = render(
|
||||||
|
<MatrixClientContext.Provider value={matrixClient}>
|
||||||
|
<DevtoolsContext.Provider value={{ room }}>
|
||||||
|
<UserList onBack={() => {}} />
|
||||||
|
</DevtoolsContext.Provider>
|
||||||
|
</MatrixClientContext.Provider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
screen.getByRole("button", { name: userId }).click();
|
||||||
|
|
||||||
|
await waitFor(() => expect(screen.getByRole("button", { name: "VERIFIED" })).toBeInTheDocument());
|
||||||
|
screen.getByRole("button", { name: "VERIFIED" }).click();
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(screen.getByText(/Verification status:/)).toHaveTextContent(/Verified by cross-signing/),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render a single device - signed by owner", async () => {
|
||||||
|
const room = new Room("!roomId", matrixClient, userId, {
|
||||||
|
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||||
|
});
|
||||||
|
|
||||||
|
const alice = new RoomMember("!roomId", userId);
|
||||||
|
alice.setMembershipEvent(
|
||||||
|
new MatrixEvent({
|
||||||
|
content: {
|
||||||
|
membership: "join",
|
||||||
|
},
|
||||||
|
state_key: userId,
|
||||||
|
room_id: "!roomId",
|
||||||
|
type: "m.room.member",
|
||||||
|
sender: userId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
room.getJoinedMembers = jest.fn().mockReturnValue([alice]);
|
||||||
|
|
||||||
|
mocked(matrixClient.getCrypto()!.getUserVerificationStatus).mockResolvedValue({
|
||||||
|
isCrossSigningVerified: jest.fn().mockReturnValue(true),
|
||||||
|
wasCrossSigningVerified: jest.fn().mockReturnValue(true),
|
||||||
|
needsUserApproval: false,
|
||||||
|
} as unknown as UserVerificationStatus);
|
||||||
|
mocked(matrixClient.getCrypto()!.getUserDeviceInfo).mockResolvedValue(
|
||||||
|
new Map([
|
||||||
|
[
|
||||||
|
userId,
|
||||||
|
new Map([
|
||||||
|
[
|
||||||
|
"SIGNED",
|
||||||
|
new Device({
|
||||||
|
deviceId: "SIGNED",
|
||||||
|
userId: userId,
|
||||||
|
algorithms: [],
|
||||||
|
keys: new Map([
|
||||||
|
["ed25519:SIGNED", "an_ed25519_public_key"],
|
||||||
|
["curve25519:SIGNED", "a_curve25519_public_key"],
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
mocked(matrixClient.getCrypto()!.getDeviceVerificationStatus).mockResolvedValue({
|
||||||
|
signedByOwner: true,
|
||||||
|
crossSigningVerified: false,
|
||||||
|
} as unknown as DeviceVerificationStatus);
|
||||||
|
|
||||||
|
const { asFragment } = render(
|
||||||
|
<MatrixClientContext.Provider value={matrixClient}>
|
||||||
|
<DevtoolsContext.Provider value={{ room }}>
|
||||||
|
<UserList onBack={() => {}} />
|
||||||
|
</DevtoolsContext.Provider>
|
||||||
|
</MatrixClientContext.Provider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
screen.getByRole("button", { name: userId }).click();
|
||||||
|
|
||||||
|
await waitFor(() => expect(screen.getByRole("button", { name: "SIGNED" })).toBeInTheDocument());
|
||||||
|
screen.getByRole("button", { name: "SIGNED" }).click();
|
||||||
|
|
||||||
|
await waitFor(() => expect(screen.getByText(/Verification status:/)).toHaveTextContent(/Signed by owner/));
|
||||||
|
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render a single device - unsigned", async () => {
|
||||||
|
const room = new Room("!roomId", matrixClient, userId, {
|
||||||
|
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||||
|
});
|
||||||
|
|
||||||
|
const alice = new RoomMember("!roomId", userId);
|
||||||
|
alice.setMembershipEvent(
|
||||||
|
new MatrixEvent({
|
||||||
|
content: {
|
||||||
|
membership: "join",
|
||||||
|
},
|
||||||
|
state_key: userId,
|
||||||
|
room_id: "!roomId",
|
||||||
|
type: "m.room.member",
|
||||||
|
sender: userId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
room.getJoinedMembers = jest.fn().mockReturnValue([alice]);
|
||||||
|
|
||||||
|
mocked(matrixClient.getCrypto()!.getUserVerificationStatus).mockResolvedValue({
|
||||||
|
isCrossSigningVerified: jest.fn().mockReturnValue(true),
|
||||||
|
wasCrossSigningVerified: jest.fn().mockReturnValue(true),
|
||||||
|
needsUserApproval: false,
|
||||||
|
} as unknown as UserVerificationStatus);
|
||||||
|
mocked(matrixClient.getCrypto()!.getUserDeviceInfo).mockResolvedValue(
|
||||||
|
new Map([
|
||||||
|
[
|
||||||
|
userId,
|
||||||
|
new Map([
|
||||||
|
[
|
||||||
|
"UNSIGNED",
|
||||||
|
new Device({
|
||||||
|
deviceId: "UNSIGNED",
|
||||||
|
userId: userId,
|
||||||
|
algorithms: [],
|
||||||
|
keys: new Map([
|
||||||
|
["ed25519:UNSIGNED", "an_ed25519_public_key"],
|
||||||
|
["curve25519:UNSIGNED", "a_curve25519_public_key"],
|
||||||
|
]),
|
||||||
|
verified: DeviceVerification.Verified,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
mocked(matrixClient.getCrypto()!.getDeviceVerificationStatus).mockResolvedValue({
|
||||||
|
signedByOwner: false,
|
||||||
|
crossSigningVerified: false,
|
||||||
|
} as unknown as DeviceVerificationStatus);
|
||||||
|
|
||||||
|
const { asFragment } = render(
|
||||||
|
<MatrixClientContext.Provider value={matrixClient}>
|
||||||
|
<DevtoolsContext.Provider value={{ room }}>
|
||||||
|
<UserList onBack={() => {}} />
|
||||||
|
</DevtoolsContext.Provider>
|
||||||
|
</MatrixClientContext.Provider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
screen.getByRole("button", { name: userId }).click();
|
||||||
|
|
||||||
|
await waitFor(() => expect(screen.getByText(/Verification status:/)).toHaveTextContent(/Verified/));
|
||||||
|
|
||||||
|
await waitFor(() => expect(screen.getByRole("button", { name: "UNSIGNED" })).toBeInTheDocument());
|
||||||
|
screen.getByRole("button", { name: "UNSIGNED" }).click();
|
||||||
|
|
||||||
|
await waitFor(() => expect(screen.getByText(/Verification status:/)).toHaveTextContent(/Not signed by owner/));
|
||||||
|
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,454 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<Users /> should render a single device - signed by owner 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<div
|
||||||
|
class="mx_DevTools_content"
|
||||||
|
>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
class="mx_CopyableText"
|
||||||
|
>
|
||||||
|
User ID: @alice:example.com
|
||||||
|
<div
|
||||||
|
aria-label="Copy"
|
||||||
|
class="mx_AccessibleButton mx_CopyableText_copyButton"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
class="mx_CopyableText"
|
||||||
|
>
|
||||||
|
Device ID: SIGNED
|
||||||
|
<div
|
||||||
|
aria-label="Copy"
|
||||||
|
class="mx_AccessibleButton mx_CopyableText_copyButton"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Displayname:
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span>
|
||||||
|
Verification status:
|
||||||
|
<div
|
||||||
|
class="mx_E2EIcon mx_E2EIcon_normal mx_E2EIcon_inline"
|
||||||
|
data-testid="e2e-icon"
|
||||||
|
/>
|
||||||
|
Signed by owner
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Dehydrated: No
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Device keys
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
class="mx_CopyableText"
|
||||||
|
>
|
||||||
|
ed25519: an_ed25519_public_key
|
||||||
|
<div
|
||||||
|
aria-label="Copy"
|
||||||
|
class="mx_AccessibleButton mx_CopyableText_copyButton"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
class="mx_CopyableText"
|
||||||
|
>
|
||||||
|
curve25519: a_curve25519_public_key
|
||||||
|
<div
|
||||||
|
aria-label="Copy"
|
||||||
|
class="mx_AccessibleButton mx_CopyableText_copyButton"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_Dialog_buttons"
|
||||||
|
>
|
||||||
|
<button>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<Users /> should render a single device - unsigned 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<div
|
||||||
|
class="mx_DevTools_content"
|
||||||
|
>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
class="mx_CopyableText"
|
||||||
|
>
|
||||||
|
User ID: @alice:example.com
|
||||||
|
<div
|
||||||
|
aria-label="Copy"
|
||||||
|
class="mx_AccessibleButton mx_CopyableText_copyButton"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
class="mx_CopyableText"
|
||||||
|
>
|
||||||
|
Device ID: UNSIGNED
|
||||||
|
<div
|
||||||
|
aria-label="Copy"
|
||||||
|
class="mx_AccessibleButton mx_CopyableText_copyButton"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Displayname:
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span>
|
||||||
|
Verification status:
|
||||||
|
<div
|
||||||
|
class="mx_E2EIcon mx_E2EIcon_warning mx_E2EIcon_inline"
|
||||||
|
data-testid="e2e-icon"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_E2EIcon_normal"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
Not signed by owner
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Dehydrated: No
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Device keys
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
class="mx_CopyableText"
|
||||||
|
>
|
||||||
|
ed25519: an_ed25519_public_key
|
||||||
|
<div
|
||||||
|
aria-label="Copy"
|
||||||
|
class="mx_AccessibleButton mx_CopyableText_copyButton"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
class="mx_CopyableText"
|
||||||
|
>
|
||||||
|
curve25519: a_curve25519_public_key
|
||||||
|
<div
|
||||||
|
aria-label="Copy"
|
||||||
|
class="mx_AccessibleButton mx_CopyableText_copyButton"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_Dialog_buttons"
|
||||||
|
>
|
||||||
|
<button>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<Users /> should render a single device - verified by cross-signing 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<div
|
||||||
|
class="mx_DevTools_content"
|
||||||
|
>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
class="mx_CopyableText"
|
||||||
|
>
|
||||||
|
User ID: @alice:example.com
|
||||||
|
<div
|
||||||
|
aria-label="Copy"
|
||||||
|
class="mx_AccessibleButton mx_CopyableText_copyButton"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
class="mx_CopyableText"
|
||||||
|
>
|
||||||
|
Device ID: VERIFIED
|
||||||
|
<div
|
||||||
|
aria-label="Copy"
|
||||||
|
class="mx_AccessibleButton mx_CopyableText_copyButton"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Displayname:
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span>
|
||||||
|
Verification status:
|
||||||
|
<div
|
||||||
|
class="mx_E2EIcon mx_E2EIcon_verified mx_E2EIcon_inline"
|
||||||
|
data-testid="e2e-icon"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_E2EIcon_normal"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
Verified by cross-signing
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Dehydrated: No
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Device keys
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
class="mx_CopyableText"
|
||||||
|
>
|
||||||
|
ed25519: an_ed25519_public_key
|
||||||
|
<div
|
||||||
|
aria-label="Copy"
|
||||||
|
class="mx_AccessibleButton mx_CopyableText_copyButton"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
class="mx_CopyableText"
|
||||||
|
>
|
||||||
|
curve25519: a_curve25519_public_key
|
||||||
|
<div
|
||||||
|
aria-label="Copy"
|
||||||
|
class="mx_AccessibleButton mx_CopyableText_copyButton"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_Dialog_buttons"
|
||||||
|
>
|
||||||
|
<button>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<Users /> should render a single user 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<div
|
||||||
|
class="mx_DevTools_content"
|
||||||
|
>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
class="mx_CopyableText"
|
||||||
|
>
|
||||||
|
User ID: @alice:example.com
|
||||||
|
<div
|
||||||
|
aria-label="Copy"
|
||||||
|
class="mx_AccessibleButton mx_CopyableText_copyButton"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Membership: join
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span>
|
||||||
|
Displayname:
|
||||||
|
<i>
|
||||||
|
None
|
||||||
|
</i>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span>
|
||||||
|
Avatar:
|
||||||
|
<i>
|
||||||
|
None
|
||||||
|
</i>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span>
|
||||||
|
Verification status:
|
||||||
|
<div
|
||||||
|
class="mx_E2EIcon mx_E2EIcon_verified mx_E2EIcon_inline"
|
||||||
|
data-testid="e2e-icon"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_E2EIcon_normal"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
Verified
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<section>
|
||||||
|
<h2>
|
||||||
|
Cryptographic devices (3)
|
||||||
|
</h2>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
class="mx_DevTools_button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_E2EIcon mx_E2EIcon_verified mx_E2EIcon_inline"
|
||||||
|
data-testid="e2e-icon"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_E2EIcon_normal"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
VERIFIED
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
class="mx_DevTools_button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_E2EIcon mx_E2EIcon_normal mx_E2EIcon_inline"
|
||||||
|
data-testid="e2e-icon"
|
||||||
|
/>
|
||||||
|
SIGNED
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
class="mx_DevTools_button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_E2EIcon mx_E2EIcon_warning mx_E2EIcon_inline"
|
||||||
|
data-testid="e2e-icon"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_E2EIcon_normal"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
UNSIGNED
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_Dialog_buttons"
|
||||||
|
>
|
||||||
|
<button>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<Users /> should render a user list 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<div
|
||||||
|
class="mx_DevTools_content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_Field mx_Field_input mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
autocomplete="off"
|
||||||
|
id="mx_Field_1"
|
||||||
|
label="Filter results"
|
||||||
|
placeholder="Filter results"
|
||||||
|
size="64"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
for="mx_Field_1"
|
||||||
|
>
|
||||||
|
Filter results
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
No results found
|
||||||
|
<div
|
||||||
|
class="mx_SettingsFlag"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mx_SettingsFlag_label"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
id="mx_LabelledToggleSwitch__r_4_"
|
||||||
|
>
|
||||||
|
Only joined users
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
aria-checked="true"
|
||||||
|
aria-disabled="false"
|
||||||
|
aria-labelledby="mx_LabelledToggleSwitch__r_4_"
|
||||||
|
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on mx_ToggleSwitch_enabled"
|
||||||
|
role="switch"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_ToggleSwitch_ball"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_Dialog_buttons"
|
||||||
|
>
|
||||||
|
<button>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
||||||
Reference in New Issue
Block a user