Add new verification section to user profile (#29200)
* Create new verification section * Remove old code and use new VerificationSection * Add styling and translation * Fix tests * Remove dead code * Fix broken test * Remove imports * Remove console.log * Update snapshots * Fix broken tests * Fix lint * Make badge expand with content * Remove unused code
This commit is contained in:
@@ -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<E2EStatus | undefined> => {
|
||||
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 <div className={classes} title={device.deviceId} />;
|
||||
} else if (isVerified) {
|
||||
return (
|
||||
<div className={classes} title={device.deviceId}>
|
||||
<div className={iconClasses} />
|
||||
<div className="mx_UserInfo_device_name">{deviceName}</div>
|
||||
<div className="mx_UserInfo_device_trusted">{trustedLabel}</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<AccessibleButton
|
||||
className={classes}
|
||||
title={device.deviceId}
|
||||
aria-label={deviceName}
|
||||
onClick={onDeviceClick}
|
||||
>
|
||||
<div className={iconClasses} />
|
||||
<div className="mx_UserInfo_device_name">{deviceName}</div>
|
||||
<div className="mx_UserInfo_device_trusted">{trustedLabel}</div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 <Spinner />;
|
||||
}
|
||||
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 = (
|
||||
<AccessibleButton kind="link" className="mx_UserInfo_expand" onClick={() => setExpanded(false)}>
|
||||
<div>{expandHideCaption}</div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
} else {
|
||||
expandButton = (
|
||||
<AccessibleButton kind="link" className="mx_UserInfo_expand" onClick={() => setExpanded(true)}>
|
||||
<div className={expandIconClasses} />
|
||||
<div>{expandCountCaption}</div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let deviceList = unverifiedDevices.map((device, i) => {
|
||||
return <DeviceItem key={i} userId={userId} device={device} isUserVerified={isUserVerified} />;
|
||||
});
|
||||
if (isExpanded) {
|
||||
const keyStart = unverifiedDevices.length;
|
||||
deviceList = deviceList.concat(
|
||||
expandSectionDevices.map((device, i) => {
|
||||
return (
|
||||
<DeviceItem key={i + keyStart} userId={userId} device={device} isUserVerified={isUserVerified} />
|
||||
);
|
||||
}),
|
||||
);
|
||||
if (dehydratedDeviceInExpandSection) {
|
||||
deviceList.push(<div>{_t("user_info|dehydrated_device_enabled")}</div>);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_UserInfo_devices">
|
||||
<div>{deviceList}</div>
|
||||
<div>{expandButton}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const MessageButton = ({ member }: { member: Member }): JSX.Element => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const [busy, setBusy] = useState(false);
|
||||
@@ -1400,12 +1127,84 @@ export const useDevices = (userId: string): IDevice[] | undefined | null => {
|
||||
return devices;
|
||||
};
|
||||
|
||||
function useHasCrossSigningKeys(cli: MatrixClient, member: User, canVerify: boolean): boolean | undefined {
|
||||
return useAsyncMemo(async () => {
|
||||
if (!canVerify) return undefined;
|
||||
return await cli.getCrypto()?.userHasCrossSigningKeys(member.userId, true);
|
||||
}, [cli, member, canVerify]);
|
||||
}
|
||||
|
||||
const VerificationSection: React.FC<{
|
||||
member: User | RoomMember;
|
||||
devices: IDevice[];
|
||||
}> = ({ member, devices }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
let content;
|
||||
const homeserverSupportsCrossSigning = useHomeserverSupportsCrossSigning(cli);
|
||||
|
||||
const userTrust = useAsyncMemo<UserVerificationStatus | undefined>(
|
||||
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 hasCrossSigningKeys = useHasCrossSigningKeys(cli, member as User, canVerify);
|
||||
|
||||
if (isUserVerified) {
|
||||
content = (
|
||||
<Badge kind="green" className="mx_UserInfo_verified_badge">
|
||||
<VerifiedIcon className="mx_UserInfo_verified_icon" height="16px" width="16px" />
|
||||
<Text size="sm" weight="medium" className="mx_UserInfo_verified_label">
|
||||
{_t("common|verified")}
|
||||
</Text>
|
||||
</Badge>
|
||||
);
|
||||
} else if (hasCrossSigningKeys === undefined) {
|
||||
// We are still fetching the cross-signing keys for the user, show spinner.
|
||||
content = <InlineSpinner size={24} />;
|
||||
} else if (canVerify && hasCrossSigningKeys) {
|
||||
content = (
|
||||
<div className="mx_UserInfo_container_verifyButton">
|
||||
<Button
|
||||
className="mx_UserInfo_verify_button"
|
||||
kind="tertiary"
|
||||
size="sm"
|
||||
onClick={() => verifyUser(cli, member as User)}
|
||||
>
|
||||
{_t("user_info|verify_button")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
<Text className="mx_UserInfo_verification_unavailable" size="sm">
|
||||
({_t("user_info|verification_unavailable")})
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex justify="center" align="center" className="mx_UserInfo_verification">
|
||||
{content}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
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 = <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<UserVerificationStatus | undefined>(
|
||||
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 = (
|
||||
<div className="mx_UserInfo_container_verifyButton">
|
||||
<AccessibleButton
|
||||
kind="link"
|
||||
className="mx_UserInfo_field mx_UserInfo_verifyButton"
|
||||
onClick={() => verifyUser(cli, member as User)}
|
||||
>
|
||||
{_t("action|verify")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
} 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 = <Spinner />;
|
||||
}
|
||||
}
|
||||
|
||||
let editDevices;
|
||||
if (member.userId == cli.getUserId()) {
|
||||
editDevices = (
|
||||
<div>
|
||||
<AccessibleButton
|
||||
kind="link"
|
||||
className="mx_UserInfo_field"
|
||||
onClick={() => {
|
||||
dis.dispatch({
|
||||
action: Action.ViewUserDeviceSettings,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{_t("user_info|edit_own_devices")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const securitySection = (
|
||||
<Container>
|
||||
<h2>{_t("common|security")}</h2>
|
||||
<p>{text}</p>
|
||||
{verifyButton}
|
||||
{cryptoEnabled && (
|
||||
<DevicesSection
|
||||
loading={showDeviceListSpinner}
|
||||
devices={devices}
|
||||
userId={member.userId}
|
||||
isUserVerified={isUserVerified}
|
||||
/>
|
||||
)}
|
||||
{editDevices}
|
||||
</Container>
|
||||
);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{securitySection}
|
||||
|
||||
<UserOptionsSection
|
||||
canInvite={roomPermissions.canInvite}
|
||||
member={member as RoomMember}
|
||||
@@ -1615,15 +1313,12 @@ const BasicUserInfo: React.FC<{
|
||||
>
|
||||
{memberDetails}
|
||||
</UserOptionsSection>
|
||||
|
||||
{adminToolsContainer}
|
||||
|
||||
{!isMe && (
|
||||
<Container>
|
||||
<IgnoreToggleButton member={member} />
|
||||
</Container>
|
||||
)}
|
||||
|
||||
{spinner}
|
||||
</React.Fragment>
|
||||
);
|
||||
@@ -1633,9 +1328,10 @@ export type Member = User | RoomMember;
|
||||
|
||||
export const UserInfoHeader: React.FC<{
|
||||
member: Member;
|
||||
e2eStatus?: E2EStatus;
|
||||
devices: IDevice[];
|
||||
roomId?: string;
|
||||
}> = ({ member, e2eStatus, roomId }) => {
|
||||
hideVerificationSection?: boolean;
|
||||
}> = ({ member, devices, roomId, hideVerificationSection }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
const onMemberAvatarClick = useCallback(() => {
|
||||
@@ -1686,7 +1382,6 @@ export const UserInfoHeader: React.FC<{
|
||||
|
||||
const timezoneInfo = useUserTimezone(cli, member.userId);
|
||||
|
||||
const e2eIcon = e2eStatus ? <E2EIcon size={18} status={e2eStatus} isUser={true} /> : null;
|
||||
const userIdentifier = UserIdentifierCustomisations.getDisplayUserIdentifier?.(member.userId, {
|
||||
roomId,
|
||||
withDisplayName: true,
|
||||
@@ -1715,7 +1410,6 @@ export const UserInfoHeader: React.FC<{
|
||||
<Heading size="sm" weight="semibold" as="h1" dir="auto">
|
||||
<Flex className="mx_UserInfo_profile_name" direction="row-reverse" align="center">
|
||||
{displayName}
|
||||
{e2eIcon}
|
||||
</Flex>
|
||||
</Heading>
|
||||
{presenceLabel}
|
||||
@@ -1734,6 +1428,7 @@ export const UserInfoHeader: React.FC<{
|
||||
</CopyableText>
|
||||
</Text>
|
||||
</Flex>
|
||||
{!hideVerificationSection && <VerificationSection member={member} devices={devices} />}
|
||||
</Container>
|
||||
</React.Fragment>
|
||||
);
|
||||
@@ -1757,13 +1452,6 @@ const UserInfo: React.FC<IProps> = ({ user, room, onClose, phase = RightPanelPha
|
||||
const isRoomEncrypted = useIsEncrypted(cli, room);
|
||||
const devices = useDevices(user.userId) ?? [];
|
||||
|
||||
const e2eStatus = useAsyncMemo(async () => {
|
||||
if (!isRoomEncrypted || !devices) {
|
||||
return undefined;
|
||||
}
|
||||
return await getE2EStatus(cli, user.userId, devices);
|
||||
}, [cli, isRoomEncrypted, user.userId, devices]);
|
||||
|
||||
const classes = ["mx_UserInfo"];
|
||||
|
||||
let cardState: IRightPanelCardState = {};
|
||||
@@ -1779,14 +1467,7 @@ const UserInfo: React.FC<IProps> = ({ user, room, onClose, phase = RightPanelPha
|
||||
let content: JSX.Element | undefined;
|
||||
switch (phase) {
|
||||
case RightPanelPhases.MemberInfo:
|
||||
content = (
|
||||
<BasicUserInfo
|
||||
room={room as Room}
|
||||
member={member as User}
|
||||
devices={devices}
|
||||
isRoomEncrypted={Boolean(isRoomEncrypted)}
|
||||
/>
|
||||
);
|
||||
content = <BasicUserInfo room={room as Room} member={member as User} />;
|
||||
break;
|
||||
case RightPanelPhases.EncryptionPanel:
|
||||
classes.push("mx_UserInfo_smallAvatar");
|
||||
@@ -1811,7 +1492,12 @@ const UserInfo: React.FC<IProps> = ({ user, room, onClose, phase = RightPanelPha
|
||||
|
||||
const header = (
|
||||
<>
|
||||
<UserInfoHeader member={member} e2eStatus={e2eStatus} roomId={room?.roomId} />
|
||||
<UserInfoHeader
|
||||
hideVerificationSection={phase === RightPanelPhases.EncryptionPanel}
|
||||
member={member}
|
||||
devices={devices}
|
||||
roomId={room?.roomId}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
@@ -548,7 +548,6 @@
|
||||
"saved": "Saved",
|
||||
"saving": "Saving…",
|
||||
"secure_backup": "Secure Backup",
|
||||
"security": "Security",
|
||||
"select_all": "Select all",
|
||||
"server": "Server",
|
||||
"settings": "Settings",
|
||||
@@ -567,7 +566,6 @@
|
||||
"thread": "Thread",
|
||||
"threads": "Threads",
|
||||
"timeline": "Timeline",
|
||||
"trusted": "Trusted",
|
||||
"unavailable": "unavailable",
|
||||
"unencrypted": "Not encrypted",
|
||||
"unmute": "Unmute",
|
||||
@@ -971,7 +969,6 @@
|
||||
"title": "Not Trusted"
|
||||
},
|
||||
"unable_to_setup_keys_error": "Unable to set up keys",
|
||||
"unsupported": "This client does not support end-to-end encryption.",
|
||||
"verification": {
|
||||
"accepting": "Accepting…",
|
||||
"after_new_login": {
|
||||
@@ -3787,18 +3784,9 @@
|
||||
"ban_room_confirm_title": "Ban from %(roomName)s",
|
||||
"ban_space_everything": "Ban them from everything I'm able to",
|
||||
"ban_space_specific": "Ban them from specific things I'm able to",
|
||||
"count_of_sessions": {
|
||||
"one": "%(count)s session",
|
||||
"other": "%(count)s sessions"
|
||||
},
|
||||
"count_of_verified_sessions": {
|
||||
"one": "1 verified session",
|
||||
"other": "%(count)s verified sessions"
|
||||
},
|
||||
"deactivate_confirm_action": "Deactivate user",
|
||||
"deactivate_confirm_description": "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?",
|
||||
"deactivate_confirm_title": "Deactivate user?",
|
||||
"dehydrated_device_enabled": "Offline device enabled",
|
||||
"demote_button": "Demote",
|
||||
"demote_self_confirm_description_space": "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the space it will be impossible to regain privileges.",
|
||||
"demote_self_confirm_room": "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.",
|
||||
@@ -3806,15 +3794,12 @@
|
||||
"disinvite_button_room": "Disinvite from room",
|
||||
"disinvite_button_room_name": "Disinvite from %(roomName)s",
|
||||
"disinvite_button_space": "Disinvite from space",
|
||||
"edit_own_devices": "Edit devices",
|
||||
"error_ban_user": "Failed to ban user",
|
||||
"error_deactivate": "Failed to deactivate user",
|
||||
"error_kicking_user": "Failed to remove user",
|
||||
"error_mute_user": "Failed to mute user",
|
||||
"error_revoke_3pid_invite_description": "Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.",
|
||||
"error_revoke_3pid_invite_title": "Failed to revoke invite",
|
||||
"hide_sessions": "Hide sessions",
|
||||
"hide_verified_sessions": "Hide verified sessions",
|
||||
"ignore_button": "Ignore",
|
||||
"ignore_confirm_description": "All messages and invites from this user will be hidden. Are you sure you want to ignore them?",
|
||||
"ignore_confirm_title": "Ignore %(user)s",
|
||||
@@ -3858,6 +3843,7 @@
|
||||
"unban_space_specific": "Unban them from specific things I'm able to",
|
||||
"unban_space_warning": "They won't be able to access whatever you're not an admin of.",
|
||||
"unignore_button": "Unignore",
|
||||
"verification_unavailable": "User verification unavailable",
|
||||
"verify_button": "Verify User",
|
||||
"verify_explainer": "For extra security, verify this user by checking a one-time code on both of your devices."
|
||||
},
|
||||
|
||||
@@ -10,11 +10,8 @@ import { type User, type MatrixClient, type RoomMember } from "matrix-js-sdk/src
|
||||
import { CrossSigningKey, type VerificationRequest } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
import dis from "./dispatcher/dispatcher";
|
||||
import Modal from "./Modal";
|
||||
import { RightPanelPhases } from "./stores/right-panel/RightPanelStorePhases";
|
||||
import { accessSecretStorage } from "./SecurityManager";
|
||||
import UntrustedDeviceDialog from "./components/views/dialogs/UntrustedDeviceDialog";
|
||||
import { type IDevice } from "./components/views/right_panel/UserInfo";
|
||||
import RightPanelStore from "./stores/right-panel/RightPanelStore";
|
||||
import { type IRightPanelCardState } from "./stores/right-panel/RightPanelStoreIPanelState";
|
||||
import { findDMForUser } from "./utils/dm/findDMForUser";
|
||||
@@ -31,32 +28,6 @@ async function enable4SIfNeeded(matrixClient: MatrixClient): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function verifyDevice(matrixClient: MatrixClient, user: User, device: IDevice): Promise<void> {
|
||||
if (matrixClient.isGuest()) {
|
||||
dis.dispatch({ action: "require_registration" });
|
||||
return;
|
||||
}
|
||||
// if cross-signing is not explicitly disabled, check if it should be enabled first.
|
||||
if (matrixClient.getCrypto()?.getTrustCrossSignedDevices()) {
|
||||
if (!(await enable4SIfNeeded(matrixClient))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Modal.createDialog(UntrustedDeviceDialog, {
|
||||
user,
|
||||
device,
|
||||
onFinished: async (action): Promise<void> => {
|
||||
if (action === "sas") {
|
||||
const verificationRequestPromise = matrixClient
|
||||
.getCrypto()
|
||||
?.requestDeviceVerification(user.userId, device.deviceId);
|
||||
setRightPanel({ member: user, verificationRequestPromise });
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function verifyUser(matrixClient: MatrixClient, user: User): Promise<void> {
|
||||
if (matrixClient.isGuest()) {
|
||||
dis.dispatch({ action: "require_registration" });
|
||||
|
||||
Reference in New Issue
Block a user