/* Copyright 2024 New Vector Ltd. Copyright 2019, 2020 The Matrix.org Foundation C.I.C. 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, { useCallback, useEffect, useRef, useState } from "react"; import { VerificationPhase, VerificationRequest, VerificationRequestEvent } from "matrix-js-sdk/src/crypto-api"; import { RoomMember, User } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import EncryptionInfo from "./EncryptionInfo"; import VerificationPanel from "./VerificationPanel"; import { ensureDMExists } from "../../../createRoom"; import { useTypedEventEmitter } from "../../../hooks/useEventEmitter"; import Modal from "../../../Modal"; import { _t } from "../../../languageHandler"; import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases"; import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; import ErrorDialog from "../dialogs/ErrorDialog"; import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; import WarningDeprecatedSvg from "../../../../res/img/e2e/warning-deprecated.svg"; import WarningSvg from "../../../../res/img/e2e/warning.svg"; // cancellation codes which constitute a key mismatch const MISMATCHES = ["m.key_mismatch", "m.user_error", "m.mismatched_sas"]; interface IProps { member: RoomMember | User; onClose: () => void; verificationRequest?: VerificationRequest; verificationRequestPromise?: Promise; layout: string; isRoomEncrypted: boolean; } const EncryptionPanel: React.FC = (props: IProps) => { const cli = useMatrixClientContext(); const { verificationRequest, verificationRequestPromise, member, onClose, layout, isRoomEncrypted } = props; const [request, setRequest] = useState(verificationRequest); // state to show a spinner immediately after clicking "start verification", // before we have a request const [isRequesting, setRequesting] = useState(false); const [phase, doSetPhase] = useState(request?.phase); const setPhase = (phase: VerificationPhase | undefined): void => { logger.debug(`EncryptionPanel: phase now ${phase === undefined ? phase : VerificationPhase[phase]}`); doSetPhase(phase); }; useEffect(() => { setRequest(verificationRequest); if (verificationRequest) { setRequesting(false); setPhase(verificationRequest.phase); } }, [verificationRequest]); useEffect(() => { async function awaitPromise(): Promise { setRequesting(true); const requestFromPromise = await verificationRequestPromise; setRequesting(false); setRequest(requestFromPromise); setPhase(requestFromPromise?.phase); } if (verificationRequestPromise) { awaitPromise(); } }, [verificationRequestPromise]); // Use a ref to track whether we are already showing the mismatch modal as state may not update fast enough // if two change events are fired in quick succession like can happen with rust crypto. const isShowingMismatchModal = useRef(false); const changeHandler = useCallback(() => { // handle transitions -> cancelled for mismatches which fire a modal instead of showing a card if ( !isShowingMismatchModal.current && request?.phase === VerificationPhase.Cancelled && MISMATCHES.includes(request.cancellationCode ?? "") ) { isShowingMismatchModal.current = true; Modal.createDialog(ErrorDialog, { headerImage: WarningDeprecatedSvg, title: _t("encryption|messages_not_secure|title"), description: (
{_t("encryption|messages_not_secure|heading")}
  • {_t("encryption|messages_not_secure|cause_1")}
  • {_t("encryption|messages_not_secure|cause_2")}
  • {_t("encryption|messages_not_secure|cause_3")}
  • {_t("encryption|messages_not_secure|cause_4")}
), onFinished: onClose, }); return; // don't update phase here as we will be transitioning away from this view shortly } if (request) { setPhase(request.phase); } }, [onClose, request]); useTypedEventEmitter(request, VerificationRequestEvent.Change, changeHandler); const onStartVerification = useCallback(async (): Promise => { setRequesting(true); let verificationRequest_: VerificationRequest; try { const roomId = await ensureDMExists(cli, member.userId); if (!roomId) { throw new Error("Unable to create Room for verification"); } verificationRequest_ = await cli.getCrypto()!.requestVerificationDM(member.userId, roomId); } catch (e) { console.error("Error starting verification", e); setRequesting(false); Modal.createDialog(ErrorDialog, { headerImage: WarningSvg, title: _t("encryption|verification|error_starting_title"), description: _t("encryption|verification|error_starting_description"), }); return; } setRequest(verificationRequest_); setPhase(verificationRequest_.phase); // Notify the RightPanelStore about this if (RightPanelStore.instance.currentCard.phase != RightPanelPhases.EncryptionPanel) { RightPanelStore.instance.pushCard({ phase: RightPanelPhases.EncryptionPanel, state: { member, verificationRequest: verificationRequest_ }, }); } if (!RightPanelStore.instance.isOpen) RightPanelStore.instance.togglePanel(null); }, [cli, member]); const requested: boolean = (!request && isRequesting) || (!!request && (phase === VerificationPhase.Requested || phase === VerificationPhase.Unsent || phase === undefined)); const isSelfVerification = request ? request.isSelfVerification : member.userId === cli.getUserId(); if (!request || requested) { const initiatedByMe = (!request && isRequesting) || (!!request && request.initiatedByMe); return ( ); } else { return ( ); } }; export default EncryptionPanel;