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:
Marc
2025-07-21 14:04:50 +02:00
committed by GitHub
parent 8a879c7fca
commit 0a97cbaada
13 changed files with 1189 additions and 533 deletions

View File

@@ -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,
};
};

View File

@@ -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,
};
}