/* Copyright 2024 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 { useEffect, useMemo, useState } from "react"; import { RoomStateEvent, MatrixEvent, EventType } from "matrix-js-sdk/src/matrix"; import { UserVerificationStatus, CryptoEvent } from "matrix-js-sdk/src/crypto-api"; import dis from "../../../../dispatcher/dispatcher"; import { MatrixClientPeg } from "../../../../MatrixClientPeg"; import { Action } from "../../../../dispatcher/actions"; import { asyncSome } from "../../../../utils/arrays"; import { getUserDeviceIds } from "../../../../utils/crypto/deviceInfo"; import { RoomMember } from "../../../../models/rooms/RoomMember"; import { _t, _td, TranslationKey } from "../../../../languageHandler"; import UserIdentifierCustomisations from "../../../../customisations/UserIdentifier"; import { E2EStatus } from "../../../../utils/ShieldUtils"; interface MemberTileViewModelProps { member: RoomMember; showPresence?: boolean; } export interface MemberTileViewState extends MemberTileViewModelProps { e2eStatus?: E2EStatus; name: string; onClick: () => void; title?: string; userLabel?: string; } export enum PowerStatus { Admin = "admin", Moderator = "moderator", } const PowerLabel: Record = { [PowerStatus.Admin]: _td("power_level|admin"), [PowerStatus.Moderator]: _td("power_level|moderator"), }; export function useMemberTileViewModel(props: MemberTileViewModelProps): MemberTileViewState { const [e2eStatus, setE2eStatus] = useState(); useEffect(() => { const cli = MatrixClientPeg.safeGet(); const updateE2EStatus = async (): Promise => { const { userId } = props.member; const isMe = userId === cli.getUserId(); const userTrust = await cli.getCrypto()?.getUserVerificationStatus(userId); if (!userTrust?.isCrossSigningVerified()) { setE2eStatus(userTrust?.wasCrossSigningVerified() ? E2EStatus.Warning : E2EStatus.Normal); return; } const deviceIDs = await getUserDeviceIds(cli, userId); const anyDeviceUnverified = await asyncSome(deviceIDs, async (deviceId) => { // 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 cli.getCrypto()?.getDeviceVerificationStatus(userId, deviceId); return !deviceTrust || (isMe ? !deviceTrust.crossSigningVerified : !deviceTrust.isVerified()); }); setE2eStatus(anyDeviceUnverified ? E2EStatus.Warning : E2EStatus.Verified); }; const onRoomStateEvents = (ev: MatrixEvent): void => { if (ev.getType() !== EventType.RoomEncryption) return; const { roomId } = props.member; if (ev.getRoomId() !== roomId) return; // The room is encrypted now. cli.removeListener(RoomStateEvent.Events, onRoomStateEvents); updateE2EStatus(); }; const onUserTrustStatusChanged = (userId: string, trustStatus: UserVerificationStatus): void => { if (userId !== props.member.userId) return; updateE2EStatus(); }; const { roomId } = props.member; if (roomId) { const isRoomEncrypted = cli.isRoomEncrypted(roomId); if (isRoomEncrypted) { cli.on(CryptoEvent.UserTrustStatusChanged, onUserTrustStatusChanged); updateE2EStatus(); } else { // Listen for room to become encrypted cli.on(RoomStateEvent.Events, onRoomStateEvents); } } return () => { if (cli) { cli.removeListener(RoomStateEvent.Events, onRoomStateEvents); cli.removeListener(CryptoEvent.UserTrustStatusChanged, onUserTrustStatusChanged); } }; }, [props.member]); const onClick = (): void => { dis.dispatch({ action: Action.ViewUser, member: props.member, push: true, }); }; const member = props.member; const name = props.member.name; const powerStatusMap = new Map([ [100, PowerStatus.Admin], [50, PowerStatus.Moderator], ]); // Find the nearest power level with a badge let powerLevel = props.member.powerLevel; for (const [pl] of powerStatusMap) { if (props.member.powerLevel >= pl) { powerLevel = pl; break; } } const title = useMemo(() => { return _t("member_list|power_label", { userName: UserIdentifierCustomisations.getDisplayUserIdentifier(member.userId, { roomId: member.roomId, }), powerLevelNumber: member.powerLevel, }).trim(); }, [member.powerLevel, member.roomId, member.userId]); let userLabel; const powerStatus = powerStatusMap.get(powerLevel); if (powerStatus) { userLabel = _t(PowerLabel[powerStatus]); } if (props.member.isInvite) { userLabel = `(${_t("member_list|invited_label")})`; } return { title, member, name, onClick, e2eStatus, showPresence: props.showPresence, userLabel, }; }