/* Copyright 2024 New Vector Ltd. Copyright 2019, 2020 The Matrix.org Foundation C.I.C. Copyright 2015, 2016 OpenMarket 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 React from "react"; import { RoomMember, RoomStateEvent, MatrixEvent, EventType } from "matrix-js-sdk/src/matrix"; import { CryptoEvent, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api"; import dis from "../../../dispatcher/dispatcher"; import { _t } from "../../../languageHandler"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { Action } from "../../../dispatcher/actions"; import EntityTile, { PowerStatus, PresenceState } from "./EntityTile"; import MemberAvatar from "./../avatars/MemberAvatar"; import DisambiguatedProfile from "../messages/DisambiguatedProfile"; import UserIdentifierCustomisations from "../../../customisations/UserIdentifier"; import { E2EState } from "./E2EIcon"; import { asyncSome } from "../../../utils/arrays"; import { getUserDeviceIds } from "../../../utils/crypto/deviceInfo"; interface IProps { member: RoomMember; showPresence?: boolean; } interface IState { isRoomEncrypted: boolean; e2eStatus?: E2EState; } export default class MemberTile extends React.Component { private userLastModifiedTime?: number; private memberLastModifiedTime?: number; public static defaultProps = { showPresence: true, }; public constructor(props: IProps) { super(props); this.state = { isRoomEncrypted: false, }; } public componentDidMount(): void { const cli = MatrixClientPeg.safeGet(); const { roomId } = this.props.member; if (roomId) { const isRoomEncrypted = cli.isRoomEncrypted(roomId); this.setState({ isRoomEncrypted, }); if (isRoomEncrypted) { cli.on(CryptoEvent.UserTrustStatusChanged, this.onUserTrustStatusChanged); this.updateE2EStatus(); } else { // Listen for room to become encrypted cli.on(RoomStateEvent.Events, this.onRoomStateEvents); } } } public componentWillUnmount(): void { const cli = MatrixClientPeg.get(); if (cli) { cli.removeListener(RoomStateEvent.Events, this.onRoomStateEvents); cli.removeListener(CryptoEvent.UserTrustStatusChanged, this.onUserTrustStatusChanged); } } private onRoomStateEvents = (ev: MatrixEvent): void => { if (ev.getType() !== EventType.RoomEncryption) return; const { roomId } = this.props.member; if (ev.getRoomId() !== roomId) return; // The room is encrypted now. const cli = MatrixClientPeg.safeGet(); cli.removeListener(RoomStateEvent.Events, this.onRoomStateEvents); this.setState({ isRoomEncrypted: true, }); this.updateE2EStatus(); }; private onUserTrustStatusChanged = (userId: string, trustStatus: UserVerificationStatus): void => { if (userId !== this.props.member.userId) return; this.updateE2EStatus(); }; private async updateE2EStatus(): Promise { const cli = MatrixClientPeg.safeGet(); const { userId } = this.props.member; const isMe = userId === cli.getUserId(); const userTrust = await cli.getCrypto()?.getUserVerificationStatus(userId); if (!userTrust?.isCrossSigningVerified()) { this.setState({ e2eStatus: userTrust?.wasCrossSigningVerified() ? E2EState.Warning : E2EState.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()); }); this.setState({ e2eStatus: anyDeviceUnverified ? E2EState.Warning : E2EState.Verified, }); } public shouldComponentUpdate(nextProps: IProps, nextState: IState): boolean { if ( this.memberLastModifiedTime === undefined || this.memberLastModifiedTime < nextProps.member.getLastModifiedTime() ) { return true; } if ( nextProps.member.user && (this.userLastModifiedTime === undefined || this.userLastModifiedTime < nextProps.member.user.getLastModifiedTime()) ) { return true; } if (nextState.isRoomEncrypted !== this.state.isRoomEncrypted || nextState.e2eStatus !== this.state.e2eStatus) { return true; } return false; } private onClick = (): void => { dis.dispatch({ action: Action.ViewUser, member: this.props.member, push: true, }); }; private getDisplayName(): string { return this.props.member.name; } private getPowerLabel(): string { return _t("member_list|power_label", { userName: UserIdentifierCustomisations.getDisplayUserIdentifier(this.props.member.userId, { roomId: this.props.member.roomId, }), powerLevelNumber: this.props.member.powerLevel, }).trim(); } public render(): React.ReactNode { const member = this.props.member; const name = this.getDisplayName(); const presenceState = member.user?.presence as PresenceState | undefined; const av =