mirror of
https://github.com/element-hq/element-web.git
synced 2025-12-28 07:14:20 +00:00
MVVM userinfo: split header and verification components (#30214)
* feat: mvvm userinfo split header and verification * test: add userinfoheader tests * fix: userHeaderVerificationView verification method
This commit is contained in:
parent
8a879c7fca
commit
0a97cbaada
@ -0,0 +1,87 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type MatrixClient, type RoomMember, type User } from "matrix-js-sdk/src/matrix";
|
||||
import { useContext } from "react";
|
||||
import { type UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
import MatrixClientContext from "../../../../contexts/MatrixClientContext";
|
||||
import { type IDevice } from "../../../views/right_panel/UserInfo";
|
||||
import { useAsyncMemo } from "../../../../hooks/useAsyncMemo";
|
||||
import { verifyUser } from "../../../../verification";
|
||||
|
||||
export interface UserInfoVerificationSectionState {
|
||||
/**
|
||||
* variables used to check if we can verify the user and display the verify button
|
||||
*/
|
||||
canVerify: boolean;
|
||||
hasCrossSigningKeys: boolean | undefined;
|
||||
/**
|
||||
* used to display correct badge value
|
||||
*/
|
||||
isUserVerified: boolean;
|
||||
/**
|
||||
* callback function when verifyUser button is clicked
|
||||
*/
|
||||
verifySelectedUser: () => Promise<void>;
|
||||
}
|
||||
|
||||
const useHomeserverSupportsCrossSigning = (cli: MatrixClient): boolean => {
|
||||
return useAsyncMemo<boolean>(
|
||||
async () => {
|
||||
return cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
|
||||
},
|
||||
[cli],
|
||||
false,
|
||||
);
|
||||
};
|
||||
|
||||
const useHasCrossSigningKeys = (cli: MatrixClient, member: User, canVerify: boolean): boolean | undefined => {
|
||||
return useAsyncMemo(async () => {
|
||||
if (!canVerify) return undefined;
|
||||
return cli.getCrypto()?.userHasCrossSigningKeys(member.userId, true);
|
||||
}, [cli, member, canVerify]);
|
||||
};
|
||||
|
||||
/**
|
||||
* View model for the userInfoVerificationHeaderView
|
||||
* @see {@link UserInfoVerificationSectionState} for more information about what this view model returns.
|
||||
*/
|
||||
export const useUserInfoVerificationViewModel = (
|
||||
member: User | RoomMember,
|
||||
devices: IDevice[],
|
||||
): UserInfoVerificationSectionState => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
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);
|
||||
const verifySelectedUser = (): Promise<void> => verifyUser(cli, member as User);
|
||||
|
||||
return {
|
||||
canVerify,
|
||||
hasCrossSigningKeys,
|
||||
isUserVerified,
|
||||
verifySelectedUser,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,115 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { RoomMember, type User } from "matrix-js-sdk/src/matrix";
|
||||
import { useCallback, useContext } from "react";
|
||||
|
||||
import { mediaFromMxc } from "../../../../customisations/Media";
|
||||
import Modal from "../../../../Modal";
|
||||
import ImageView from "../../../views/elements/ImageView";
|
||||
import SdkConfig from "../../../../SdkConfig";
|
||||
import MatrixClientContext from "../../../../contexts/MatrixClientContext";
|
||||
import { type Member } from "../../../views/right_panel/UserInfo";
|
||||
import { useUserTimezone } from "../../../../hooks/useUserTimezone";
|
||||
import UserIdentifierCustomisations from "../../../../customisations/UserIdentifier";
|
||||
|
||||
export interface PresenceInfo {
|
||||
lastActiveAgo: number | undefined;
|
||||
currentlyActive: boolean | undefined;
|
||||
state: string | undefined;
|
||||
}
|
||||
|
||||
export interface TimezoneInfo {
|
||||
timezone: string;
|
||||
friendly: string;
|
||||
}
|
||||
|
||||
export interface UserInfoHeaderState {
|
||||
/**
|
||||
* callback function when selected user avatar is clicked in user info
|
||||
*/
|
||||
onMemberAvatarClick: () => void;
|
||||
/**
|
||||
* Object containing information about the precense of the selected user
|
||||
*/
|
||||
precenseInfo: PresenceInfo;
|
||||
/**
|
||||
* Boolean that show or hide the precense information
|
||||
*/
|
||||
showPresence: boolean;
|
||||
/**
|
||||
* Timezone object
|
||||
*/
|
||||
timezoneInfo: TimezoneInfo | null;
|
||||
/**
|
||||
* Displayed identifier for the selected user
|
||||
*/
|
||||
userIdentifier: string | null;
|
||||
}
|
||||
interface UserInfoHeaderViewModelProps {
|
||||
member: Member;
|
||||
roomId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* View model for the userInfoHeaderView
|
||||
* props
|
||||
* @see {@link UserInfoHeaderState} for more information about what this view model returns.
|
||||
*/
|
||||
export function useUserfoHeaderViewModel({ member, roomId }: UserInfoHeaderViewModelProps): UserInfoHeaderState {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
let showPresence = true;
|
||||
|
||||
const precenseInfo: PresenceInfo = {
|
||||
lastActiveAgo: undefined,
|
||||
currentlyActive: undefined,
|
||||
state: undefined,
|
||||
};
|
||||
|
||||
const enablePresenceByHsUrl = SdkConfig.get("enable_presence_by_hs_url");
|
||||
|
||||
const timezoneInfo = useUserTimezone(cli, member.userId);
|
||||
|
||||
const userIdentifier = UserIdentifierCustomisations.getDisplayUserIdentifier?.(member.userId, {
|
||||
roomId,
|
||||
withDisplayName: true,
|
||||
});
|
||||
|
||||
const onMemberAvatarClick = useCallback(() => {
|
||||
const avatarUrl = (member as RoomMember).getMxcAvatarUrl
|
||||
? (member as RoomMember).getMxcAvatarUrl()
|
||||
: (member as User).avatarUrl;
|
||||
|
||||
const httpUrl = mediaFromMxc(avatarUrl).srcHttp;
|
||||
if (!httpUrl) return;
|
||||
|
||||
const params = {
|
||||
src: httpUrl,
|
||||
name: (member as RoomMember).name || (member as User).displayName,
|
||||
};
|
||||
|
||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", undefined, true);
|
||||
}, [member]);
|
||||
|
||||
if (member instanceof RoomMember && member.user) {
|
||||
precenseInfo.state = member.user.presence;
|
||||
precenseInfo.lastActiveAgo = member.user.lastActiveAgo;
|
||||
precenseInfo.currentlyActive = member.user.currentlyActive;
|
||||
}
|
||||
|
||||
if (enablePresenceByHsUrl && enablePresenceByHsUrl[cli.baseUrl] !== undefined) {
|
||||
showPresence = enablePresenceByHsUrl[cli.baseUrl];
|
||||
}
|
||||
|
||||
return {
|
||||
onMemberAvatarClick,
|
||||
showPresence,
|
||||
precenseInfo,
|
||||
timezoneInfo,
|
||||
userIdentifier,
|
||||
};
|
||||
}
|
||||
@ -25,8 +25,7 @@ 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 { 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 { MenuItem } from "@vector-im/compound-web";
|
||||
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";
|
||||
@ -40,41 +39,32 @@ import Modal from "../../../Modal";
|
||||
import { _t, UserFriendlyError } from "../../../languageHandler";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import { type ButtonEvent } from "../elements/AccessibleButton";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import MultiInviter from "../../../utils/MultiInviter";
|
||||
import { useTypedEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
|
||||
import EncryptionPanel from "./EncryptionPanel";
|
||||
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
|
||||
import { verifyUser } from "../../../verification";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { useIsEncrypted } from "../../../hooks/useIsEncrypted";
|
||||
import BaseCard from "./BaseCard";
|
||||
import ImageView from "../elements/ImageView";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import MemberAvatar from "../avatars/MemberAvatar";
|
||||
import PresenceLabel from "../rooms/PresenceLabel";
|
||||
import { ShareDialog } from "../dialogs/ShareDialog";
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
import { type ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
||||
import { UIComponent } from "../../../settings/UIFeature";
|
||||
import { TimelineRenderingType } from "../../../contexts/RoomContext";
|
||||
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
||||
import { type IRightPanelCardState } from "../../../stores/right-panel/RightPanelStoreIPanelState";
|
||||
import UserIdentifierCustomisations from "../../../customisations/UserIdentifier";
|
||||
import PosthogTrackers from "../../../PosthogTrackers";
|
||||
import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { DirectoryMember, startDmOnFirstMessage } from "../../../utils/direct-messages";
|
||||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
||||
import { Flex } from "../../utils/Flex";
|
||||
import CopyableText from "../elements/CopyableText";
|
||||
import { useUserTimezone } from "../../../hooks/useUserTimezone";
|
||||
import { UserInfoAdminToolsContainer } from "./user_info/UserInfoAdminToolsContainer";
|
||||
import { PowerLevelSection } from "./user_info/UserInfoPowerLevels";
|
||||
import { UserInfoHeaderView } from "./user_info/UserInfoHeaderView";
|
||||
|
||||
export interface IDevice extends Device {
|
||||
ambiguous?: boolean;
|
||||
@ -298,7 +288,7 @@ export const warnSelfDemote = async (isSpace: boolean): Promise<boolean> => {
|
||||
return !!confirmed;
|
||||
};
|
||||
|
||||
const Container: React.FC<{
|
||||
export const Container: React.FC<{
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}> = ({ children, className }) => {
|
||||
@ -426,16 +416,6 @@ const useIsSynapseAdmin = (cli?: MatrixClient): boolean => {
|
||||
return useAsyncMemo(async () => (cli ? cli.isSynapseAdministrator().catch(() => false) : false), [cli], false);
|
||||
};
|
||||
|
||||
const useHomeserverSupportsCrossSigning = (cli: MatrixClient): boolean => {
|
||||
return useAsyncMemo<boolean>(
|
||||
async () => {
|
||||
return cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
|
||||
},
|
||||
[cli],
|
||||
false,
|
||||
);
|
||||
};
|
||||
|
||||
export interface IRoomPermissions {
|
||||
modifyLevelMax: number;
|
||||
canEdit: boolean;
|
||||
@ -567,80 +547,6 @@ 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;
|
||||
@ -761,114 +667,6 @@ const BasicUserInfo: React.FC<{
|
||||
|
||||
export type Member = User | RoomMember;
|
||||
|
||||
export const UserInfoHeader: React.FC<{
|
||||
member: Member;
|
||||
devices: IDevice[];
|
||||
roomId?: string;
|
||||
hideVerificationSection?: boolean;
|
||||
}> = ({ member, devices, roomId, hideVerificationSection }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
const onMemberAvatarClick = useCallback(() => {
|
||||
const avatarUrl = (member as RoomMember).getMxcAvatarUrl
|
||||
? (member as RoomMember).getMxcAvatarUrl()
|
||||
: (member as User).avatarUrl;
|
||||
|
||||
const httpUrl = mediaFromMxc(avatarUrl).srcHttp;
|
||||
if (!httpUrl) return;
|
||||
|
||||
const params = {
|
||||
src: httpUrl,
|
||||
name: (member as RoomMember).name || (member as User).displayName,
|
||||
};
|
||||
|
||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", undefined, true);
|
||||
}, [member]);
|
||||
|
||||
const avatarUrl = (member as User).avatarUrl;
|
||||
|
||||
let presenceState: string | undefined;
|
||||
let presenceLastActiveAgo: number | undefined;
|
||||
let presenceCurrentlyActive: boolean | undefined;
|
||||
if (member instanceof RoomMember && member.user) {
|
||||
presenceState = member.user.presence;
|
||||
presenceLastActiveAgo = member.user.lastActiveAgo;
|
||||
presenceCurrentlyActive = member.user.currentlyActive;
|
||||
}
|
||||
|
||||
const enablePresenceByHsUrl = SdkConfig.get("enable_presence_by_hs_url");
|
||||
let showPresence = true;
|
||||
if (enablePresenceByHsUrl && enablePresenceByHsUrl[cli.baseUrl] !== undefined) {
|
||||
showPresence = enablePresenceByHsUrl[cli.baseUrl];
|
||||
}
|
||||
|
||||
let presenceLabel: JSX.Element | undefined;
|
||||
if (showPresence) {
|
||||
presenceLabel = (
|
||||
<PresenceLabel
|
||||
activeAgo={presenceLastActiveAgo}
|
||||
currentlyActive={presenceCurrentlyActive}
|
||||
presenceState={presenceState}
|
||||
className="mx_UserInfo_profileStatus"
|
||||
coloured
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const timezoneInfo = useUserTimezone(cli, member.userId);
|
||||
|
||||
const userIdentifier = UserIdentifierCustomisations.getDisplayUserIdentifier?.(member.userId, {
|
||||
roomId,
|
||||
withDisplayName: true,
|
||||
});
|
||||
const displayName = (member as RoomMember).rawDisplayName;
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="mx_UserInfo_avatar">
|
||||
<div className="mx_UserInfo_avatar_transition">
|
||||
<div className="mx_UserInfo_avatar_transition_child">
|
||||
<MemberAvatar
|
||||
key={member.userId} // to instantly blank the avatar when UserInfo changes members
|
||||
member={member as RoomMember}
|
||||
size="120px"
|
||||
resizeMethod="scale"
|
||||
fallbackUserId={member.userId}
|
||||
onClick={onMemberAvatarClick}
|
||||
urls={avatarUrl ? [avatarUrl] : undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Container className="mx_UserInfo_header">
|
||||
<Flex direction="column" align="center" className="mx_UserInfo_profile">
|
||||
<Heading size="sm" weight="semibold" as="h1" dir="auto">
|
||||
<Flex className="mx_UserInfo_profile_name" direction="row-reverse" align="center">
|
||||
{displayName}
|
||||
</Flex>
|
||||
</Heading>
|
||||
{presenceLabel}
|
||||
{timezoneInfo && (
|
||||
<Tooltip label={timezoneInfo?.timezone ?? ""}>
|
||||
<Flex align="center" className="mx_UserInfo_timezone">
|
||||
<Text size="sm" weight="regular">
|
||||
{timezoneInfo?.friendly ?? ""}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Text size="sm" weight="semibold" className="mx_UserInfo_profile_mxid">
|
||||
<CopyableText getTextToCopy={() => userIdentifier} border={false}>
|
||||
{userIdentifier}
|
||||
</CopyableText>
|
||||
</Text>
|
||||
</Flex>
|
||||
{!hideVerificationSection && <VerificationSection member={member} devices={devices} />}
|
||||
</Container>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
interface IProps {
|
||||
user: Member;
|
||||
room?: Room;
|
||||
@ -927,7 +725,7 @@ const UserInfo: React.FC<IProps> = ({ user, room, onClose, phase = RightPanelPha
|
||||
|
||||
const header = (
|
||||
<>
|
||||
<UserInfoHeader
|
||||
<UserInfoHeaderView
|
||||
hideVerificationSection={phase === RightPanelPhases.EncryptionPanel}
|
||||
member={member}
|
||||
devices={devices}
|
||||
|
||||
@ -0,0 +1,63 @@
|
||||
/*
|
||||
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 { type User, type RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import { Text, Button, InlineSpinner, Badge } from "@vector-im/compound-web";
|
||||
import { VerifiedIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import { useUserInfoVerificationViewModel } from "../../../viewmodels/right_panel/user_info/UserInfoHeaderVerificationViewModel";
|
||||
import { type IDevice } from "../UserInfo";
|
||||
import { Flex } from "../../../utils/Flex";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
|
||||
export const UserInfoHeaderVerificationView: React.FC<{
|
||||
member: User | RoomMember;
|
||||
devices: IDevice[];
|
||||
}> = ({ member, devices }) => {
|
||||
let content;
|
||||
const vm = useUserInfoVerificationViewModel(member, devices);
|
||||
|
||||
if (vm.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 (vm.hasCrossSigningKeys === undefined) {
|
||||
// We are still fetching the cross-signing keys for the user, show spinner.
|
||||
content = <InlineSpinner size={24} />;
|
||||
} else if (vm.canVerify && vm.hasCrossSigningKeys) {
|
||||
content = (
|
||||
<div className="mx_UserInfo_container_verifyButton">
|
||||
<Button
|
||||
className="mx_UserInfo_verify_button"
|
||||
kind="tertiary"
|
||||
size="sm"
|
||||
onClick={() => vm.verifySelectedUser()}
|
||||
>
|
||||
{_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>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,96 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type JSX } from "react";
|
||||
import { type User, type RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import { Heading, Tooltip, Text } from "@vector-im/compound-web";
|
||||
|
||||
import { useUserfoHeaderViewModel } from "../../../viewmodels/right_panel/user_info/UserInfoHeaderViewModel";
|
||||
import MemberAvatar from "../../avatars/MemberAvatar";
|
||||
import { Container, type Member, type IDevice } from "../UserInfo";
|
||||
import { Flex } from "../../../utils/Flex";
|
||||
import PresenceLabel from "../../rooms/PresenceLabel";
|
||||
import CopyableText from "../../elements/CopyableText";
|
||||
import { UserInfoHeaderVerificationView } from "./UserInfoHeaderVerificationView";
|
||||
|
||||
export interface UserInfoHeaderViewProps {
|
||||
member: Member;
|
||||
roomId?: string;
|
||||
devices: IDevice[];
|
||||
hideVerificationSection: boolean;
|
||||
}
|
||||
|
||||
export const UserInfoHeaderView: React.FC<UserInfoHeaderViewProps> = ({
|
||||
member,
|
||||
devices,
|
||||
roomId,
|
||||
hideVerificationSection,
|
||||
}) => {
|
||||
const vm = useUserfoHeaderViewModel({ member, roomId });
|
||||
const avatarUrl = (member as User).avatarUrl;
|
||||
const displayName = (member as RoomMember).rawDisplayName;
|
||||
|
||||
let presenceLabel: JSX.Element | undefined;
|
||||
|
||||
if (vm.showPresence) {
|
||||
presenceLabel = (
|
||||
<PresenceLabel
|
||||
activeAgo={vm.precenseInfo.lastActiveAgo}
|
||||
currentlyActive={vm.precenseInfo.currentlyActive}
|
||||
presenceState={vm.precenseInfo.state}
|
||||
className="mx_UserInfo_profileStatus"
|
||||
coloured
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="mx_UserInfo_avatar">
|
||||
<div className="mx_UserInfo_avatar_transition">
|
||||
<div className="mx_UserInfo_avatar_transition_child">
|
||||
<MemberAvatar
|
||||
key={member.userId} // to instantly blank the avatar when UserInfo changes members
|
||||
member={member as RoomMember}
|
||||
size="120px"
|
||||
resizeMethod="scale"
|
||||
fallbackUserId={member.userId}
|
||||
onClick={vm.onMemberAvatarClick}
|
||||
urls={avatarUrl ? [avatarUrl] : undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Container className="mx_UserInfo_header">
|
||||
<Flex direction="column" align="center" className="mx_UserInfo_profile">
|
||||
<Heading size="sm" weight="semibold" as="h1" dir="auto">
|
||||
<Flex className="mx_UserInfo_profile_name" direction="row-reverse" align="center">
|
||||
{displayName}
|
||||
</Flex>
|
||||
</Heading>
|
||||
{presenceLabel}
|
||||
{vm.timezoneInfo && (
|
||||
<Tooltip label={vm.timezoneInfo?.timezone ?? ""}>
|
||||
<Flex align="center" className="mx_UserInfo_timezone">
|
||||
<Text size="sm" weight="regular">
|
||||
{vm.timezoneInfo?.friendly ?? ""}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Text size="sm" weight="semibold" className="mx_UserInfo_profile_mxid">
|
||||
<CopyableText getTextToCopy={() => vm.userIdentifier} border={false}>
|
||||
{vm.userIdentifier}
|
||||
</CopyableText>
|
||||
</Text>
|
||||
</Flex>
|
||||
{!hideVerificationSection && <UserInfoHeaderVerificationView member={member} devices={devices} />}
|
||||
</Container>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,194 @@
|
||||
/*
|
||||
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 { Device, type MatrixClient, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import { type Mocked } from "jest-mock";
|
||||
import { UserVerificationStatus, type CryptoApi } from "matrix-js-sdk/src/crypto-api";
|
||||
import { renderHook, waitFor } from "jest-matrix-react";
|
||||
|
||||
import { createTestClient, withClientContextRenderOptions } from "../../../../../test-utils";
|
||||
import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
|
||||
import { useUserInfoVerificationViewModel } from "../../../../../../src/components/viewmodels/right_panel/user_info/UserInfoHeaderVerificationViewModel";
|
||||
|
||||
describe("useUserInfoVerificationHeaderViewModel", () => {
|
||||
const defaultRoomId = "!fkfk";
|
||||
const defaultUserId = "@user:example.com";
|
||||
|
||||
const defaultMember = new RoomMember(defaultRoomId, defaultUserId);
|
||||
|
||||
const defaultProps = {
|
||||
devices: [] as Device[],
|
||||
member: defaultMember,
|
||||
};
|
||||
let mockClient: MatrixClient;
|
||||
let mockCrypto: Mocked<CryptoApi>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockCrypto = {
|
||||
bootstrapSecretStorage: jest.fn(),
|
||||
bootstrapCrossSigning: jest.fn(),
|
||||
getCrossSigningKeyId: jest.fn(),
|
||||
getVerificationRequestsToDeviceInProgress: jest.fn().mockReturnValue([]),
|
||||
getUserDeviceInfo: jest.fn(),
|
||||
getDeviceVerificationStatus: jest.fn(),
|
||||
getUserVerificationStatus: jest.fn(),
|
||||
isDehydrationSupported: jest.fn().mockResolvedValue(false),
|
||||
startDehydration: jest.fn(),
|
||||
getKeyBackupInfo: jest.fn().mockResolvedValue(null),
|
||||
userHasCrossSigningKeys: jest.fn().mockResolvedValue(false),
|
||||
} as unknown as Mocked<CryptoApi>;
|
||||
|
||||
mockClient = createTestClient();
|
||||
jest.spyOn(mockClient, "doesServerSupportUnstableFeature").mockResolvedValue(true);
|
||||
jest.spyOn(mockClient.secretStorage, "hasKey").mockResolvedValue(true);
|
||||
jest.spyOn(mockClient, "getCrypto").mockReturnValue(mockCrypto);
|
||||
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient);
|
||||
jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(mockClient);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
const renderUserInfoHeaderVerificationHook = (props = defaultProps) => {
|
||||
return renderHook(
|
||||
() => useUserInfoVerificationViewModel(props.member, props.devices),
|
||||
withClientContextRenderOptions(mockClient),
|
||||
);
|
||||
};
|
||||
|
||||
it("should be able to verify user", async () => {
|
||||
const notMeId = "@notMe";
|
||||
const notMetMember = new RoomMember(defaultRoomId, notMeId);
|
||||
const device1 = new Device({
|
||||
deviceId: "d1",
|
||||
userId: notMeId,
|
||||
displayName: "my device",
|
||||
algorithms: [],
|
||||
keys: new Map(),
|
||||
});
|
||||
|
||||
// mock the user as not verified
|
||||
jest.spyOn(mockCrypto, "getUserVerificationStatus").mockResolvedValue(
|
||||
new UserVerificationStatus(false, false, false),
|
||||
);
|
||||
|
||||
jest.spyOn(mockClient, "getUserId").mockReturnValue(defaultMember.userId);
|
||||
|
||||
// the selected user is not the default user, so he can make user verification
|
||||
const { result } = renderUserInfoHeaderVerificationHook({ member: notMetMember, devices: [device1] });
|
||||
await waitFor(() => {
|
||||
const canVerify = result.current.canVerify;
|
||||
|
||||
expect(canVerify).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not be able to verify user if user is not me", async () => {
|
||||
const device1 = new Device({
|
||||
deviceId: "d1",
|
||||
userId: defaultMember.userId,
|
||||
displayName: "my device",
|
||||
algorithms: [],
|
||||
keys: new Map(),
|
||||
});
|
||||
|
||||
// mock the user as not verified
|
||||
jest.spyOn(mockCrypto, "getUserVerificationStatus").mockResolvedValue(
|
||||
new UserVerificationStatus(false, false, false),
|
||||
);
|
||||
|
||||
jest.spyOn(mockClient, "getUserId").mockReturnValue(defaultMember.userId);
|
||||
|
||||
const { result } = renderUserInfoHeaderVerificationHook({ member: defaultMember, devices: [device1] });
|
||||
await waitFor(() => {
|
||||
const canVerify = result.current.canVerify;
|
||||
|
||||
expect(canVerify).toBeFalsy();
|
||||
// if we cant verify the user the hasCrossSigningKeys value should also be undefined
|
||||
expect(result.current.hasCrossSigningKeys).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not be able to verify user if im already verified", async () => {
|
||||
const notMeId = "@notMe";
|
||||
const notMetMember = new RoomMember(defaultRoomId, notMeId);
|
||||
const device1 = new Device({
|
||||
deviceId: "d1",
|
||||
userId: notMeId,
|
||||
displayName: "my device",
|
||||
algorithms: [],
|
||||
keys: new Map(),
|
||||
});
|
||||
|
||||
// mock the user as already verified
|
||||
jest.spyOn(mockCrypto, "getUserVerificationStatus").mockResolvedValue(
|
||||
new UserVerificationStatus(true, true, false),
|
||||
);
|
||||
|
||||
jest.spyOn(mockClient, "getUserId").mockReturnValue(defaultMember.userId);
|
||||
|
||||
// the selected user is not the default user, so he can make user verification
|
||||
const { result } = renderUserInfoHeaderVerificationHook({ member: notMetMember, devices: [device1] });
|
||||
await waitFor(() => {
|
||||
const canVerify = result.current.canVerify;
|
||||
|
||||
expect(canVerify).toBeFalsy();
|
||||
// if we cant verify the user the hasCrossSigningKeys value should also be undefined
|
||||
expect(result.current.hasCrossSigningKeys).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not be able to verify user there is no devices", async () => {
|
||||
const notMeId = "@notMe";
|
||||
const notMetMember = new RoomMember(defaultRoomId, notMeId);
|
||||
|
||||
// mock the user as not verified
|
||||
jest.spyOn(mockCrypto, "getUserVerificationStatus").mockResolvedValue(
|
||||
new UserVerificationStatus(false, false, false),
|
||||
);
|
||||
|
||||
jest.spyOn(mockClient, "getUserId").mockReturnValue(defaultMember.userId);
|
||||
|
||||
// the selected user is not the default user, so he can make user verification
|
||||
const { result } = renderUserInfoHeaderVerificationHook({ member: notMetMember, devices: [] });
|
||||
await waitFor(() => {
|
||||
const canVerify = result.current.canVerify;
|
||||
|
||||
expect(canVerify).toBeFalsy();
|
||||
// if we cant verify the user the hasCrossSigningKeys value should also be undefined
|
||||
expect(result.current.hasCrossSigningKeys).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should get correct hasCrossSigningKeys values", async () => {
|
||||
const notMeId = "@notMe";
|
||||
const notMetMember = new RoomMember(defaultRoomId, notMeId);
|
||||
const device1 = new Device({
|
||||
deviceId: "d1",
|
||||
userId: notMeId,
|
||||
displayName: "my device",
|
||||
algorithms: [],
|
||||
keys: new Map(),
|
||||
});
|
||||
|
||||
// mock the user as not verified
|
||||
jest.spyOn(mockCrypto, "getUserVerificationStatus").mockResolvedValue(
|
||||
new UserVerificationStatus(false, false, false),
|
||||
);
|
||||
|
||||
jest.spyOn(mockClient, "getUserId").mockReturnValue(defaultMember.userId);
|
||||
|
||||
jest.spyOn(mockCrypto, "userHasCrossSigningKeys").mockResolvedValue(true);
|
||||
const { result } = renderUserInfoHeaderVerificationHook({ member: notMetMember, devices: [device1] });
|
||||
await waitFor(() => {
|
||||
const hasCrossSigningKeys = result.current.hasCrossSigningKeys;
|
||||
|
||||
expect(hasCrossSigningKeys).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,179 @@
|
||||
/*
|
||||
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 { type MatrixClient, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import { mocked, type Mocked } from "jest-mock";
|
||||
import { type CryptoApi } from "matrix-js-sdk/src/crypto-api";
|
||||
import { renderHook } from "jest-matrix-react";
|
||||
|
||||
import { withClientContextRenderOptions } from "../../../../../test-utils";
|
||||
import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
|
||||
import { useUserfoHeaderViewModel } from "../../../../../../src/components/viewmodels/right_panel/user_info/UserInfoHeaderViewModel";
|
||||
import * as UseTimezone from "../../../../../../src/hooks/useUserTimezone";
|
||||
import SdkConfig from "../../../../../../src/SdkConfig";
|
||||
import Modal from "../../../../../../src/Modal";
|
||||
import ImageView from "../../../../../../src/components/views/elements/ImageView";
|
||||
import * as Media from "../../../../../../src/customisations/Media";
|
||||
import { type IConfigOptions } from "../../../../../../src/IConfigOptions";
|
||||
|
||||
jest.mock("../../../../../../src/customisations/UserIdentifier", () => {
|
||||
return {
|
||||
getDisplayUserIdentifier: jest.fn().mockReturnValue("customUserIdentifier"),
|
||||
};
|
||||
});
|
||||
|
||||
describe("useUserInfoHeaderViewModel", () => {
|
||||
const defaultRoomId = "!fkfk";
|
||||
const defaultUserId = "@user:example.com";
|
||||
|
||||
const defaultMember = new RoomMember(defaultRoomId, defaultUserId);
|
||||
|
||||
const defaultProps = {
|
||||
member: defaultMember,
|
||||
roomId: defaultRoomId,
|
||||
};
|
||||
|
||||
let mockClient: Mocked<MatrixClient>;
|
||||
let mockCrypto: Mocked<CryptoApi>;
|
||||
|
||||
const mockAvatarUrl = "mock-avatar-url";
|
||||
const oldGet = SdkConfig.get;
|
||||
|
||||
beforeEach(() => {
|
||||
mockCrypto = mocked({
|
||||
getDeviceVerificationStatus: jest.fn(),
|
||||
getUserDeviceInfo: jest.fn(),
|
||||
userHasCrossSigningKeys: jest.fn().mockResolvedValue(false),
|
||||
getUserVerificationStatus: jest.fn(),
|
||||
isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(false),
|
||||
} as unknown as CryptoApi);
|
||||
|
||||
mockClient = mocked({
|
||||
getUser: jest.fn(),
|
||||
isGuest: jest.fn().mockReturnValue(false),
|
||||
isUserIgnored: jest.fn(),
|
||||
getIgnoredUsers: jest.fn(),
|
||||
setIgnoredUsers: jest.fn(),
|
||||
getUserId: jest.fn(),
|
||||
getSafeUserId: jest.fn(),
|
||||
getDomain: jest.fn(),
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
isSynapseAdministrator: jest.fn().mockResolvedValue(false),
|
||||
doesServerSupportUnstableFeature: jest.fn().mockReturnValue(false),
|
||||
doesServerSupportExtendedProfiles: jest.fn().mockResolvedValue(false),
|
||||
getExtendedProfileProperty: jest.fn().mockRejectedValue(new Error("Not supported")),
|
||||
mxcUrlToHttp: jest.fn().mockReturnValue(mockAvatarUrl),
|
||||
removeListener: jest.fn(),
|
||||
currentState: {
|
||||
on: jest.fn(),
|
||||
},
|
||||
getRoom: jest.fn(),
|
||||
credentials: {},
|
||||
setPowerLevel: jest.fn(),
|
||||
getCrypto: jest.fn().mockReturnValue(mockCrypto),
|
||||
baseUrl: "homeserver.url",
|
||||
} as unknown as MatrixClient);
|
||||
|
||||
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient);
|
||||
jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(mockClient);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
const renderUserInfoHeaderViewModelHook = (props = defaultProps) => {
|
||||
return renderHook(() => useUserfoHeaderViewModel(props), withClientContextRenderOptions(mockClient));
|
||||
};
|
||||
it("should give user timezone info", () => {
|
||||
const defaultTZ = { timezone: "FR", friendly: "fr" };
|
||||
jest.spyOn(UseTimezone, "useUserTimezone").mockReturnValue(defaultTZ);
|
||||
|
||||
const { result } = renderUserInfoHeaderViewModelHook();
|
||||
const timezone = result.current.timezoneInfo;
|
||||
|
||||
expect(UseTimezone.useUserTimezone).toHaveBeenCalledWith(mockClient, defaultMember.userId);
|
||||
expect(timezone).toEqual(defaultTZ);
|
||||
});
|
||||
|
||||
it("should give correct showPresence value based on enablePresenceByHsUrl", () => {
|
||||
jest.spyOn(SdkConfig, "get").mockImplementation((key: string) => {
|
||||
if (key === "enable_presence_by_hs_url") {
|
||||
return {
|
||||
[mockClient.baseUrl]: false,
|
||||
};
|
||||
}
|
||||
return oldGet(key as keyof IConfigOptions);
|
||||
});
|
||||
const { result } = renderUserInfoHeaderViewModelHook();
|
||||
const showPresence = result.current.showPresence;
|
||||
expect(showPresence).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should have default value true for showPresence", () => {
|
||||
jest.spyOn(SdkConfig, "get").mockImplementation(() => false);
|
||||
const { result } = renderUserInfoHeaderViewModelHook();
|
||||
const showPresence = result.current.showPresence;
|
||||
expect(showPresence).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should open image dialog when avatar is clicked", () => {
|
||||
const props = Object.assign({}, defaultProps);
|
||||
const spyModale = jest.spyOn(Modal, "createDialog");
|
||||
const spyMedia = jest.spyOn(Media, "mediaFromMxc");
|
||||
jest.spyOn(props.member, "getMxcAvatarUrl").mockReturnValue(mockAvatarUrl);
|
||||
|
||||
const { result } = renderUserInfoHeaderViewModelHook(props);
|
||||
|
||||
result.current.onMemberAvatarClick();
|
||||
|
||||
expect(spyModale).toHaveBeenCalledWith(
|
||||
ImageView,
|
||||
{
|
||||
src: mockAvatarUrl,
|
||||
name: defaultMember.name,
|
||||
},
|
||||
"mx_Dialog_lightbox",
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
expect(spyMedia).toHaveBeenCalledWith(mockAvatarUrl);
|
||||
});
|
||||
|
||||
it("should not open image dialog when avatar url is null", () => {
|
||||
const props = Object.assign({}, defaultProps);
|
||||
const spyModale = jest.spyOn(Modal, "createDialog");
|
||||
jest.spyOn(props.member, "getMxcAvatarUrl").mockReturnValue(mockAvatarUrl);
|
||||
jest.spyOn(Media, "mediaFromMxc").mockReturnValue({
|
||||
srcHttp: null,
|
||||
isEncrypted: false,
|
||||
srcMxc: "",
|
||||
thumbnailMxc: undefined,
|
||||
hasThumbnail: false,
|
||||
thumbnailHttp: null,
|
||||
getThumbnailHttp: function (width: number, height: number, mode?: "scale" | "crop"): string | null {
|
||||
throw new Error("Function not implemented.");
|
||||
},
|
||||
getThumbnailOfSourceHttp: function (width: number, height: number, mode?: "scale" | "crop"): string | null {
|
||||
throw new Error("Function not implemented.");
|
||||
},
|
||||
getSquareThumbnailHttp: function (dim: number): string | null {
|
||||
throw new Error("Function not implemented.");
|
||||
},
|
||||
downloadSource: function (): Promise<Response> {
|
||||
throw new Error("Function not implemented.");
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderUserInfoHeaderViewModelHook(props);
|
||||
|
||||
result.current.onMemberAvatarClick();
|
||||
|
||||
expect(spyModale).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render, screen, act, waitForElementToBeRemoved, waitFor } from "jest-matrix-react";
|
||||
import { render, screen, act, waitForElementToBeRemoved } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { type Mocked, mocked } from "jest-mock";
|
||||
import { type Room, User, type MatrixClient, RoomMember, Device } from "matrix-js-sdk/src/matrix";
|
||||
@ -23,7 +23,6 @@ import {
|
||||
import UserInfo, {
|
||||
disambiguateDevices,
|
||||
getPowerLevels,
|
||||
UserInfoHeader,
|
||||
UserOptionsSection,
|
||||
} from "../../../../../src/components/views/right_panel/UserInfo";
|
||||
import dis from "../../../../../src/dispatcher/dispatcher";
|
||||
@ -440,64 +439,6 @@ describe("<UserInfo />", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("<UserInfoHeader />", () => {
|
||||
const defaultMember = new RoomMember(defaultRoomId, defaultUserId);
|
||||
|
||||
const defaultProps = {
|
||||
member: defaultMember,
|
||||
roomId: defaultRoomId,
|
||||
};
|
||||
|
||||
const renderComponent = (props = {}) => {
|
||||
const device1 = new Device({
|
||||
deviceId: "d1",
|
||||
userId: defaultUserId,
|
||||
displayName: "my device",
|
||||
algorithms: [],
|
||||
keys: new Map(),
|
||||
});
|
||||
const devicesMap = new Map<string, Device>([[device1.deviceId, device1]]);
|
||||
const userDeviceMap = new Map<string, Map<string, Device>>([[defaultUserId, devicesMap]]);
|
||||
mockCrypto.getUserDeviceInfo.mockResolvedValue(userDeviceMap);
|
||||
mockClient.doesServerSupportUnstableFeature.mockResolvedValue(true);
|
||||
const Wrapper = (wrapperProps = {}) => {
|
||||
return <MatrixClientContext.Provider value={mockClient} {...wrapperProps} />;
|
||||
};
|
||||
|
||||
return render(<UserInfoHeader {...defaultProps} {...props} devices={[device1]} />, {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
};
|
||||
|
||||
it("renders custom user identifiers in the header", () => {
|
||||
renderComponent();
|
||||
expect(screen.getByText("customUserIdentifier")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders verified badge when user is verified", async () => {
|
||||
mockCrypto.getUserVerificationStatus.mockResolvedValue(new UserVerificationStatus(true, true, false));
|
||||
const { container } = renderComponent();
|
||||
await waitFor(() => expect(screen.getByText("Verified")).toBeInTheDocument());
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders verify button", async () => {
|
||||
mockCrypto.getUserVerificationStatus.mockResolvedValue(new UserVerificationStatus(false, false, false));
|
||||
mockCrypto.userHasCrossSigningKeys.mockResolvedValue(true);
|
||||
const { container } = renderComponent();
|
||||
await waitFor(() => expect(screen.getByText("Verify User")).toBeInTheDocument());
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders verification unavailable message", async () => {
|
||||
mockCrypto.getUserVerificationStatus.mockResolvedValue(new UserVerificationStatus(false, false, false));
|
||||
mockCrypto.userHasCrossSigningKeys.mockResolvedValue(false);
|
||||
const { container } = renderComponent();
|
||||
await waitFor(() => expect(screen.getByText("(User verification unavailable)")).toBeInTheDocument());
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("<UserOptionsSection />", () => {
|
||||
const member = new RoomMember(defaultRoomId, defaultUserId);
|
||||
const defaultProps = { member, canInvite: false, isSpace: false };
|
||||
|
||||
@ -0,0 +1,96 @@
|
||||
/*
|
||||
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 { mocked, type Mocked } from "jest-mock";
|
||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { UserVerificationStatus, type CryptoApi } from "matrix-js-sdk/src/crypto-api";
|
||||
import { Device, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import { render, waitFor, screen } from "jest-matrix-react";
|
||||
import React from "react";
|
||||
|
||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import { UserInfoHeaderVerificationView } from "../../../../../src/components/views/right_panel/user_info/UserInfoHeaderVerificationView";
|
||||
import { createTestClient } from "../../../../test-utils";
|
||||
|
||||
describe("<UserInfoHeaderVerificationView />", () => {
|
||||
const defaultRoomId = "!fkfk";
|
||||
const defaultUserId = "@user:example.com";
|
||||
|
||||
const defaultMember = new RoomMember(defaultRoomId, defaultUserId);
|
||||
|
||||
let mockClient: MatrixClient;
|
||||
let mockCrypto: Mocked<CryptoApi>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockCrypto = mocked({
|
||||
bootstrapSecretStorage: jest.fn(),
|
||||
bootstrapCrossSigning: jest.fn(),
|
||||
getCrossSigningKeyId: jest.fn(),
|
||||
getVerificationRequestsToDeviceInProgress: jest.fn().mockReturnValue([]),
|
||||
getUserDeviceInfo: jest.fn(),
|
||||
getDeviceVerificationStatus: jest.fn(),
|
||||
getUserVerificationStatus: jest.fn(),
|
||||
isDehydrationSupported: jest.fn().mockResolvedValue(false),
|
||||
startDehydration: jest.fn(),
|
||||
getKeyBackupInfo: jest.fn().mockResolvedValue(null),
|
||||
userHasCrossSigningKeys: jest.fn().mockResolvedValue(false),
|
||||
} as unknown as CryptoApi);
|
||||
|
||||
mockClient = createTestClient();
|
||||
jest.spyOn(mockClient, "doesServerSupportUnstableFeature").mockResolvedValue(true);
|
||||
jest.spyOn(mockClient.secretStorage, "hasKey").mockResolvedValue(true);
|
||||
jest.spyOn(mockClient, "getCrypto").mockReturnValue(mockCrypto);
|
||||
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient);
|
||||
jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(mockClient);
|
||||
});
|
||||
|
||||
const renderComponent = () => {
|
||||
const device1 = new Device({
|
||||
deviceId: "d1",
|
||||
userId: defaultUserId,
|
||||
displayName: "my device",
|
||||
algorithms: [],
|
||||
keys: new Map(),
|
||||
});
|
||||
const devicesMap = new Map<string, Device>([[device1.deviceId, device1]]);
|
||||
const userDeviceMap = new Map<string, Map<string, Device>>([[defaultUserId, devicesMap]]);
|
||||
|
||||
mockCrypto.getUserDeviceInfo.mockResolvedValue(userDeviceMap);
|
||||
jest.spyOn(mockClient, "doesServerSupportUnstableFeature").mockResolvedValue(true);
|
||||
const Wrapper = (wrapperProps = {}) => {
|
||||
return <MatrixClientContext.Provider value={mockClient} {...wrapperProps} />;
|
||||
};
|
||||
|
||||
return render(<UserInfoHeaderVerificationView member={defaultMember} devices={[device1]} />, {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
};
|
||||
|
||||
it("renders verified badge when user is verified", async () => {
|
||||
mockCrypto.getUserVerificationStatus.mockResolvedValue(new UserVerificationStatus(true, true, false));
|
||||
const { container } = renderComponent();
|
||||
await waitFor(() => expect(screen.getByText("Verified")).toBeInTheDocument());
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders verify button", async () => {
|
||||
mockCrypto.getUserVerificationStatus.mockResolvedValue(new UserVerificationStatus(false, false, false));
|
||||
mockCrypto.userHasCrossSigningKeys.mockResolvedValue(true);
|
||||
const { container } = renderComponent();
|
||||
await waitFor(() => expect(screen.getByText("Verify User")).toBeInTheDocument());
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders verification unavailable message", async () => {
|
||||
mockCrypto.getUserVerificationStatus.mockResolvedValue(new UserVerificationStatus(false, false, false));
|
||||
mockCrypto.userHasCrossSigningKeys.mockResolvedValue(false);
|
||||
const { container } = renderComponent();
|
||||
await waitFor(() => expect(screen.getByText("(User verification unavailable)")).toBeInTheDocument());
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,195 @@
|
||||
/*
|
||||
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 { mocked, type Mocked } from "jest-mock";
|
||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { type CryptoApi } from "matrix-js-sdk/src/crypto-api";
|
||||
import { Device, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import { fireEvent, render, screen } from "jest-matrix-react";
|
||||
import React from "react";
|
||||
|
||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import { UserInfoHeaderView } from "../../../../../src/components/views/right_panel/user_info/UserInfoHeaderView";
|
||||
import { createTestClient } from "../../../../test-utils";
|
||||
import { useUserfoHeaderViewModel } from "../../../../../src/components/viewmodels/right_panel/user_info/UserInfoHeaderViewModel";
|
||||
|
||||
// Mock the viewmodel hooks
|
||||
jest.mock("../../../../../src/components/viewmodels/right_panel/user_info/UserInfoHeaderViewModel", () => ({
|
||||
useUserfoHeaderViewModel: jest.fn().mockReturnValue({
|
||||
onMemberAvatarClick: jest.fn(),
|
||||
precenseInfo: {
|
||||
lastActiveAgo: undefined,
|
||||
currentlyActive: undefined,
|
||||
state: undefined,
|
||||
},
|
||||
showPresence: false,
|
||||
timezoneInfo: null,
|
||||
userIdentifier: "customUserIdentifier",
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("<UserInfoHeaderView />", () => {
|
||||
const defaultRoomId = "!fkfk";
|
||||
const defaultUserId = "@user:example.com";
|
||||
|
||||
const defaultMember = new RoomMember(defaultRoomId, defaultUserId);
|
||||
const defaultProps = {
|
||||
member: defaultMember,
|
||||
roomId: defaultRoomId,
|
||||
};
|
||||
|
||||
let mockClient: MatrixClient;
|
||||
let mockCrypto: Mocked<CryptoApi>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockCrypto = mocked({
|
||||
bootstrapSecretStorage: jest.fn(),
|
||||
bootstrapCrossSigning: jest.fn(),
|
||||
getCrossSigningKeyId: jest.fn(),
|
||||
getVerificationRequestsToDeviceInProgress: jest.fn().mockReturnValue([]),
|
||||
getUserDeviceInfo: jest.fn(),
|
||||
getDeviceVerificationStatus: jest.fn(),
|
||||
getUserVerificationStatus: jest.fn(),
|
||||
isDehydrationSupported: jest.fn().mockResolvedValue(false),
|
||||
startDehydration: jest.fn(),
|
||||
getKeyBackupInfo: jest.fn().mockResolvedValue(null),
|
||||
userHasCrossSigningKeys: jest.fn().mockResolvedValue(false),
|
||||
} as unknown as CryptoApi);
|
||||
|
||||
mockClient = createTestClient();
|
||||
mockClient.doesServerSupportExtendedProfiles = () => Promise.resolve(false);
|
||||
|
||||
jest.spyOn(mockClient, "doesServerSupportUnstableFeature").mockResolvedValue(true);
|
||||
jest.spyOn(mockClient.secretStorage, "hasKey").mockResolvedValue(true);
|
||||
jest.spyOn(mockClient, "getCrypto").mockReturnValue(mockCrypto);
|
||||
jest.spyOn(mockClient, "doesServerSupportUnstableFeature").mockResolvedValue(true);
|
||||
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient);
|
||||
jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(mockClient);
|
||||
});
|
||||
|
||||
const renderComponent = (
|
||||
props = {
|
||||
hideVerificationSection: false,
|
||||
},
|
||||
) => {
|
||||
const device1 = new Device({
|
||||
deviceId: "d1",
|
||||
userId: defaultUserId,
|
||||
displayName: "my device",
|
||||
algorithms: [],
|
||||
keys: new Map(),
|
||||
});
|
||||
|
||||
const devicesMap = new Map<string, Device>([[device1.deviceId, device1]]);
|
||||
const userDeviceMap = new Map<string, Map<string, Device>>([[defaultUserId, devicesMap]]);
|
||||
|
||||
mockCrypto.getUserDeviceInfo.mockResolvedValue(userDeviceMap);
|
||||
|
||||
const Wrapper = (wrapperProps = {}) => {
|
||||
return <MatrixClientContext.Provider value={mockClient} {...wrapperProps} />;
|
||||
};
|
||||
|
||||
return render(
|
||||
<UserInfoHeaderView
|
||||
{...defaultProps}
|
||||
{...props}
|
||||
devices={[device1]}
|
||||
hideVerificationSection={props.hideVerificationSection}
|
||||
/>,
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
it("renders custom user identifiers in the header", () => {
|
||||
const { container } = renderComponent();
|
||||
expect(screen.getByText("customUserIdentifier")).toBeInTheDocument();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should not render verification view if hideVerificationSection is true", () => {
|
||||
mocked(useUserfoHeaderViewModel).mockReturnValue({
|
||||
onMemberAvatarClick: jest.fn(),
|
||||
precenseInfo: {
|
||||
lastActiveAgo: undefined,
|
||||
currentlyActive: undefined,
|
||||
state: undefined,
|
||||
},
|
||||
showPresence: false,
|
||||
timezoneInfo: null,
|
||||
userIdentifier: "null",
|
||||
});
|
||||
|
||||
const { container } = renderComponent({ hideVerificationSection: true });
|
||||
const verificationClass = container.getElementsByClassName("mx_UserInfo_verification").length;
|
||||
|
||||
expect(verificationClass).toEqual(0);
|
||||
});
|
||||
|
||||
it("should render timezone if it exist", () => {
|
||||
mocked(useUserfoHeaderViewModel).mockReturnValue({
|
||||
onMemberAvatarClick: jest.fn(),
|
||||
precenseInfo: {
|
||||
lastActiveAgo: undefined,
|
||||
currentlyActive: undefined,
|
||||
state: undefined,
|
||||
},
|
||||
showPresence: false,
|
||||
timezoneInfo: {
|
||||
timezone: "FR",
|
||||
friendly: "paris",
|
||||
},
|
||||
userIdentifier: null,
|
||||
});
|
||||
|
||||
renderComponent({ hideVerificationSection: false });
|
||||
expect(screen.getByText("paris")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render correct presence label", () => {
|
||||
mocked(useUserfoHeaderViewModel).mockReturnValue({
|
||||
onMemberAvatarClick: jest.fn(),
|
||||
precenseInfo: {
|
||||
lastActiveAgo: 0,
|
||||
currentlyActive: true,
|
||||
state: "online",
|
||||
},
|
||||
showPresence: true,
|
||||
timezoneInfo: null,
|
||||
userIdentifier: null,
|
||||
});
|
||||
|
||||
renderComponent({ hideVerificationSection: false });
|
||||
expect(screen.getByText("Online")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should be able to click on member avatar", () => {
|
||||
const onMemberAvatarClick = jest.fn();
|
||||
mocked(useUserfoHeaderViewModel).mockReturnValue({
|
||||
onMemberAvatarClick,
|
||||
precenseInfo: {
|
||||
lastActiveAgo: undefined,
|
||||
currentlyActive: undefined,
|
||||
state: undefined,
|
||||
},
|
||||
showPresence: false,
|
||||
timezoneInfo: {
|
||||
timezone: "FR",
|
||||
friendly: "paris",
|
||||
},
|
||||
userIdentifier: null,
|
||||
});
|
||||
renderComponent();
|
||||
const avatar = screen.getByRole("button", { name: "Profile picture" });
|
||||
|
||||
fireEvent.click(avatar);
|
||||
|
||||
expect(onMemberAvatarClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@ -614,270 +614,3 @@ exports[`<UserInfo /> with crypto enabled should render a deactivate button for
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<UserInfoHeader /> renders verification unavailable message 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_UserInfo_avatar"
|
||||
>
|
||||
<div
|
||||
class="mx_UserInfo_avatar_transition"
|
||||
>
|
||||
<div
|
||||
class="mx_UserInfo_avatar_transition_child"
|
||||
>
|
||||
<button
|
||||
aria-label="Profile picture"
|
||||
aria-live="off"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar _avatar-imageless_1qbcf_52"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="button"
|
||||
style="--cpd-avatar-size: 120px;"
|
||||
title="customUserIdentifier"
|
||||
>
|
||||
u
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_UserInfo_container mx_UserInfo_header"
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_UserInfo_profile"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<h1
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
dir="auto"
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_UserInfo_profile_name"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row-reverse; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
@user:example.com
|
||||
</div>
|
||||
</h1>
|
||||
<div
|
||||
class="mx_PresenceLabel mx_UserInfo_profileStatus"
|
||||
>
|
||||
Unknown
|
||||
</div>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-sm-semibold_6v6n8_36 mx_UserInfo_profile_mxid"
|
||||
>
|
||||
<div
|
||||
class="mx_CopyableText"
|
||||
>
|
||||
customUserIdentifier
|
||||
<div
|
||||
aria-label="Copy"
|
||||
class="mx_AccessibleButton mx_CopyableText_copyButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Flex mx_UserInfo_verification"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-sm-regular_6v6n8_31 mx_UserInfo_verification_unavailable"
|
||||
>
|
||||
(
|
||||
User verification unavailable
|
||||
)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<UserInfoHeader /> renders verified badge when user is verified 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_UserInfo_avatar"
|
||||
>
|
||||
<div
|
||||
class="mx_UserInfo_avatar_transition"
|
||||
>
|
||||
<div
|
||||
class="mx_UserInfo_avatar_transition_child"
|
||||
>
|
||||
<button
|
||||
aria-label="Profile picture"
|
||||
aria-live="off"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar _avatar-imageless_1qbcf_52"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="button"
|
||||
style="--cpd-avatar-size: 120px;"
|
||||
title="customUserIdentifier"
|
||||
>
|
||||
u
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_UserInfo_container mx_UserInfo_header"
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_UserInfo_profile"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<h1
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
dir="auto"
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_UserInfo_profile_name"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row-reverse; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
@user:example.com
|
||||
</div>
|
||||
</h1>
|
||||
<div
|
||||
class="mx_PresenceLabel mx_UserInfo_profileStatus"
|
||||
>
|
||||
Unknown
|
||||
</div>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-sm-semibold_6v6n8_36 mx_UserInfo_profile_mxid"
|
||||
>
|
||||
<div
|
||||
class="mx_CopyableText"
|
||||
>
|
||||
customUserIdentifier
|
||||
<div
|
||||
aria-label="Copy"
|
||||
class="mx_AccessibleButton mx_CopyableText_copyButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Flex mx_UserInfo_verification"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-sm-medium_6v6n8_41 _badge_1t12g_8 mx_UserInfo_verified_badge"
|
||||
data-kind="green"
|
||||
>
|
||||
<svg
|
||||
class="mx_UserInfo_verified_icon"
|
||||
fill="currentColor"
|
||||
height="16px"
|
||||
viewBox="0 0 24 24"
|
||||
width="16px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8.15 21.75 6.7 19.3l-2.75-.6a.94.94 0 0 1-.6-.387.93.93 0 0 1-.175-.688L3.45 14.8l-1.875-2.15a.93.93 0 0 1-.25-.65q0-.375.25-.65L3.45 9.2l-.275-2.825a.93.93 0 0 1 .175-.687.94.94 0 0 1 .6-.388l2.75-.6 1.45-2.45a.98.98 0 0 1 .55-.437.97.97 0 0 1 .7.037l2.6 1.1 2.6-1.1a.97.97 0 0 1 .7-.038q.35.112.55.438L17.3 4.7l2.75.6q.375.075.6.388.225.312.175.687L20.55 9.2l1.875 2.15q.25.275.25.65t-.25.65L20.55 14.8l.275 2.825a.93.93 0 0 1-.175.688.94.94 0 0 1-.6.387l-2.75.6-1.45 2.45a.98.98 0 0 1-.55.438.97.97 0 0 1-.7-.038l-2.6-1.1-2.6 1.1a.97.97 0 0 1-.7.038.98.98 0 0 1-.55-.438m2.8-9.05L9.5 11.275A.93.93 0 0 0 8.812 11q-.412 0-.712.3a.95.95 0 0 0-.275.7q0 .425.275.7l2.15 2.15q.3.3.7.3t.7-.3l4.25-4.25q.3-.3.287-.7a1.06 1.06 0 0 0-.287-.7 1.02 1.02 0 0 0-.713-.312.93.93 0 0 0-.712.287z"
|
||||
/>
|
||||
</svg>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-sm-medium_6v6n8_41 mx_UserInfo_verified_label"
|
||||
>
|
||||
Verified
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<UserInfoHeader /> renders verify button 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_UserInfo_avatar"
|
||||
>
|
||||
<div
|
||||
class="mx_UserInfo_avatar_transition"
|
||||
>
|
||||
<div
|
||||
class="mx_UserInfo_avatar_transition_child"
|
||||
>
|
||||
<button
|
||||
aria-label="Profile picture"
|
||||
aria-live="off"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar _avatar-imageless_1qbcf_52"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="button"
|
||||
style="--cpd-avatar-size: 120px;"
|
||||
title="customUserIdentifier"
|
||||
>
|
||||
u
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_UserInfo_container mx_UserInfo_header"
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_UserInfo_profile"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<h1
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
dir="auto"
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_UserInfo_profile_name"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row-reverse; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
@user:example.com
|
||||
</div>
|
||||
</h1>
|
||||
<div
|
||||
class="mx_PresenceLabel mx_UserInfo_profileStatus"
|
||||
>
|
||||
Unknown
|
||||
</div>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-sm-semibold_6v6n8_36 mx_UserInfo_profile_mxid"
|
||||
>
|
||||
<div
|
||||
class="mx_CopyableText"
|
||||
>
|
||||
customUserIdentifier
|
||||
<div
|
||||
aria-label="Copy"
|
||||
class="mx_AccessibleButton mx_CopyableText_copyButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Flex mx_UserInfo_verification"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_UserInfo_container_verifyButton"
|
||||
>
|
||||
<button
|
||||
class="_button_vczzf_8 mx_UserInfo_verify_button"
|
||||
data-kind="tertiary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Verify User
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@ -0,0 +1,73 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<UserInfoHeaderVerificationView /> renders verification unavailable message 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_Flex mx_UserInfo_verification"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-sm-regular_6v6n8_31 mx_UserInfo_verification_unavailable"
|
||||
>
|
||||
(
|
||||
User verification unavailable
|
||||
)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<UserInfoHeaderVerificationView /> renders verified badge when user is verified 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_Flex mx_UserInfo_verification"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-sm-medium_6v6n8_41 _badge_1t12g_8 mx_UserInfo_verified_badge"
|
||||
data-kind="green"
|
||||
>
|
||||
<svg
|
||||
class="mx_UserInfo_verified_icon"
|
||||
fill="currentColor"
|
||||
height="16px"
|
||||
viewBox="0 0 24 24"
|
||||
width="16px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8.15 21.75 6.7 19.3l-2.75-.6a.94.94 0 0 1-.6-.387.93.93 0 0 1-.175-.688L3.45 14.8l-1.875-2.15a.93.93 0 0 1-.25-.65q0-.375.25-.65L3.45 9.2l-.275-2.825a.93.93 0 0 1 .175-.687.94.94 0 0 1 .6-.388l2.75-.6 1.45-2.45a.98.98 0 0 1 .55-.437.97.97 0 0 1 .7.037l2.6 1.1 2.6-1.1a.97.97 0 0 1 .7-.038q.35.112.55.438L17.3 4.7l2.75.6q.375.075.6.388.225.312.175.687L20.55 9.2l1.875 2.15q.25.275.25.65t-.25.65L20.55 14.8l.275 2.825a.93.93 0 0 1-.175.688.94.94 0 0 1-.6.387l-2.75.6-1.45 2.45a.98.98 0 0 1-.55.438.97.97 0 0 1-.7-.038l-2.6-1.1-2.6 1.1a.97.97 0 0 1-.7.038.98.98 0 0 1-.55-.438m2.8-9.05L9.5 11.275A.93.93 0 0 0 8.812 11q-.412 0-.712.3a.95.95 0 0 0-.275.7q0 .425.275.7l2.15 2.15q.3.3.7.3t.7-.3l4.25-4.25q.3-.3.287-.7a1.06 1.06 0 0 0-.287-.7 1.02 1.02 0 0 0-.713-.312.93.93 0 0 0-.712.287z"
|
||||
/>
|
||||
</svg>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-sm-medium_6v6n8_41 mx_UserInfo_verified_label"
|
||||
>
|
||||
Verified
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<UserInfoHeaderVerificationView /> renders verify button 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_Flex mx_UserInfo_verification"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<div
|
||||
class="mx_UserInfo_container_verifyButton"
|
||||
>
|
||||
<button
|
||||
class="_button_vczzf_8 mx_UserInfo_verify_button"
|
||||
data-kind="tertiary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Verify User
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -0,0 +1,86 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<UserInfoHeaderView /> renders custom user identifiers in the header 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_UserInfo_avatar"
|
||||
>
|
||||
<div
|
||||
class="mx_UserInfo_avatar_transition"
|
||||
>
|
||||
<div
|
||||
class="mx_UserInfo_avatar_transition_child"
|
||||
>
|
||||
<button
|
||||
aria-label="Profile picture"
|
||||
aria-live="off"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar _avatar-imageless_1qbcf_52"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="button"
|
||||
style="--cpd-avatar-size: 120px;"
|
||||
title="@user:example.com"
|
||||
>
|
||||
u
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_UserInfo_container mx_UserInfo_header"
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_UserInfo_profile"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<h1
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
dir="auto"
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_UserInfo_profile_name"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row-reverse; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
@user:example.com
|
||||
</div>
|
||||
</h1>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-sm-semibold_6v6n8_36 mx_UserInfo_profile_mxid"
|
||||
>
|
||||
<div
|
||||
class="mx_CopyableText"
|
||||
>
|
||||
customUserIdentifier
|
||||
<div
|
||||
aria-label="Copy"
|
||||
class="mx_AccessibleButton mx_CopyableText_copyButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Flex mx_UserInfo_verification"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<svg
|
||||
class="_icon_11k6c_18"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
style="width: 24px; height: 24px;"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M12 4.031a8 8 0 1 0 8 8 1 1 0 0 1 2 0c0 5.523-4.477 10-10 10s-10-4.477-10-10 4.477-10 10-10a1 1 0 1 1 0 2"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
Loading…
Reference in New Issue
Block a user